Skip to content

Commit

Permalink
add 'vscode.executeLinkProvider' command
Browse files Browse the repository at this point in the history
  • Loading branch information
jrieken committed Jul 25, 2016
1 parent c1b999f commit d906704
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 100 deletions.
3 changes: 1 addition & 2 deletions src/vs/editor/common/services/editorWorkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {createDecorator} from 'vs/platform/instantiation/common/instantiation';
import {IChange, ILineChange, IPosition, IRange} from 'vs/editor/common/editorCommon';
import {IInplaceReplaceSupportResult, ILink, ISuggestResult} from 'vs/editor/common/modes';
import {IInplaceReplaceSupportResult, ISuggestResult} from 'vs/editor/common/modes';

export var ID_EDITOR_WORKER_SERVICE = 'editorWorkerService';
export var IEditorWorkerService = createDecorator<IEditorWorkerService>(ID_EDITOR_WORKER_SERVICE);
Expand All @@ -18,7 +18,6 @@ export interface IEditorWorkerService {

computeDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise<ILineChange[]>;
computeDirtyDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise<IChange[]>;
computeLinks(resource:URI):TPromise<ILink[]>;
textualSuggest(resource: URI, position: IPosition): TPromise<ISuggestResult[]>;
navigateValueSet(resource: URI, range:IRange, up:boolean): TPromise<IInplaceReplaceSupportResult>;
}
23 changes: 16 additions & 7 deletions src/vs/editor/common/services/editorWorkerServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*--------------------------------------------------------------------------------------------*/
'use strict';

