From cd5348abcdcaac0c1b9a8f9da515aa34b1d4cdbd Mon Sep 17 00:00:00 2001 From: ritesh006 Date: Tue, 9 Sep 2025 18:02:22 +0530 Subject: [PATCH 1/4] ts-codelens: implementations for overrides + test --- .../typescript-language-features/package.json | 6 ++ .../package.nls.json | 1 + .../codeLens/implementationsCodeLens.ts | 63 ++++++++++++++----- .../smoke/implementationsCodeLens.test.ts | 47 ++++++++++++++ 4 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts 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"', + ); + }, + ); + }); +}); From c099047be6edff8160ea5e0fb085bdfeeb519770 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:40:11 -0700 Subject: [PATCH 2/4] Update tests and small tweaks --- .../typescript-language-features/package.json | 4 +- .../package.nls.json | 2 +- .../codeLens/implementationsCodeLens.ts | 50 ++--- .../smoke/implementationsCodeLens.test.ts | 180 +++++++++++++++--- .../src/test/smoke/referencesCodeLens.test.ts | 4 +- 5 files changed, 177 insertions(+), 63 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 0a86124c140..2a04d409f29 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -218,10 +218,10 @@ "description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%", "scope": "window" }, - "typescript.implementationsCodeLens.showOnClassMethods": { + "typescript.implementationsCodeLens.showOnAllClassMethods": { "type": "boolean", "default": false, - "description": "%typescript.implementationsCodeLens.showOnClassMethods.desc%", + "description": "%typescript.implementationsCodeLens.showOnAllClassMethods.desc%", "scope": "window" }, "typescript.reportStyleChecksAsWarnings": { diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 106e965b3f6..26b99390645 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -56,7 +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.implementationsCodeLens.showOnAllClassMethods": "Enable/disable showing implementations CodeLens above all class methods instead of only on abstract 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 ed4627707f7..252dc0345c3 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -26,7 +26,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip this._register( vscode.workspace.onDidChangeConfiguration(evt => { if (evt.affectsConfiguration(`${language.id}.implementationsCodeLens.showOnInterfaceMethods`) || - evt.affectsConfiguration(`${language.id}.implementationsCodeLens.showOnClassMethods`)) { + evt.affectsConfiguration(`${language.id}.implementationsCodeLens.showOnAllClassMethods`)) { this.changeEmitter.fire(); } }) @@ -70,12 +70,9 @@ 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: 'editor.action.showReferences', + command: locations.length ? 'editor.action.showReferences' : '', arguments: [codeLens.document, codeLens.range.start, locations] }; } @@ -93,33 +90,15 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip ): vscode.Range | undefined { 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); - } - - // Keep the interface node itself so we enter children + // Always show on interfaces if (item.kind === PConst.Kind.interface) { return getSymbolRange(document, item); } - // Interface members (behind existing setting) + // Always show on abstract classes/properties 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.class || + item.kind === PConst.Kind.method || item.kind === PConst.Kind.memberVariable || item.kind === PConst.Kind.memberGetAccessor || item.kind === PConst.Kind.memberSetAccessor) && @@ -128,12 +107,25 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip return getSymbolRange(document, item); } - // Class methods (behind new setting; default off) + // If configured, show interface members + if ( + item.kind === PConst.Kind.method && + parent?.kind === PConst.Kind.interface && + cfg.get('implementationsCodeLens.showOnInterfaceMethods', false) + ) { + return getSymbolRange(document, item); + } + + + // If configured show on all class methods if ( item.kind === PConst.Kind.method && parent?.kind === PConst.Kind.class && - cfg.get('implementationsCodeLens.showOnClassMethods', false) + cfg.get('implementationsCodeLens.showOnAllClassMethods', false) ) { + if (/\bprivate\b/.test(item.kindModifiers ?? '')) { + return undefined; + } return getSymbolRange(document, item); } diff --git a/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts b/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts index 3fed30cb6be..463f9a202fe 100644 --- a/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts +++ b/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts @@ -5,42 +5,164 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { joinLines, withRandomFileEditor } from "../testUtils"; +import { disposeAll } from '../../utils/dispose'; +import { joinLines, withRandomFileEditor } from '../testUtils'; +import { updateConfig, VsCodeConfiguration } from './referencesCodeLens.test'; -suite("TypeScript Implementations CodeLens", () => { - test("should show implementations code lens for overridden methods", async () => { +const Config = { + referencesCodeLens: 'typescript.referencesCodeLens.enabled', + implementationsCodeLens: 'typescript.implementationsCodeLens.enabled', + showOnAllClassMethods: 'typescript.implementationsCodeLens.showOnAllClassMethods', +}; + +function getCodeLenses(doc: vscode.TextDocument) { + return vscode.commands.executeCommand('vscode.executeCodeLensProvider', doc.uri); +} + +suite('TypeScript Implementations CodeLens', () => { + const configDefaults = Object.freeze({ + [Config.referencesCodeLens]: false, + [Config.implementationsCodeLens]: true, + [Config.showOnAllClassMethods]: false, + }); + + const _disposables: vscode.Disposable[] = []; + let oldConfig: { [key: string]: any } = {}; + + setup(async () => { + // the tests assume that typescript features are registered + await vscode.extensions.getExtension('vscode.typescript-language-features')!.activate(); + + // Save off config and apply defaults + oldConfig = await updateConfig(configDefaults); + }); + + teardown(async () => { + disposeAll(_disposables); + + // Restore config + await updateConfig(oldConfig); + + return vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('Should show on interfaces and abstract classes', async () => { await withRandomFileEditor( joinLines( - "abstract class A {", - " foo() {}", - "}", - "class B extends A {", - " foo() {}", - "}", + 'interface IFoo {}', + 'class Foo implements IFoo {}', + 'abstract class AbstractBase {}', + 'class Concrete extends AbstractBase {}' ), - "ts", - async (editor: vscode.TextEditor, doc: vscode.TextDocument) => { - assert.strictEqual( - editor.document, - doc, - "Editor and document should match", - ); + 'ts', + async (_editor: vscode.TextEditor, doc: vscode.TextDocument) => { + const lenses = await getCodeLenses(doc); + assert.strictEqual(lenses?.length, 2); - const lenses = await vscode.commands.executeCommand( - "vscode.executeCodeLensProvider", - doc.uri, - ); + assert.strictEqual(lenses?.[0].range.start.line, 0, 'Expected interface IFoo to have a CodeLens'); + assert.strictEqual(lenses?.[1].range.start.line, 2, 'Expected abstract class AbstractBase to have a CodeLens'); + }, + ); + }); - const fooLens = lenses?.find((lens) => - doc.getText(lens.range).includes("foo"), - ); + test('Should show on abstract methods, properties, and getters', async () => { + await withRandomFileEditor( + joinLines( + 'abstract class Base {', + ' abstract method(): void;', + ' abstract property: string;', + ' abstract get getter(): number;', + '}', + 'class Derived extends Base {', + ' method() {}', + ' property = "test";', + ' get getter() { return 42; }', + '}', + ), + 'ts', + async (_editor: vscode.TextEditor, doc: vscode.TextDocument) => { + const lenses = await getCodeLenses(doc); + assert.strictEqual(lenses?.length, 4); - assert.ok(fooLens, "Expected a CodeLens above foo()"); - assert.match( - fooLens!.command?.title ?? "", - /1 implementation/, - 'Expected lens to show "1 implementation"', - ); + assert.strictEqual(lenses?.[0].range.start.line, 0, 'Expected abstract class to have a CodeLens'); + assert.strictEqual(lenses?.[1].range.start.line, 1, 'Expected abstract method to have a CodeLens'); + assert.strictEqual(lenses?.[2].range.start.line, 2, 'Expected abstract property to have a CodeLens'); + assert.strictEqual(lenses?.[3].range.start.line, 3, 'Expected abstract getter to have a CodeLens'); + }, + ); + }); + + test('Should not show implementations on methods by default', async () => { + await withRandomFileEditor( + joinLines( + 'abstract class A {', + ' foo() {}', + '}', + 'class B extends A {', + ' foo() {}', + '}', + ), + 'ts', + async (_editor: vscode.TextEditor, doc: vscode.TextDocument) => { + const lenses = await getCodeLenses(doc); + assert.strictEqual(lenses?.length, 1); + }, + ); + }); + + test('should show on all methods when showOnAllClassMethods is enabled', async () => { + await updateConfig({ + [Config.showOnAllClassMethods]: true + }); + + await withRandomFileEditor( + joinLines( + 'abstract class A {', + ' foo() {}', + '}', + 'class B extends A {', + ' foo() {}', + '}', + ), + 'ts', + async (_editor: vscode.TextEditor, doc: vscode.TextDocument) => { + const lenses = await getCodeLenses(doc); + assert.strictEqual(lenses?.length, 3); + + assert.strictEqual(lenses?.[0].range.start.line, 0, 'Expected class A to have a CodeLens'); + assert.strictEqual(lenses?.[1].range.start.line, 1, 'Expected method A.foo to have a CodeLens'); + assert.strictEqual(lenses?.[2].range.start.line, 4, 'Expected method B.foo to have a CodeLens'); + }, + ); + }); + + test('should not show on private methods when showOnAllClassMethods is enabled', async () => { + await updateConfig({ + [Config.showOnAllClassMethods]: true + }); + + await withRandomFileEditor( + joinLines( + 'abstract class A {', + ' public foo() {}', + ' private bar() {}', + ' protected baz() {}', + '}', + 'class B extends A {', + ' public foo() {}', + ' protected baz() {}', + '}', + ), + 'ts', + async (_editor: vscode.TextEditor, doc: vscode.TextDocument) => { + const lenses = await getCodeLenses(doc); + assert.strictEqual(lenses?.length, 5); + + assert.strictEqual(lenses?.[0].range.start.line, 0, 'Expected class A to have a CodeLens'); + assert.strictEqual(lenses?.[1].range.start.line, 1, 'Expected method A.foo to have a CodeLens'); + assert.strictEqual(lenses?.[2].range.start.line, 3, 'Expected method A.baz to have a CodeLens'); + assert.strictEqual(lenses?.[3].range.start.line, 6, 'Expected method B.foo to have a CodeLens'); + assert.strictEqual(lenses?.[4].range.start.line, 7, 'Expected method B.baz to have a CodeLens'); }, ); }); diff --git a/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts b/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts index f6259e0d276..7e068249a2e 100644 --- a/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts +++ b/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts @@ -10,9 +10,9 @@ import { createTestEditor, wait } from '../../test/testUtils'; import { disposeAll } from '../../utils/dispose'; -type VsCodeConfiguration = { [key: string]: any }; +export type VsCodeConfiguration = { [key: string]: any }; -async function updateConfig(newConfig: VsCodeConfiguration): Promise { +export async function updateConfig(newConfig: VsCodeConfiguration): Promise { const oldConfig: VsCodeConfiguration = {}; const config = vscode.workspace.getConfiguration(undefined); for (const configKey of Object.keys(newConfig)) { From c3bcc160ce6fc4f458c87347b018208f2fe28581 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:42:59 -0700 Subject: [PATCH 3/4] Doc fixes --- .../src/languageFeatures/codeLens/implementationsCodeLens.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts index 252dc0345c3..d012a6ab9d9 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -107,7 +107,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip return getSymbolRange(document, item); } - // If configured, show interface members + // If configured, show on interface methods if ( item.kind === PConst.Kind.method && parent?.kind === PConst.Kind.interface && @@ -117,12 +117,13 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip } - // If configured show on all class methods + // If configured, show on all class methods if ( item.kind === PConst.Kind.method && parent?.kind === PConst.Kind.class && cfg.get('implementationsCodeLens.showOnAllClassMethods', false) ) { + // But not private ones as these can never be overridden if (/\bprivate\b/.test(item.kindModifiers ?? '')) { return undefined; } From 40fd1d8ebc3be8bdb1595128960e855d1856ef55 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:16:00 -0700 Subject: [PATCH 4/4] Fix string name --- extensions/typescript-language-features/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 2a04d409f29..81d1611e8da 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -221,7 +221,7 @@ "typescript.implementationsCodeLens.showOnAllClassMethods": { "type": "boolean", "default": false, - "description": "%typescript.implementationsCodeLens.showOnAllClassMethods.desc%", + "description": "%typescript.implementationsCodeLens.showOnAllClassMethods%", "scope": "window" }, "typescript.reportStyleChecksAsWarnings": {