diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 5f61518e8c4f9679e87aeb520ff2436d062d19d9..6d3fa05baa700a6a77906c63c29348d9c64f01f8 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1001,11 +1001,16 @@ export interface ILink { range: IRange; url?: URI | string; } + +export interface ILinksList { + links: ILink[]; + dispose?(): void; +} /** * A provider of links. */ export interface LinkProvider { - provideLinks(model: model.ITextModel, token: CancellationToken): ProviderResult; + provideLinks(model: model.ITextModel, token: CancellationToken): ProviderResult; resolveLink?: (link: ILink, token: CancellationToken) => ProviderResult; } diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index 9448d45a0eaeb2215565f92ebe76a5c15a5bfb44..467590c7640a675f4b85c14d3b379ba808c4348b 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -58,12 +58,14 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker this._workerManager = this._register(new WorkerManager(this._modelService)); // todo@joh make sure this happens only once - this._register(modes.LinkProviderRegistry.register('*', { + this._register(modes.LinkProviderRegistry.register('*', { provideLinks: (model, token) => { if (!canSyncModel(this._modelService, model.uri)) { - return Promise.resolve([]); // File too large + return Promise.resolve({ links: [] }); // File too large } - return this._workerManager.withWorker().then(client => client.computeLinks(model.uri)); + return this._workerManager.withWorker().then(client => client.computeLinks(model.uri)).then(links => { + return links && { links }; + }); } })); this._register(modes.CompletionProviderRegistry.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService))); diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index 91aac6c28a2749fdef49c9eac26601f125a22953..5745767952c5501c28f6203c3ceef87e38811a2a 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -8,9 +8,11 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { ILink, LinkProvider, LinkProviderRegistry } from 'vs/editor/common/modes'; +import { ILink, LinkProvider, LinkProviderRegistry, ILinksList } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { isDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { coalesce } from 'vs/base/common/arrays'; export class Link implements ILink { @@ -66,77 +68,99 @@ export class Link implements ILink { } } -export function getLinks(model: ITextModel, token: CancellationToken): Promise { +export class LinksList extends Disposable { - let links: Link[] = []; + readonly links: Link[]; - // ask all providers for links in parallel - const promises = LinkProviderRegistry.ordered(model).reverse().map(provider => { - return Promise.resolve(provider.provideLinks(model, token)).then(result => { - if (Array.isArray(result)) { - const newLinks = result.map(link => new Link(link, provider)); - links = union(links, newLinks); + constructor(tuples: [ILinksList, LinkProvider][]) { + super(); + let links: Link[] = []; + for (const [list, provider] of tuples) { + // merge all links + const newLinks = list.links.map(link => new Link(link, provider)); + links = LinksList._union(links, newLinks); + // register disposables + if (isDisposable(provider)) { + this._register(provider); } - }, onUnexpectedExternalError); - }); + } + this.links = links; + } - return Promise.all(promises).then(() => { - return links; - }); -} + private static _union(oldLinks: Link[], newLinks: Link[]): Link[] { + // reunite oldLinks with newLinks and remove duplicates + let result: Link[] = []; + let oldIndex: number; + let oldLen: number; + let newIndex: number; + let newLen: number; + + for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) { + const oldLink = oldLinks[oldIndex]; + const newLink = newLinks[newIndex]; + + if (Range.areIntersectingOrTouching(oldLink.range, newLink.range)) { + // Remove the oldLink + oldIndex++; + continue; + } -function union(oldLinks: Link[], newLinks: Link[]): Link[] { - // reunite oldLinks with newLinks and remove duplicates - let result: Link[] = []; - let oldIndex: number; - let oldLen: number; - let newIndex: number; - let newLen: number; - - for (oldIndex = 0, newIndex = 0, oldLen = oldLinks.length, newLen = newLinks.length; oldIndex < oldLen && newIndex < newLen;) { - const oldLink = oldLinks[oldIndex]; - const newLink = newLinks[newIndex]; - - if (Range.areIntersectingOrTouching(oldLink.range, newLink.range)) { - // Remove the oldLink - oldIndex++; - continue; - } + const comparisonResult = Range.compareRangesUsingStarts(oldLink.range, newLink.range); - const 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++; + } + } - 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]); } - } - for (; oldIndex < oldLen; oldIndex++) { - result.push(oldLinks[oldIndex]); - } - for (; newIndex < newLen; newIndex++) { - result.push(newLinks[newIndex]); + return result; } - return result; } -CommandsRegistry.registerCommand('_executeLinkProvider', (accessor, ...args) => { +export function getLinks(model: ITextModel, token: CancellationToken): Promise { + + const lists: [ILinksList, LinkProvider][] = []; + + // ask all providers for links in parallel + const promises = LinkProviderRegistry.ordered(model).reverse().map((provider, i) => { + return Promise.resolve(provider.provideLinks(model, token)).then(result => { + if (result) { + lists[i] = [result, provider]; + } + }, onUnexpectedExternalError); + }); + return Promise.all(promises).then(() => new LinksList(coalesce(lists))); +} + + +CommandsRegistry.registerCommand('_executeLinkProvider', async (accessor, ...args): Promise => { const [uri] = args; if (!(uri instanceof URI)) { - return undefined; + return []; } - const model = accessor.get(IModelService).getModel(uri); if (!model) { - return undefined; + return []; } - - return getLinks(model, CancellationToken.None); + const list = await getLinks(model, CancellationToken.None); + if (!list) { + return []; + } + const result = list.links.slice(0); + list.dispose(); + return result; }); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index b3c5f8d0cb6fe67685d4294770cd5547f2f459bf..3b94a6906ab672c1b0cc75b261422df5d6f3c9ff 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -19,7 +19,7 @@ import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, TrackedRangeSti import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { LinkProviderRegistry } from 'vs/editor/common/modes'; import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from 'vs/editor/contrib/goToDefinition/clickLinkGesture'; -import { Link, getLinks } from 'vs/editor/contrib/links/getLinks'; +import { Link, getLinks, LinksList } from 'vs/editor/contrib/links/getLinks'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -157,7 +157,8 @@ class LinkDetector implements editorCommon.IEditorContribution { private enabled: boolean; private listenersToRemove: IDisposable[]; private readonly timeout: async.TimeoutTimer; - private computePromise: async.CancelablePromise | null; + private computePromise: async.CancelablePromise | null; + private activeLinksList: LinksList | null; private activeLinkDecorationId: string | null; private readonly openerService: IOpenerService; private readonly notificationService: INotificationService; @@ -210,6 +211,7 @@ class LinkDetector implements editorCommon.IEditorContribution { this.timeout = new async.TimeoutTimer(); this.computePromise = null; + this.activeLinksList = null; this.currentOccurrences = {}; this.activeLinkDecorationId = null; this.beginCompute(); @@ -246,10 +248,15 @@ class LinkDetector implements editorCommon.IEditorContribution { return; } + if (this.activeLinksList) { + this.activeLinksList.dispose(); + this.activeLinksList = null; + } + this.computePromise = async.createCancelablePromise(token => getLinks(model, token)); try { - const links = await this.computePromise; - this.updateDecorations(links); + this.activeLinksList = await this.computePromise; + this.updateDecorations(this.activeLinksList.links); } catch (err) { onUnexpectedError(err); } finally { @@ -380,6 +387,9 @@ class LinkDetector implements editorCommon.IEditorContribution { private stop(): void { this.timeout.cancel(); + if (this.activeLinksList) { + this.activeLinksList.dispose(); + } if (this.computePromise) { this.computePromise.cancel(); this.computePromise = null; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a3d8cf807937534f463bb0ca4291ff7c562ffdbf..b79be56812a2f432776fa022882ca2449ee2aa99 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5253,11 +5253,16 @@ declare namespace monaco.languages { url?: Uri | string; } + export interface ILinksList { + links: ILink[]; + dispose?(): void; + } + /** * A provider of links. */ export interface LinkProvider { - provideLinks(model: editor.ITextModel, token: CancellationToken): ProviderResult; + provideLinks(model: editor.ITextModel, token: CancellationToken): ProviderResult; resolveLink?: (link: ILink, token: CancellationToken) => ProviderResult; } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index d1d3b70aaf14c71f69ac152594d6d79a62d215fa..bae673a1dd799a19ccaf349768642b0a0e178ea0 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -428,7 +428,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- links $registerDocumentLinkProvider(handle: number, selector: ISerializedDocumentFilter[]): void { - this._registrations[handle] = modes.LinkProviderRegistry.register(selector, { + this._registrations[handle] = modes.LinkProviderRegistry.register(selector, { provideLinks: (model, token) => { return this._proxy.$provideDocumentLinks(handle, model.uri, token).then(dto => { if (dto) { @@ -437,7 +437,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._heapService.trackObject(obj); }); } - return dto; + return { links: dto as modes.ILink[] }; }); }, resolveLink: (link, token) => { @@ -446,7 +446,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha MainThreadLanguageFeatures._reviveLinkDTO(obj); this._heapService.trackObject(obj); } - return obj; + return obj as modes.ILink; }); } }); diff --git a/src/vs/workbench/contrib/output/common/outputLinkProvider.ts b/src/vs/workbench/contrib/output/common/outputLinkProvider.ts index 39eac432b29684d20e471dfc510284c8ba59d037..ad9d566e5c1ff323bbaaf0664849497b764846fd 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/common/outputLinkProvider.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { LinkProviderRegistry, ILink } from 'vs/editor/common/modes'; +import { LinkProviderRegistry, ILink, ILinksList } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/contrib/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/common/services/webWorker'; @@ -42,8 +42,8 @@ export class OutputLinkProvider { if (folders.length > 0) { if (!this.linkProviderRegistration) { this.linkProviderRegistration = LinkProviderRegistry.register([{ language: OUTPUT_MODE_ID, scheme: '*' }, { language: LOG_MODE_ID, scheme: '*' }], { - provideLinks: (model, token): Promise => { - return this.provideLinks(model.uri); + provideLinks: (model): Promise => { + return this.provideLinks(model.uri).then(links => links && { links }); } }); } diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index fde7069af585b5cae32304b0f19c8ddef56ec614..b0dea4c3f4d7c6aeda6013279b6699fd479f01c3 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -1046,9 +1046,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getLinks(model, CancellationToken.None); - assert.equal(value.length, 1); - let [first] = value; + let { links } = await getLinks(model, CancellationToken.None); + assert.equal(links.length, 1); + let [first] = links; assert.equal(first.url, 'foo:bar#3'); assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 2, endColumn: 2 }); }); @@ -1068,9 +1068,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getLinks(model, CancellationToken.None); - assert.equal(value.length, 1); - let [first] = value; + let { links } = await getLinks(model, CancellationToken.None); + assert.equal(links.length, 1); + let [first] = links; assert.equal(first.url, 'foo:bar#3'); assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 2, endColumn: 2 }); });