import {IntervalTimer, ShallowCancelThenPromise} from 'vs/base/common/async';
import {IntervalTimer, ShallowCancelThenPromise, wireCancellationToken} from 'vs/base/common/async';
import {Disposable, IDisposable, dispose} from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {SimpleWorkerClient} from 'vs/base/common/worker/simpleWorker';
import {DefaultWorkerFactory} from 'vs/base/worker/defaultWorkerFactory';
import * as editorCommon from 'vs/editor/common/editorCommon';
import {WordHelper} from 'vs/editor/common/model/textModelWithTokensHelpers';
import {IInplaceReplaceSupportResult, ILink, ISuggestResult} from 'vs/editor/common/modes';
import {IInplaceReplaceSupportResult, ILink, ISuggestResult, LinkProviderRegistry, LinkProvider} from 'vs/editor/common/modes';
import {IEditorWorkerService} from 'vs/editor/common/services/editorWorkerService';
import {IModelService} from 'vs/editor/common/services/modelService';
import {EditorSimpleWorkerImpl} from 'vs/editor/common/services/editorSimpleWorker';
Expand All @@ -31,10 +31,23 @@ const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000;
export class EditorWorkerServiceImpl implements IEditorWorkerService {
public _serviceBrand: any;

private _workerManager:WorkerManager;
private _workerManager: WorkerManager;
private _registration: IDisposable;

constructor(@IModelService modelService:IModelService) {
this._workerManager = new WorkerManager(modelService);

// todo@joh make sure this happens only once
this._registration = LinkProviderRegistry.register('*', <LinkProvider>{
provideLinks: (model, token) => {
return wireCancellationToken(token, this._workerManager.withWorker().then(client => client.computeLinks(model.uri)));
}
});
}

public dispose(): void {
this._workerManager.dispose();
this._registration.dispose();
}

public computeDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise<editorCommon.ILineChange[]> {
Expand All @@ -45,10 +58,6 @@ export class EditorWorkerServiceImpl implements IEditorWorkerService {
return this._workerManager.withWorker().then(client => client.computeDirtyDiff(original, modified, ignoreTrimWhitespace));
}

public computeLinks(resource:URI):TPromise<ILink[]> {
return this._workerManager.withWorker().then(client => client.computeLinks(resource));
}

public textualSuggest(resource: URI, position: editorCommon.IPosition): TPromise<ISuggestResult[]> {
return this._workerManager.withWorker().then(client => client.textualSuggest(resource, position));
}
Expand Down
78 changes: 4 additions & 74 deletions src/vs/editor/contrib/links/browser/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {TPromise} from 'vs/base/common/winjs.base';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {IEditorService, IResourceInput} from 'vs/platform/editor/common/editor';
import {IMessageService} from 'vs/platform/message/common/message';
import {Range} from 'vs/editor/common/core/range';
import {EditorAction} from 'vs/editor/common/editorAction';
import {Behaviour} from 'vs/editor/common/editorActionEnablement';
import * as editorCommon from 'vs/editor/common/editorCommon';
Expand Down Expand Up @@ -75,16 +74,6 @@ class LinkOccurence {
}
}

class Link {
range: Range;
url: string;

constructor(source:ILink) {
this.range = new Range(source.range.startLineNumber, source.range.startColumn, source.range.endLineNumber, source.range.endColumn);
this.url = source.url;
}
}

class LinkDetector implements editorCommon.IEditorContribution {

public static ID: string = 'editor.linkDetector';
Expand All @@ -102,7 +91,7 @@ class LinkDetector implements editorCommon.IEditorContribution {
private editor:ICodeEditor;
private listenersToRemove:IDisposable[];
private timeoutPromise:TPromise<void>;
private computePromise:TPromise<ILink[]>;
private computePromise:TPromise<void>;
private activeLinkDecorationId:string;
private lastMouseEvent:IEditorMouseEvent;
private editorService:IEditorService;
Expand Down Expand Up @@ -172,75 +161,16 @@ class LinkDetector implements editorCommon.IEditorContribution {
return;
}

let modePromise:TPromise<ILink[]> = TPromise.as(null);
if (LinkProviderRegistry.has(this.editor.getModel())) {
modePromise = getLinks(this.editor.getModel());
if (!LinkProviderRegistry.has(this.editor.getModel())) {
return;
}

let standardPromise:TPromise<ILink[]> = this.editorWorkerService.computeLinks(this.editor.getModel().uri);

this.computePromise = TPromise.join([modePromise, standardPromise]).then((r) => {
let a = r[0];
let b = r[1];
if (!a || a.length === 0) {
return b || [];
}
if (!b || b.length === 0) {
return a || [];
}
return LinkDetector._linksUnion(a.map(el => new Link(el)), b.map(el => new Link(el)));
});

this.computePromise.then((links:ILink[]) => {
this.computePromise = getLinks(this.editor.getModel()).then(links => {
this.updateDecorations(links);
this.computePromise = null;
});
}

private static _linksUnion(oldLinks: Link[], newLinks: Link[]): Link[] {
// reunite oldLinks with newLinks and remove duplicates
var result: Link[] = [],
oldIndex: number,
oldLen: number,
newIndex: number,
newLen: number,
oldLink: Link,
newLink: Link,
comparisonResult: number;

for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) {
oldLink = oldLinks[oldIndex];
newLink = newLinks[newIndex];

if (Range.areIntersectingOrTouching(oldLink.range, newLink.range)) {
// Remove the oldLink
oldIndex++;
continue;
}

comparisonResult = Range.compareRangesUsingStarts(oldLink.range, newLink.range);

if (comparisonResult < 0) {
// oldLink is before
result.push(oldLink);
oldIndex++;
} else {
// newLink is before
result.push(newLink);
newIndex++;
}
}

for (; oldIndex < oldLen; oldIndex++) {
result.push(oldLinks[oldIndex]);
}
for (; newIndex < newLen; newIndex++) {
result.push(newLinks[newIndex]);
}

return result;
}

private updateDecorations(links:ILink[]):void {
this.editor.changeDecorations((changeAccessor:editorCommon.IModelDecorationsChangeAccessor) => {
var oldDecorations:string[] = [];
Expand Down
88 changes: 72 additions & 16 deletions src/vs/editor/contrib/links/common/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,88 @@
'use strict';

import {onUnexpectedError} from 'vs/base/common/errors';
import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {Range} from 'vs/editor/common/core/range';
import {IReadOnlyModel} from 'vs/editor/common/editorCommon';
import {ILink, LinkProviderRegistry} from 'vs/editor/common/modes';
import {asWinJsPromise} from 'vs/base/common/async';
import {CommandsRegistry} from 'vs/platform/commands/common/commands';
import {IModelService} from 'vs/editor/common/services/modelService';

export function getLinks(model: IReadOnlyModel): TPromise<ILink[]> {

const promises = LinkProviderRegistry.ordered(model).map((support) => {
return asWinJsPromise((token) => {
return support.provideLinks(model, token);
}).then((result) => {
let links: ILink[] = [];

// ask all providers for links in parallel
const promises = LinkProviderRegistry.ordered(model).reverse().map(support => {
return asWinJsPromise(token => support.provideLinks(model, token)).then(result => {
if (Array.isArray(result)) {
return <ILink[]> result;
links = union(links, result);
}
}, err => {
onUnexpectedError(err);
});
}, onUnexpectedError);
});

return TPromise.join(promises).then(manyLinks => {
let result: ILink[] = [];
for (let links of manyLinks) {
if (links) {
result = result.concat(links);
}
}
return result;
return TPromise.join(promises).then(() => {
return links;
});
}

function union(oldLinks: ILink[], newLinks: ILink[]): ILink[] {
// reunite oldLinks with newLinks and remove duplicates
var result: ILink[] = [],
oldIndex: number,
oldLen: number,
newIndex: number,
newLen: number,
oldLink: ILink,
newLink: ILink,
comparisonResult: number;

for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) {
oldLink = oldLinks[oldIndex];
newLink = newLinks[newIndex];

if (Range.areIntersectingOrTouching(oldLink.range, newLink.range)) {
// Remove the oldLink
oldIndex++;
continue;
}

comparisonResult = Range.compareRangesUsingStarts(oldLink.range, newLink.range);

if (comparisonResult < 0) {
// oldLink is before
result.push(oldLink);
oldIndex++;
} else {
// newLink is before
result.push(newLink);
newIndex++;
}
}

for (; oldIndex < oldLen; oldIndex++) {
result.push(oldLinks[oldIndex]);
}
for (; newIndex < newLen; newIndex++) {
result.push(newLinks[newIndex]);
}

return result;
}

CommandsRegistry.registerCommand('_executeLinkProvider', (accessor, ...args) => {

const [uri] = args;
if (!(uri instanceof URI)) {
return;
}

const model = accessor.get(IModelService).getModel(uri);
if (!model) {
return;
}

return getLinks(model);
});
16 changes: 15 additions & 1 deletion src/vs/workbench/api/node/extHostApiCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ class ExtHostApiCommands {
],
returns: 'A promise that resolves to an array of TextEdits.'
});

this._register('vscode.executeLinkProvider', this._executeDocumentLinkProvider, {
description: 'Execute document link provider.',
args: [
{ name: 'uri', description: 'Uri of a text document', constraint: URI }
],
returns: 'A promise that resolves to an array of DocumentLink-instances.'
});

this._register('vscode.previewHtml', (uri: URI, position?: vscode.ViewColumn, label?: string) => {
return this._commands.executeCommand('_workbench.previewHtml',
Expand Down Expand Up @@ -417,4 +423,12 @@ class ExtHostApiCommands {
}
});
}

private _executeDocumentLinkProvider(resource: URI): Thenable<vscode.DocumentLink[]> {
return this._commands.executeCommand<modes.ILink[]>('_executeLinkProvider', resource).then(value => {
if (Array.isArray(value)) {
return value.map(typeConverters.DocumentLink.to);
}
});
}
}
22 changes: 22 additions & 0 deletions src/vs/workbench/test/node/api/extHostApiCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,26 @@ suite('ExtHostLanguageFeatureCommands', function() {
});
});
});

test('Links, back and forth', function() {

disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, <vscode.DocumentLinkProvider>{
provideDocumentLinks(): any {
return [new types.DocumentLink(new types.Range(0, 0, 0, 20), URI.parse('foo:bar'))];
}
}));

return threadService.sync().then(() => {
return commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', model.uri).then(value => {
assert.equal(value.length, 1);
let [first] = value;

assert.equal(first.target.toString(), 'foo:bar');
assert.equal(first.range.start.line, 0);
assert.equal(first.range.start.character, 0);
assert.equal(first.range.end.line, 0);
assert.equal(first.range.end.character, 20);
});
});
});
});

0 comments on commit d906704

Please sign in to comment.