diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 9dcb59d3e50..7611b2fee34 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.showOnAllClassMethods": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.showOnAllClassMethods%", + "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..26b99390645 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.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 6088292f8d0..d012a6ab9d9 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.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('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('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.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; } } 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..463f9a202fe --- /dev/null +++ b/extensions/typescript-language-features/src/test/smoke/implementationsCodeLens.test.ts @@ -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.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( + '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'); + }, + ); + }); +}); 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)) {