diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index b48ba9ea67a..0a86124c140 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -218,6 +218,12 @@ "description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%", "scope": "window" }, + "typescript.implementationsCodeLens.showOnClassMethods": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.showOnClassMethods.desc%", + "scope": "window" + }, "typescript.reportStyleChecksAsWarnings": { "type": "boolean", "default": true, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index ceb221b137b..106e965b3f6 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -56,6 +56,7 @@ "typescript.referencesCodeLens.showOnAllFunctions": "Enable/disable references CodeLens on all functions in TypeScript files.", "typescript.implementationsCodeLens.enabled": "Enable/disable implementations CodeLens. This CodeLens shows the implementers of an interface.", "typescript.implementationsCodeLens.showOnInterfaceMethods": "Enable/disable implementations CodeLens on interface methods.", + "typescript.implementationsCodeLens.showOnClassMethods": "Enable/disable showing 'implementations' CodeLens above class methods.", "typescript.openTsServerLog.title": "Open TS Server log", "typescript.restartTsServer": "Restart TS Server", "typescript.selectTypeScriptVersion.title": "Select TypeScript Version...", diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts index 6088292f8d0..ed4627707f7 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -25,7 +25,8 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip super(client, _cachedResponse); this._register( vscode.workspace.onDidChangeConfiguration(evt => { - if (evt.affectsConfiguration(`${language.id}.implementationsCodeLens.showOnInterfaceMethods`)) { + if (evt.affectsConfiguration(`${language.id}.implementationsCodeLens.showOnInterfaceMethods`) || + evt.affectsConfiguration(`${language.id}.implementationsCodeLens.showOnClassMethods`)) { this.changeEmitter.fire(); } }) @@ -69,9 +70,12 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip } private getCommand(locations: vscode.Location[], codeLens: ReferencesCodeLens): vscode.Command | undefined { + if (!locations.length) { + return undefined; + } return { title: this.getTitle(locations), - command: locations.length ? 'editor.action.showReferences' : '', + command: 'editor.action.showReferences', arguments: [codeLens.document, codeLens.range.start, locations] }; } @@ -87,23 +91,52 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip item: Proto.NavigationTree, parent: Proto.NavigationTree | undefined ): vscode.Range | undefined { - if (item.kind === PConst.Kind.method && parent && parent.kind === PConst.Kind.interface && vscode.workspace.getConfiguration(this.language.id).get('implementationsCodeLens.showOnInterfaceMethods')) { + const cfg = vscode.workspace.getConfiguration(this.language.id); + + // Keep the class node itself so we enter children + if (item.kind === PConst.Kind.class) { return getSymbolRange(document, item); } - switch (item.kind) { - case PConst.Kind.interface: - return getSymbolRange(document, item); - case PConst.Kind.class: - case PConst.Kind.method: - case PConst.Kind.memberVariable: - case PConst.Kind.memberGetAccessor: - case PConst.Kind.memberSetAccessor: - if (item.kindModifiers.match(/\babstract\b/g)) { - return getSymbolRange(document, item); - } - break; + // Keep the interface node itself so we enter children + if (item.kind === PConst.Kind.interface) { + return getSymbolRange(document, item); } + + // Interface members (behind existing setting) + if ( + item.kind === PConst.Kind.method && + parent?.kind === PConst.Kind.interface && + cfg.get('implementationsCodeLens.showOnInterfaceMethods') + ) { + return getSymbolRange(document, item); + } + + // Skip private methods (cannot be overridden) + if (item.kind === PConst.Kind.method && /\bprivate\b/.test(item.kindModifiers ?? '')) { + return undefined; + } + + // Abstract members (always show) + if ( + (item.kind === PConst.Kind.method || + item.kind === PConst.Kind.memberVariable || + item.kind === PConst.Kind.memberGetAccessor || + item.kind === PConst.Kind.memberSetAccessor) && + /\babstract\b/.test(item.kindModifiers ?? '') + ) { + return getSymbolRange(document, item); + } + + // Class methods (behind new setting; default off) + if ( + item.kind === PConst.Kind.method && + parent?.kind === PConst.Kind.class && + cfg.get('implementationsCodeLens.showOnClassMethods', false) + ) { + return getSymbolRange(document, item); + } + return undefined; } } diff --git a/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts b/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts new file mode 100644 index 00000000000..3fed30cb6be --- /dev/null +++ b/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { joinLines, withRandomFileEditor } from "../testUtils"; + +suite("TypeScript Implementations CodeLens", () => { + test("should show implementations code lens for overridden methods", async () => { + await withRandomFileEditor( + joinLines( + "abstract class A {", + " foo() {}", + "}", + "class B extends A {", + " foo() {}", + "}", + ), + "ts", + async (editor: vscode.TextEditor, doc: vscode.TextDocument) => { + assert.strictEqual( + editor.document, + doc, + "Editor and document should match", + ); + + const lenses = await vscode.commands.executeCommand( + "vscode.executeCodeLensProvider", + doc.uri, + ); + + const fooLens = lenses?.find((lens) => + doc.getText(lens.range).includes("foo"), + ); + + assert.ok(fooLens, "Expected a CodeLens above foo()"); + assert.match( + fooLens!.command?.title ?? "", + /1 implementation/, + 'Expected lens to show "1 implementation"', + ); + }, + ); + }); +});