Merge pull request #264546 from ritesh006/test/implementations-codelens

feat(ts-codeLens): show "implementations" CodeLens for overridden methods #263749
This commit is contained in:
Matt Bierner
2025-11-03 12:04:42 -08:00
committed by GitHub
5 changed files with 218 additions and 16 deletions

View File

@@ -218,6 +218,12 @@
"description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%",
"scope": "window"
},
"typescript.implementationsCodeLens.showOnAllClassMethods": {
"type": "boolean",
"default": false,
"description": "%typescript.implementationsCodeLens.showOnAllClassMethods%",
"scope": "window"
},
"typescript.reportStyleChecksAsWarnings": {
"type": "boolean",
"default": true,

View File

@@ -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.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...",

View File

@@ -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.showOnAllClassMethods`)) {
this.changeEmitter.fire();
}
})
@@ -87,23 +88,48 @@ 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<boolean>('implementationsCodeLens.showOnInterfaceMethods')) {
const cfg = vscode.workspace.getConfiguration(this.language.id);
// Always show on interfaces
if (item.kind === PConst.Kind.interface) {
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;
// Always show on abstract classes/properties
if (
(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) &&
/\babstract\b/.test(item.kindModifiers ?? '')
) {
return getSymbolRange(document, item);
}
// If configured, show on interface methods
if (
item.kind === PConst.Kind.method &&
parent?.kind === PConst.Kind.interface &&
cfg.get<boolean>('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<boolean>('implementationsCodeLens.showOnAllClassMethods', false)
) {
// But not private ones as these can never be overridden
if (/\bprivate\b/.test(item.kindModifiers ?? '')) {
return undefined;
}
return getSymbolRange(document, item);
}
return undefined;
}
}

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* 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 { disposeAll } from '../../utils/dispose';
import { joinLines, withRandomFileEditor } from '../testUtils';
import { updateConfig, VsCodeConfiguration } from './referencesCodeLens.test';
const Config = {
referencesCodeLens: 'typescript.referencesCodeLens.enabled',
implementationsCodeLens: 'typescript.implementationsCodeLens.enabled',
showOnAllClassMethods: 'typescript.implementationsCodeLens.showOnAllClassMethods',
};
function getCodeLenses(doc: vscode.TextDocument) {
return vscode.commands.executeCommand<vscode.CodeLens[]>('vscode.executeCodeLensProvider', doc.uri);
}
suite('TypeScript Implementations CodeLens', () => {
const configDefaults = Object.freeze<VsCodeConfiguration>({
[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(
'interface IFoo {}',
'class Foo implements IFoo {}',
'abstract class AbstractBase {}',
'class Concrete extends AbstractBase {}'
),
'ts',
async (_editor: vscode.TextEditor, doc: vscode.TextDocument) => {
const lenses = await getCodeLenses(doc);
assert.strictEqual(lenses?.length, 2);
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');
},
);
});
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.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');
},
);
});
});

View File

@@ -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<VsCodeConfiguration> {
export async function updateConfig(newConfig: VsCodeConfiguration): Promise<VsCodeConfiguration> {
const oldConfig: VsCodeConfiguration = {};
const config = vscode.workspace.getConfiguration(undefined);
for (const configKey of Object.keys(newConfig)) {