From 223ea4bbc01b492c249a5065badd1970cd7c68db Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 28 Feb 2017 17:12:16 -0800 Subject: [PATCH] Prototype of Implementations Code Lens Provider for TypeScript (#20784) * Prototype of Implementations Code Lens Provider for TypeScript Adds a prototype code lens that shows the number of implementations for interfaces and abstract classes. This shares a lot of code with the references code lens provider, so I extracted most of the common stuff into a base class. * Support children of interfaces * Add setting to control implementations code lens --- extensions/typescript/package.json | 5 + extensions/typescript/package.nls.json | 1 + .../src/features/baseCodeLensProvider.ts | 119 ++++++++++++++ .../implementationsCodeLensProvider.ts | 97 +++++++++++ .../features/referencesCodeLensProvider.ts | 150 +++++------------- extensions/typescript/src/typescriptMain.ts | 5 + 6 files changed, 270 insertions(+), 107 deletions(-) create mode 100644 extensions/typescript/src/features/baseCodeLensProvider.ts create mode 100644 extensions/typescript/src/features/implementationsCodeLensProvider.ts diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index da446fac401..b0fb028676e 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -111,6 +111,11 @@ "default": false, "description": "%typescript.referencesCodeLens.enabled%" }, + "typescript.implementationsCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.enabled%" + }, "typescript.tsserver.trace": { "type": "string", "enum": [ diff --git a/extensions/typescript/package.nls.json b/extensions/typescript/package.nls.json index c33a12dbc21..87a33de20fe 100644 --- a/extensions/typescript/package.nls.json +++ b/extensions/typescript/package.nls.json @@ -27,5 +27,6 @@ "typescript.goToProjectConfig.title": "Go to Project Configuration", "javascript.goToProjectConfig.title": "Go to Project Configuration", "typescript.referencesCodeLens.enabled": "Enable/disable references CodeLens.", + "typescript.implementationsCodeLens.enabled": "Enable/disable implementations CodeLens.", "typescript.selectTypeScriptVersion.title": "Select TypeScript Version" } \ No newline at end of file diff --git a/extensions/typescript/src/features/baseCodeLensProvider.ts b/extensions/typescript/src/features/baseCodeLensProvider.ts new file mode 100644 index 00000000000..986cb09b8d0 --- /dev/null +++ b/extensions/typescript/src/features/baseCodeLensProvider.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Position, Event, EventEmitter, workspace, } from 'vscode'; +import * as Proto from '../protocol'; + +import { ITypescriptServiceClient } from '../typescriptService'; + +export class ReferencesCodeLens extends CodeLens { + constructor( + public document: Uri, + public file: string, + range: Range + ) { + super(range); + } +} + +export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider { + private enabled: boolean = false; + private onDidChangeCodeLensesEmitter = new EventEmitter(); + + public constructor( + protected client: ITypescriptServiceClient, + private toggleSettingName: string + ) { } + + public get onDidChangeCodeLenses(): Event { + return this.onDidChangeCodeLensesEmitter.event; + } + + public updateConfiguration(): void { + const typeScriptConfig = workspace.getConfiguration('typescript'); + const wasEnabled = this.enabled; + this.enabled = typeScriptConfig.get(this.toggleSettingName, false); + if (wasEnabled !== this.enabled) { + this.onDidChangeCodeLensesEmitter.fire(); + } + } + + provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + if (!this.enabled) { + return Promise.resolve([]); + } + + const filepath = this.client.normalizePath(document.uri); + if (!filepath) { + return Promise.resolve([]); + } + return this.client.execute('navtree', { file: filepath }, token).then(response => { + if (!response) { + return []; + } + const tree = response.body; + const referenceableSpans: Range[] = []; + if (tree && tree.childItems) { + tree.childItems.forEach(item => this.walkNavTree(document, item, null, referenceableSpans)); + } + return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span)); + }); + } + + protected abstract extractSymbol( + document: TextDocument, + item: Proto.NavigationTree, + parent: Proto.NavigationTree | null + ): Range | null; + + private walkNavTree( + document: TextDocument, + item: Proto.NavigationTree, + parent: Proto.NavigationTree | null, + results: Range[] + ): void { + if (!item) { + return; + } + + const range = this.extractSymbol(document, item, parent); + if (range) { + results.push(range); + } + + (item.childItems || []).forEach(child => this.walkNavTree(document, child, item, results)); + } + + /** + * TODO: TS currently requires the position for 'references 'to be inside of the identifer + * Massage the range to make sure this is the case + */ + protected getSymbolRange(document: TextDocument, item: Proto.NavigationTree): Range | null { + if (!item) { + return null; + } + + const span = item.spans && item.spans[0]; + if (!span) { + return null; + } + + const range = new Range( + span.start.line - 1, span.start.offset - 1, + span.end.line - 1, span.end.offset - 1); + + const text = document.getText(range); + + const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}\\b`, 'gm'); + const match = identifierMatch.exec(text); + const prefixLength = match ? match.index + match[1].length : 0; + const startOffset = document.offsetAt(new Position(range.start.line, range.start.character)) + prefixLength; + return new Range( + document.positionAt(startOffset), + document.positionAt(startOffset + item.text.length)); + } +} diff --git a/extensions/typescript/src/features/implementationsCodeLensProvider.ts b/extensions/typescript/src/features/implementationsCodeLensProvider.ts new file mode 100644 index 00000000000..e7ebc0303ef --- /dev/null +++ b/extensions/typescript/src/features/implementationsCodeLensProvider.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { CodeLens, CancellationToken, TextDocument, Range, Location } from 'vscode'; +import * as Proto from '../protocol'; +import * as PConst from '../protocol.const'; + +import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; +import { ITypescriptServiceClient } from '../typescriptService'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider { + public constructor( + client: ITypescriptServiceClient + ) { + super(client, 'implementationsCodeLens.enabled'); + } + + resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { + const codeLens = inputCodeLens as ReferencesCodeLens; + const args: Proto.FileLocationRequestArgs = { + file: codeLens.file, + line: codeLens.range.start.line + 1, + offset: codeLens.range.start.character + 1 + }; + return this.client.execute('implementation', args, token).then(response => { + if (!response || !response.body) { + throw codeLens; + } + + const locations = response.body + .map(reference => + new Location(this.client.asUrl(reference.file), + new Range( + reference.start.line - 1, reference.start.offset - 1, + reference.end.line - 1, reference.end.offset - 1))) + // Exclude original from implementations + .filter(location => + !(location.uri.fsPath === codeLens.document.fsPath && + location.range.start.line === codeLens.range.start.line)); + + codeLens.command = { + title: locations.length === 1 + ? localize('oneImplementationLabel', '1 implementation') + : localize('manyImplementationLabel', '{0} implementations', locations.length), + command: 'editor.action.showReferences', + arguments: [codeLens.document, codeLens.range.start, locations] + }; + return codeLens; + }).catch(() => { + codeLens.command = { + title: localize('implementationsErrorLabel', 'Could not determine implementations'), + command: '' + }; + return codeLens; + }); + } + + protected extractSymbol( + document: TextDocument, + item: Proto.NavigationTree, + parent: Proto.NavigationTree | null + ): Range | null { + // Handle children of interfaces + if (parent && parent.kind === PConst.Kind.interface) { + switch (item.kind) { + case PConst.Kind.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + return super.getSymbolRange(document, item); + } + } + + switch (item.kind) { + case PConst.Kind.interface: + return super.getSymbolRange(document, item); + + case PConst.Kind.class: + case PConst.Kind.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + if (item.kindModifiers.match(/\babstract\b/g)) { + return super.getSymbolRange(document, item); + } + break; + } + return null; + } +} diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index ae24f0f01cc..0bbf6d9ae4d 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -5,67 +5,21 @@ 'use strict'; -import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Location, Position, workspace, EventEmitter, Event } from 'vscode'; +import { CodeLens, CancellationToken, TextDocument, Range, Location } from 'vscode'; import * as Proto from '../protocol'; import * as PConst from '../protocol.const'; +import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); - -class ReferencesCodeLens extends CodeLens { - constructor( - public document: Uri, - public file: string, - range: Range - ) { - super(range); - } -} - -export default class TypeScriptReferencesCodeLensProvider implements CodeLensProvider { - private enabled = false; - - private onDidChangeCodeLensesEmitter = new EventEmitter(); - +export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider { public constructor( - private client: ITypescriptServiceClient) { } - - public get onDidChangeCodeLenses(): Event { - return this.onDidChangeCodeLensesEmitter.event; - } - - public updateConfiguration(): void { - const typeScriptConfig = workspace.getConfiguration('typescript'); - const wasEnabled = this.enabled; - this.enabled = typeScriptConfig.get('referencesCodeLens.enabled', false); - if (wasEnabled !== this.enabled) { - this.onDidChangeCodeLensesEmitter.fire(); - } - } - - provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { - if (!this.enabled) { - return Promise.resolve([]); - } - - const filepath = this.client.normalizePath(document.uri); - if (!filepath) { - return Promise.resolve([]); - } - return this.client.execute('navtree', { file: filepath }, token).then(response => { - if (!response) { - return []; - } - const tree = response.body; - const referenceableSpans: Range[] = []; - if (tree && tree.childItems) { - tree.childItems.forEach(item => this.extractReferenceableSymbols(document, item, referenceableSpans)); - } - return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span)); - }); + client: ITypescriptServiceClient + ) { + super(client, 'referencesCodeLens.enabled'); } resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { @@ -80,16 +34,16 @@ export default class TypeScriptReferencesCodeLensProvider implements CodeLensPro throw codeLens; } - // Exclude original definition from references const locations = response.body.refs - .filter(reference => - !(reference.start.line === codeLens.range.start.line + 1 - && reference.start.offset === codeLens.range.start.character + 1)) .map(reference => new Location(this.client.asUrl(reference.file), new Range( reference.start.line - 1, reference.start.offset - 1, - reference.end.line - 1, reference.end.offset - 1))); + reference.end.line - 1, reference.end.offset - 1))) + .filter(location => + // Exclude original definition from references + !(location.uri.fsPath === codeLens.document.fsPath && + location.range.start.isEqual(codeLens.range.start))); codeLens.command = { title: locations.length === 1 @@ -108,57 +62,39 @@ export default class TypeScriptReferencesCodeLensProvider implements CodeLensPro }); } - private extractReferenceableSymbols(document: TextDocument, item: Proto.NavigationTree, results: Range[]) { - if (!item) { - return; - } - - const span = item.spans && item.spans[0]; - if (span) { - const range = new Range( - span.start.line - 1, span.start.offset - 1, - span.end.line - 1, span.end.offset - 1); - - // TODO: TS currently requires the position for 'references 'to be inside of the identifer - // Massage the range to make sure this is the case - const text = document.getText(range); - - switch (item.kind) { - case PConst.Kind.const: - case PConst.Kind.let: - case PConst.Kind.variable: - case PConst.Kind.function: - // Only show references for exported variables - if (!item.kindModifiers.match(/\bexport\b/)) { - break; - } - // fallthrough - - case PConst.Kind.class: - if (item.text === '') { - break; - } - // fallthrough - - case PConst.Kind.memberFunction: - case PConst.Kind.memberVariable: - case PConst.Kind.memberGetAccessor: - case PConst.Kind.memberSetAccessor: - case PConst.Kind.constructorImplementation: - case PConst.Kind.interface: - case PConst.Kind.type: - case PConst.Kind.enum: - const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}\\b`, 'gm'); - const match = identifierMatch.exec(text); - const prefixLength = match ? match.index + match[1].length : 0; - const startOffset = document.offsetAt(new Position(range.start.line, range.start.character)) + prefixLength; - results.push(new Range( - document.positionAt(startOffset), - document.positionAt(startOffset + item.text.length))); + protected extractSymbol( + document: TextDocument, + item: Proto.NavigationTree, + _parent: Proto.NavigationTree | null + ): Range | null { + switch (item.kind) { + case PConst.Kind.const: + case PConst.Kind.let: + case PConst.Kind.variable: + case PConst.Kind.function: + // Only show references for exported variables + if (!item.kindModifiers.match(/\bexport\b/)) { break; - } + } + // fallthrough + + case PConst.Kind.class: + if (item.text === '') { + break; + } + // fallthrough + + case PConst.Kind.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + case PConst.Kind.constructorImplementation: + case PConst.Kind.interface: + case PConst.Kind.type: + case PConst.Kind.enum: + return super.getSymbolRange(document, item); } - (item.childItems || []).forEach(item => this.extractReferenceableSymbols(document, item, results)); + return null; } -}; +} diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 3857dab548d..4dee65fa959 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -40,6 +40,7 @@ import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import JsDocCompletionHelper from './features/jsDocCompletionProvider'; +import ImplementationCodeLensProvider from './features/implementationsCodeLensProvider'; import * as BuildStatus from './utils/buildStatus'; import * as ProjectStatus from './utils/projectStatus'; @@ -218,6 +219,10 @@ class LanguageProvider { this.referenceCodeLensProvider = new ReferenceCodeLensProvider(client); this.referenceCodeLensProvider.updateConfiguration(); this.disposables.push(languages.registerCodeLensProvider(selector, this.referenceCodeLensProvider)); + + const implementationCodeLens = new ImplementationCodeLensProvider(client); + implementationCodeLens.updateConfiguration(); + this.disposables.push(languages.registerCodeLensProvider(selector, implementationCodeLens)); } if (client.apiVersion.has213Features()) {