diff --git a/src/vs/editor/common/services/editorWorkerService.ts b/src/vs/editor/common/services/editorWorkerService.ts index 7f4594e6129..6ae27e9af0f 100644 --- a/src/vs/editor/common/services/editorWorkerService.ts +++ b/src/vs/editor/common/services/editorWorkerService.ts @@ -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(ID_EDITOR_WORKER_SERVICE); @@ -18,7 +18,6 @@ export interface IEditorWorkerService { computeDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise; computeDirtyDiff(original:URI, modified:URI, ignoreTrimWhitespace:boolean):TPromise; - computeLinks(resource:URI):TPromise; textualSuggest(resource: URI, position: IPosition): TPromise; navigateValueSet(resource: URI, range:IRange, up:boolean): TPromise; } diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index 3616b833aec..88988dfecef 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ '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'; @@ -12,7 +12,7 @@ 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'; @@ -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('*', { + 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 { @@ -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 { - return this._workerManager.withWorker().then(client => client.computeLinks(resource)); - } - public textualSuggest(resource: URI, position: editorCommon.IPosition): TPromise { return this._workerManager.withWorker().then(client => client.textualSuggest(resource, position)); } diff --git a/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index c08ae5f629a..2510a7ae3ec 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -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'; @@ -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'; @@ -102,7 +91,7 @@ class LinkDetector implements editorCommon.IEditorContribution { private editor:ICodeEditor; private listenersToRemove:IDisposable[]; private timeoutPromise:TPromise; - private computePromise:TPromise; + private computePromise:TPromise; private activeLinkDecorationId:string; private lastMouseEvent:IEditorMouseEvent; private editorService:IEditorService; @@ -172,75 +161,16 @@ class LinkDetector implements editorCommon.IEditorContribution { return; } - let modePromise:TPromise = TPromise.as(null); - if (LinkProviderRegistry.has(this.editor.getModel())) { - modePromise = getLinks(this.editor.getModel()); + if (!LinkProviderRegistry.has(this.editor.getModel())) { + return; } - let standardPromise:TPromise = 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[] = []; diff --git a/src/vs/editor/contrib/links/common/links.ts b/src/vs/editor/contrib/links/common/links.ts index cedde11015f..2526bbd11bb 100644 --- a/src/vs/editor/contrib/links/common/links.ts +++ b/src/vs/editor/contrib/links/common/links.ts @@ -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 { - 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 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); +}); \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 049a4f97ba6..5f2ffc7831d 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -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', @@ -417,4 +423,12 @@ class ExtHostApiCommands { } }); } + + private _executeDocumentLinkProvider(resource: URI): Thenable { + return this._commands.executeCommand('_executeLinkProvider', resource).then(value => { + if (Array.isArray(value)) { + return value.map(typeConverters.DocumentLink.to); + } + }); + } } diff --git a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts index c73809e74cb..168c41180da 100644 --- a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts @@ -401,4 +401,26 @@ suite('ExtHostLanguageFeatureCommands', function() { }); }); }); + + test('Links, back and forth', function() { + + disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, { + 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.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); + }); + }); + }); });