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)) {