Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
})
Expand Down Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should skip showing these on private methods as these cannot be overridden

}

return undefined;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading