diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 96f58bb3a07..aeb2f2c3ed2 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2312,6 +2312,47 @@ declare namespace vscode { resolveCompletionItem?(item: CompletionItem, token: CancellationToken): CompletionItem | Thenable; } + + /** + * A document link is a range in a text document that links to an internal or external resource, like another + * text document or a web site. + */ + export class DocumentLink { + + /** + * The range this link applies to. + */ + range: Range; + + /** + * The uri this link points to. + */ + target: Uri; + + /** + * Creates a new document link. + * + * @param range The range the document link applies to. Must not be empty. + * @param target The uri the document link points to. + */ + constructor(range: Range, target: Uri); + } + + /** + * The document link provider defines the contract between extensions and feature of showing + * links in the editor. + */ + export interface DocumentLinkProvider { + + /** + * @param document The document in which the command was invoked. + * @param token A cancellation token. + * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result + * can be signaled by returning `undefined`, `null`, or an empty array. + */ + provideDocumentLinks(document: TextDocument, token: CancellationToken): DocumentLink[] | Thenable; + } + /** * A tuple of two characters, like a pair of * opening and closing brackets. @@ -3753,6 +3794,19 @@ declare namespace vscode { */ export function registerSignatureHelpProvider(selector: DocumentSelector, provider: SignatureHelpProvider, ...triggerCharacters: string[]): Disposable; + /** + * Register a document link provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A document link provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerDocumentLinkProvider(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable; + /** * Set a [language configuration](#LanguageConfiguration) for a language. * diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index fe870ef47ff..82372870a41 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -74,6 +74,7 @@ export class ExtHostAPIImplementation { CompletionItem: typeof vscode.CompletionItem; CompletionItemKind: typeof vscode.CompletionItemKind; CompletionList: typeof vscode.CompletionList; + DocumentLink: typeof vscode.DocumentLink; IndentAction: typeof vscode.IndentAction; OverviewRulerLane: typeof vscode.OverviewRulerLane; TextEditorRevealType: typeof vscode.TextEditorRevealType; @@ -150,6 +151,7 @@ export class ExtHostAPIImplementation { this.CompletionItem = extHostTypes.CompletionItem; this.CompletionItemKind = extHostTypes.CompletionItemKind; this.CompletionList = extHostTypes.CompletionList; + this.DocumentLink = extHostTypes.DocumentLink; this.ViewColumn = extHostTypes.ViewColumn; this.StatusBarAlignment = extHostTypes.StatusBarAlignment; this.IndentAction = Modes.IndentAction; @@ -369,6 +371,9 @@ export class ExtHostAPIImplementation { registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { return languageFeatures.registerCompletionItemProvider(selector, provider, triggerCharacters); }, + registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { + return languageFeatures.registerDocumentLinkProvider(selector, provider); + }, setLanguageConfiguration: (language: string, configuration: vscode.LanguageConfiguration):vscode.Disposable => { return languageFeatures.setLanguageConfiguration(language, configuration); } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index dcd24828edf..490e1c38beb 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -127,6 +127,7 @@ export abstract class MainThreadLanguageFeaturesShape { $registerRenameSupport(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } $registerSuggestSupport(handle: number, selector: vscode.DocumentSelector, triggerCharacters: string[]): TPromise { throw ni(); } $registerSignatureHelpProvider(handle: number, selector: vscode.DocumentSelector, triggerCharacter: string[]): TPromise { throw ni(); } + $registerDocumentLinkProvider(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } $setLanguageConfiguration(handle: number, languageId:string, configuration: vscode.LanguageConfiguration): TPromise { throw ni(); } } @@ -267,6 +268,8 @@ export abstract class ExtHostLanguageFeaturesShape { $provideCompletionItems(handle: number, resource: URI, position: editorCommon.IPosition): TPromise { throw ni(); } $resolveCompletionItem(handle: number, resource: URI, position: editorCommon.IPosition, suggestion: modes.ISuggestion): TPromise { throw ni(); } $provideSignatureHelp(handle: number, resource: URI, position: editorCommon.IPosition): TPromise { throw ni(); } + $providDocumentLinks(handle: number, resource: URI): TPromise { throw ni(); } + } export abstract class ExtHostQuickOpenShape { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index efbfe5d059b..d2a45eb6a94 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -588,10 +588,31 @@ class SignatureHelpAdapter { } } +class LinkProviderAdapter { + + private _documents: ExtHostDocuments; + private _provider: vscode.DocumentLinkProvider; + + constructor(documents: ExtHostDocuments, provider: vscode.DocumentLinkProvider) { + this._documents = documents; + this._provider = provider; + } + + provideLinks(resource: URI): TPromise { + const doc = this._documents.getDocumentData(resource).document; + + return asWinJsPromise(token => this._provider.provideDocumentLinks(doc, token)).then(links => { + if (Array.isArray(links)) { + return links.map(TypeConverters.DocumentLink.from); + } + }); + } +} + type Adapter = OutlineAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | QuickFixAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter; + | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter; export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { @@ -821,6 +842,19 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(resource, position)); } + // --- links + + registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { + const handle = this._nextHandle(); + this._adapter[handle] = new LinkProviderAdapter(this._documents, provider); + this._proxy.$registerDocumentLinkProvider(handle, selector); + return this._createDisposable(handle); + } + + $providDocumentLinks(handle: number, resource: URI): TPromise { + return this._withAdapter(handle, LinkProviderAdapter, adapter => adapter.provideLinks(resource)); + } + // --- configuration setLanguageConfiguration(languageId:string, configuration: vscode.LanguageConfiguration): vscode.Disposable { diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 4c4f4b42dcf..777667e6842 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -318,6 +318,19 @@ export namespace SignatureHelp { } } +export namespace DocumentLink { + + export function from(link: types.DocumentLink): modes.ILink { + return { + range: fromRange(link.range), + url: link.target.toString() + }; + } + + export function to(link: modes.ILink):types.DocumentLink { + return new types.DocumentLink(toRange(link.range), URI.parse(link.url)); + } +} export namespace Command { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index e06cf82c77a..c0058a88efd 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -206,6 +206,17 @@ export class Position { export class Range { + static is(thing: any): thing is Range { + if (thing instanceof Range) { + return true; + } + if (!thing) { + return false; + } + return Position.is((thing).start) + && Position.is((thing.end)); + } + protected _start: Position; protected _end: Position; @@ -769,3 +780,21 @@ export enum TextEditorRevealType { InCenter = 1, InCenterIfOutsideViewport = 2 } + +export class DocumentLink { + + range: Range; + + target: URI; + + constructor(range: Range, target: URI) { + if (!(target instanceof URI)) { + throw illegalArgument('target'); + } + if (!Range.is(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.target = target; + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts index b24b25507a9..d6c87d52e3c 100644 --- a/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts @@ -201,6 +201,17 @@ export class MainThreadLanguageFeatures extends MainThreadLanguageFeaturesShape return undefined; } + // --- links + + $registerDocumentLinkProvider(handle: number, selector: vscode.DocumentSelector): TPromise { + this._registrations[handle] = modes.LinkProviderRegistry.register(selector, { + provideLinks: (model, token) => { + return wireCancellationToken(token, this._proxy.$providDocumentLinks(handle, model.uri)); + } + }); + return undefined; + } + // --- configuration $setLanguageConfiguration(handle: number, languageId: string, configuration: vscode.LanguageConfiguration): TPromise { diff --git a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts index 79e3cb3c72e..e2da57a4192 100644 --- a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts @@ -35,6 +35,7 @@ import {rename} from 'vs/editor/contrib/rename/common/rename'; import {provideSignatureHelp} from 'vs/editor/contrib/parameterHints/common/parameterHints'; import {provideSuggestionItems} from 'vs/editor/contrib/suggest/common/suggest'; import {getDocumentFormattingEdits, getDocumentRangeFormattingEdits, getOnTypeFormattingEdits} from 'vs/editor/contrib/format/common/format'; +import {getLinks} from 'vs/editor/contrib/links/common/links'; import {asWinJsPromise} from 'vs/base/common/async'; import {MainContext, ExtHostContext} from 'vs/workbench/api/node/extHost.protocol'; import {ExtHostDiagnostics} from 'vs/workbench/api/node/extHostDiagnostics'; @@ -949,4 +950,48 @@ suite('ExtHostLanguageFeatures', function() { }); }); }); + + test('Links, data conversion', function () { + + disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, { + provideDocumentLinks() { + return [new types.DocumentLink(new types.Range(0, 0, 1, 1), types.Uri.parse('foo:bar#3'))]; + } + })); + + return threadService.sync().then(() => { + return getLinks(model).then(value => { + assert.equal(value.length, 1); + let [first] = value; + + assert.equal(first.url, 'foo:bar#3'); + assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 2, endColumn: 2 }); + }); + }); + }); + + test('Links, evil provider', function () { + + disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, { + provideDocumentLinks() { + return [new types.DocumentLink(new types.Range(0, 0, 1, 1), types.Uri.parse('foo:bar#3'))]; + } + })); + + disposables.push(extHost.registerDocumentLinkProvider(defaultSelector, { + provideDocumentLinks(): any { + throw new Error(); + } + })); + + return threadService.sync().then(() => { + return getLinks(model).then(value => { + assert.equal(value.length, 1); + let [first] = value; + + assert.equal(first.url, 'foo:bar#3'); + assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 2, endColumn: 2 }); + }); + }); + }); }); diff --git a/src/vs/workbench/test/node/api/extHostTypes.test.ts b/src/vs/workbench/test/node/api/extHostTypes.test.ts index f8de451a7c1..652f5ef1fb4 100644 --- a/src/vs/workbench/test/node/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/node/api/extHostTypes.test.ts @@ -16,9 +16,9 @@ function assertToJSON(a: any, expected: any) { assert.deepEqual(actual, expected); } -suite('ExtHostTypes', function() { +suite('ExtHostTypes', function () { - test('URI, toJSON', function() { + test('URI, toJSON', function () { let uri = URI.parse('file:///path/test.file'); let data = uri.toJSON(); @@ -34,7 +34,7 @@ suite('ExtHostTypes', function() { }); }); - test('Disposable', function() { + test('Disposable', function () { let count = 0; let d = new types.Disposable(() => { @@ -61,7 +61,7 @@ suite('ExtHostTypes', function() { }); - test('Position', function() { + test('Position', function () { assert.throws(() => new types.Position(-1, 0)); assert.throws(() => new types.Position(0, -1)); @@ -75,12 +75,12 @@ suite('ExtHostTypes', function() { assert.equal(character, 0); }); - test('Position, toJSON', function() { + test('Position, toJSON', function () { let pos = new types.Position(4, 2); assertToJSON(pos, { line: 4, character: 2 }); }); - test('Position, isBefore(OrEqual)?', function() { + test('Position, isBefore(OrEqual)?', function () { let p1 = new types.Position(1, 3); let p2 = new types.Position(1, 2); let p3 = new types.Position(0, 4); @@ -91,7 +91,7 @@ suite('ExtHostTypes', function() { assert.ok(p3.isBefore(p2)); }); - test('Position, isAfter(OrEqual)?', function() { + test('Position, isAfter(OrEqual)?', function () { let p1 = new types.Position(1, 3); let p2 = new types.Position(1, 2); let p3 = new types.Position(0, 4); @@ -103,7 +103,7 @@ suite('ExtHostTypes', function() { assert.ok(p1.isAfter(p3)); }); - test('Position, compareTo', function() { + test('Position, compareTo', function () { let p1 = new types.Position(1, 3); let p2 = new types.Position(1, 2); let p3 = new types.Position(0, 4); @@ -115,7 +115,7 @@ suite('ExtHostTypes', function() { assert.equal(p1.compareTo(p3), 1); }); - test('Position, translate', function() { + test('Position, translate', function () { let p1 = new types.Position(1, 3); assert.ok(p1.translate() === p1); @@ -153,7 +153,7 @@ suite('ExtHostTypes', function() { assert.throws(() => p1.translate(0, -4)); }); - test('Position, with', function() { + test('Position, with', function () { let p1 = new types.Position(1, 3); assert.ok(p1.with() === p1); @@ -176,7 +176,7 @@ suite('ExtHostTypes', function() { assert.throws(() => p1.with({ character: -1 })); }); - test('Range', function() { + test('Range', function () { assert.throws(() => new types.Range(-1, 0, 0, 0)); assert.throws(() => new types.Range(0, -1, 0, 0)); assert.throws(() => new types.Range(new types.Position(0, 0), undefined)); @@ -189,13 +189,13 @@ suite('ExtHostTypes', function() { assert.throws(() => range.start = new types.Position(0, 3)); }); - test('Range, toJSON', function() { + test('Range, toJSON', function () { let range = new types.Range(1, 2, 3, 4); assertToJSON(range, [{ line: 1, character: 2 }, { line: 3, character: 4 }]); }); - test('Range, sorting', function() { + test('Range, sorting', function () { // sorts start/end let range = new types.Range(1, 0, 0, 0); assert.equal(range.start.line, 0); @@ -206,7 +206,7 @@ suite('ExtHostTypes', function() { assert.equal(range.end.line, 1); }); - test('Range, isEmpty|isSingleLine', function() { + test('Range, isEmpty|isSingleLine', function () { let range = new types.Range(1, 0, 0, 0); assert.ok(!range.isEmpty); assert.ok(!range.isSingleLine); @@ -224,7 +224,7 @@ suite('ExtHostTypes', function() { assert.ok(!range.isSingleLine); }); - test('Range, contains', function() { + test('Range, contains', function () { let range = new types.Range(1, 1, 2, 11); assert.ok(range.contains(range.start)); @@ -237,7 +237,7 @@ suite('ExtHostTypes', function() { assert.ok(!range.contains(new types.Range(1, 1, 3, 11))); }); - test('Range, intersection', function() { + test('Range, intersection', function () { let range = new types.Range(1, 1, 2, 11); let res: types.Range; @@ -267,7 +267,7 @@ suite('ExtHostTypes', function() { assert.throws(() => range.intersection(undefined)); }); - test('Range, union', function() { + test('Range, union', function () { let ran1 = new types.Range(0, 0, 5, 5); assert.ok(ran1.union(new types.Range(0, 0, 1, 1)) === ran1); @@ -284,7 +284,7 @@ suite('ExtHostTypes', function() { assert.equal(res.start.character, 0); }); - test('Range, with', function() { + test('Range, with', function () { let range = new types.Range(1, 1, 2, 11); assert.ok(range.with(range.start) === range); @@ -310,7 +310,7 @@ suite('ExtHostTypes', function() { assert.equal(res.start.line, 1); assert.equal(res.start.character, 1); - res = range.with({ end: new types.Position(9, 8), start: new types.Position(2, 3)}); + res = range.with({ end: new types.Position(9, 8), start: new types.Position(2, 3) }); assert.equal(res.end.line, 9); assert.equal(res.end.character, 8); assert.equal(res.start.line, 2); @@ -320,7 +320,7 @@ suite('ExtHostTypes', function() { assert.throws(() => range.with(undefined, null)); }); - test('TextEdit', function() { + test('TextEdit', function () { assert.throws(() => new types.TextEdit(null, 'far')); assert.throws(() => new types.TextEdit(undefined, 'far')); @@ -337,7 +337,7 @@ suite('ExtHostTypes', function() { assert.equal(edit.newText, ''); }); - test('WorkspaceEdit', function() { + test('WorkspaceEdit', function () { let a = types.Uri.file('a.ts'); let b = types.Uri.file('b.ts'); @@ -368,6 +368,11 @@ suite('ExtHostTypes', function() { }); + test('DocumentLink', function () { + assert.throws(() => new types.DocumentLink(null, null)); + assert.throws(() => new types.DocumentLink(new types.Range(1, 1, 1, 1), null)); + }); + test('toJSON & stringify', function() { assertToJSON(new types.Selection(3, 4, 2, 1), { start: { line: 2, character: 1 }, end: { line: 3, character: 4 }, anchor: { line: 3, character: 4 }, active: { line: 2, character: 1 } });