mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 04:23:32 +01:00
Prototype of Implementations Code Lens Provider for TypeScript (#20784)
* Prototype of Implementations Code Lens Provider for TypeScript Adds a prototype code lens that shows the number of implementations for interfaces and abstract classes. This shares a lot of code with the references code lens provider, so I extracted most of the common stuff into a base class. * Support children of interfaces * Add setting to control implementations code lens
This commit is contained in:
119
extensions/typescript/src/features/baseCodeLensProvider.ts
Normal file
119
extensions/typescript/src/features/baseCodeLensProvider.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Position, Event, EventEmitter, workspace, } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
|
||||
import { ITypescriptServiceClient } from '../typescriptService';
|
||||
|
||||
export class ReferencesCodeLens extends CodeLens {
|
||||
constructor(
|
||||
public document: Uri,
|
||||
public file: string,
|
||||
range: Range
|
||||
) {
|
||||
super(range);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider {
|
||||
private enabled: boolean = false;
|
||||
private onDidChangeCodeLensesEmitter = new EventEmitter<void>();
|
||||
|
||||
public constructor(
|
||||
protected client: ITypescriptServiceClient,
|
||||
private toggleSettingName: string
|
||||
) { }
|
||||
|
||||
public get onDidChangeCodeLenses(): Event<void> {
|
||||
return this.onDidChangeCodeLensesEmitter.event;
|
||||
}
|
||||
|
||||
public updateConfiguration(): void {
|
||||
const typeScriptConfig = workspace.getConfiguration('typescript');
|
||||
const wasEnabled = this.enabled;
|
||||
this.enabled = typeScriptConfig.get(this.toggleSettingName, false);
|
||||
if (wasEnabled !== this.enabled) {
|
||||
this.onDidChangeCodeLensesEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
|
||||
if (!this.enabled) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.client.execute('navtree', { file: filepath }, token).then(response => {
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
const tree = response.body;
|
||||
const referenceableSpans: Range[] = [];
|
||||
if (tree && tree.childItems) {
|
||||
tree.childItems.forEach(item => this.walkNavTree(document, item, null, referenceableSpans));
|
||||
}
|
||||
return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span));
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract extractSymbol(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
parent: Proto.NavigationTree | null
|
||||
): Range | null;
|
||||
|
||||
private walkNavTree(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
parent: Proto.NavigationTree | null,
|
||||
results: Range[]
|
||||
): void {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = this.extractSymbol(document, item, parent);
|
||||
if (range) {
|
||||
results.push(range);
|
||||
}
|
||||
|
||||
(item.childItems || []).forEach(child => this.walkNavTree(document, child, item, results));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: TS currently requires the position for 'references 'to be inside of the identifer
|
||||
* Massage the range to make sure this is the case
|
||||
*/
|
||||
protected getSymbolRange(document: TextDocument, item: Proto.NavigationTree): Range | null {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const span = item.spans && item.spans[0];
|
||||
if (!span) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const range = new Range(
|
||||
span.start.line - 1, span.start.offset - 1,
|
||||
span.end.line - 1, span.end.offset - 1);
|
||||
|
||||
const text = document.getText(range);
|
||||
|
||||
const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}\\b`, 'gm');
|
||||
const match = identifierMatch.exec(text);
|
||||
const prefixLength = match ? match.index + match[1].length : 0;
|
||||
const startOffset = document.offsetAt(new Position(range.start.line, range.start.character)) + prefixLength;
|
||||
return new Range(
|
||||
document.positionAt(startOffset),
|
||||
document.positionAt(startOffset + item.text.length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { CodeLens, CancellationToken, TextDocument, Range, Location } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import * as PConst from '../protocol.const';
|
||||
|
||||
import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider';
|
||||
import { ITypescriptServiceClient } from '../typescriptService';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider {
|
||||
public constructor(
|
||||
client: ITypescriptServiceClient
|
||||
) {
|
||||
super(client, 'implementationsCodeLens.enabled');
|
||||
}
|
||||
|
||||
resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise<CodeLens> {
|
||||
const codeLens = inputCodeLens as ReferencesCodeLens;
|
||||
const args: Proto.FileLocationRequestArgs = {
|
||||
file: codeLens.file,
|
||||
line: codeLens.range.start.line + 1,
|
||||
offset: codeLens.range.start.character + 1
|
||||
};
|
||||
return this.client.execute('implementation', args, token).then(response => {
|
||||
if (!response || !response.body) {
|
||||
throw codeLens;
|
||||
}
|
||||
|
||||
const locations = response.body
|
||||
.map(reference =>
|
||||
new Location(this.client.asUrl(reference.file),
|
||||
new Range(
|
||||
reference.start.line - 1, reference.start.offset - 1,
|
||||
reference.end.line - 1, reference.end.offset - 1)))
|
||||
// Exclude original from implementations
|
||||
.filter(location =>
|
||||
!(location.uri.fsPath === codeLens.document.fsPath &&
|
||||
location.range.start.line === codeLens.range.start.line));
|
||||
|
||||
codeLens.command = {
|
||||
title: locations.length === 1
|
||||
? localize('oneImplementationLabel', '1 implementation')
|
||||
: localize('manyImplementationLabel', '{0} implementations', locations.length),
|
||||
command: 'editor.action.showReferences',
|
||||
arguments: [codeLens.document, codeLens.range.start, locations]
|
||||
};
|
||||
return codeLens;
|
||||
}).catch(() => {
|
||||
codeLens.command = {
|
||||
title: localize('implementationsErrorLabel', 'Could not determine implementations'),
|
||||
command: ''
|
||||
};
|
||||
return codeLens;
|
||||
});
|
||||
}
|
||||
|
||||
protected extractSymbol(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
parent: Proto.NavigationTree | null
|
||||
): Range | null {
|
||||
// Handle children of interfaces
|
||||
if (parent && parent.kind === PConst.Kind.interface) {
|
||||
switch (item.kind) {
|
||||
case PConst.Kind.memberFunction:
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
return super.getSymbolRange(document, item);
|
||||
}
|
||||
}
|
||||
|
||||
switch (item.kind) {
|
||||
case PConst.Kind.interface:
|
||||
return super.getSymbolRange(document, item);
|
||||
|
||||
case PConst.Kind.class:
|
||||
case PConst.Kind.memberFunction:
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
if (item.kindModifiers.match(/\babstract\b/g)) {
|
||||
return super.getSymbolRange(document, item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,67 +5,21 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Location, Position, workspace, EventEmitter, Event } from 'vscode';
|
||||
import { CodeLens, CancellationToken, TextDocument, Range, Location } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import * as PConst from '../protocol.const';
|
||||
|
||||
import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider';
|
||||
import { ITypescriptServiceClient } from '../typescriptService';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
class ReferencesCodeLens extends CodeLens {
|
||||
constructor(
|
||||
public document: Uri,
|
||||
public file: string,
|
||||
range: Range
|
||||
) {
|
||||
super(range);
|
||||
}
|
||||
}
|
||||
|
||||
export default class TypeScriptReferencesCodeLensProvider implements CodeLensProvider {
|
||||
private enabled = false;
|
||||
|
||||
private onDidChangeCodeLensesEmitter = new EventEmitter<void>();
|
||||
|
||||
export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider {
|
||||
public constructor(
|
||||
private client: ITypescriptServiceClient) { }
|
||||
|
||||
public get onDidChangeCodeLenses(): Event<void> {
|
||||
return this.onDidChangeCodeLensesEmitter.event;
|
||||
}
|
||||
|
||||
public updateConfiguration(): void {
|
||||
const typeScriptConfig = workspace.getConfiguration('typescript');
|
||||
const wasEnabled = this.enabled;
|
||||
this.enabled = typeScriptConfig.get('referencesCodeLens.enabled', false);
|
||||
if (wasEnabled !== this.enabled) {
|
||||
this.onDidChangeCodeLensesEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
|
||||
if (!this.enabled) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.client.execute('navtree', { file: filepath }, token).then(response => {
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
const tree = response.body;
|
||||
const referenceableSpans: Range[] = [];
|
||||
if (tree && tree.childItems) {
|
||||
tree.childItems.forEach(item => this.extractReferenceableSymbols(document, item, referenceableSpans));
|
||||
}
|
||||
return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span));
|
||||
});
|
||||
client: ITypescriptServiceClient
|
||||
) {
|
||||
super(client, 'referencesCodeLens.enabled');
|
||||
}
|
||||
|
||||
resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise<CodeLens> {
|
||||
@@ -80,16 +34,16 @@ export default class TypeScriptReferencesCodeLensProvider implements CodeLensPro
|
||||
throw codeLens;
|
||||
}
|
||||
|
||||
// Exclude original definition from references
|
||||
const locations = response.body.refs
|
||||
.filter(reference =>
|
||||
!(reference.start.line === codeLens.range.start.line + 1
|
||||
&& reference.start.offset === codeLens.range.start.character + 1))
|
||||
.map(reference =>
|
||||
new Location(this.client.asUrl(reference.file),
|
||||
new Range(
|
||||
reference.start.line - 1, reference.start.offset - 1,
|
||||
reference.end.line - 1, reference.end.offset - 1)));
|
||||
reference.end.line - 1, reference.end.offset - 1)))
|
||||
.filter(location =>
|
||||
// Exclude original definition from references
|
||||
!(location.uri.fsPath === codeLens.document.fsPath &&
|
||||
location.range.start.isEqual(codeLens.range.start)));
|
||||
|
||||
codeLens.command = {
|
||||
title: locations.length === 1
|
||||
@@ -108,57 +62,39 @@ export default class TypeScriptReferencesCodeLensProvider implements CodeLensPro
|
||||
});
|
||||
}
|
||||
|
||||
private extractReferenceableSymbols(document: TextDocument, item: Proto.NavigationTree, results: Range[]) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const span = item.spans && item.spans[0];
|
||||
if (span) {
|
||||
const range = new Range(
|
||||
span.start.line - 1, span.start.offset - 1,
|
||||
span.end.line - 1, span.end.offset - 1);
|
||||
|
||||
// TODO: TS currently requires the position for 'references 'to be inside of the identifer
|
||||
// Massage the range to make sure this is the case
|
||||
const text = document.getText(range);
|
||||
|
||||
switch (item.kind) {
|
||||
case PConst.Kind.const:
|
||||
case PConst.Kind.let:
|
||||
case PConst.Kind.variable:
|
||||
case PConst.Kind.function:
|
||||
// Only show references for exported variables
|
||||
if (!item.kindModifiers.match(/\bexport\b/)) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case PConst.Kind.class:
|
||||
if (item.text === '<class>') {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case PConst.Kind.memberFunction:
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
case PConst.Kind.constructorImplementation:
|
||||
case PConst.Kind.interface:
|
||||
case PConst.Kind.type:
|
||||
case PConst.Kind.enum:
|
||||
const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}\\b`, 'gm');
|
||||
const match = identifierMatch.exec(text);
|
||||
const prefixLength = match ? match.index + match[1].length : 0;
|
||||
const startOffset = document.offsetAt(new Position(range.start.line, range.start.character)) + prefixLength;
|
||||
results.push(new Range(
|
||||
document.positionAt(startOffset),
|
||||
document.positionAt(startOffset + item.text.length)));
|
||||
protected extractSymbol(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
_parent: Proto.NavigationTree | null
|
||||
): Range | null {
|
||||
switch (item.kind) {
|
||||
case PConst.Kind.const:
|
||||
case PConst.Kind.let:
|
||||
case PConst.Kind.variable:
|
||||
case PConst.Kind.function:
|
||||
// Only show references for exported variables
|
||||
if (!item.kindModifiers.match(/\bexport\b/)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case PConst.Kind.class:
|
||||
if (item.text === '<class>') {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case PConst.Kind.memberFunction:
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
case PConst.Kind.constructorImplementation:
|
||||
case PConst.Kind.interface:
|
||||
case PConst.Kind.type:
|
||||
case PConst.Kind.enum:
|
||||
return super.getSymbolRange(document, item);
|
||||
}
|
||||
|
||||
(item.childItems || []).forEach(item => this.extractReferenceableSymbols(document, item, results));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import WorkspaceSymbolProvider from './features/workspaceSymbolProvider';
|
||||
import CodeActionProvider from './features/codeActionProvider';
|
||||
import ReferenceCodeLensProvider from './features/referencesCodeLensProvider';
|
||||
import JsDocCompletionHelper from './features/jsDocCompletionProvider';
|
||||
import ImplementationCodeLensProvider from './features/implementationsCodeLensProvider';
|
||||
|
||||
import * as BuildStatus from './utils/buildStatus';
|
||||
import * as ProjectStatus from './utils/projectStatus';
|
||||
@@ -218,6 +219,10 @@ class LanguageProvider {
|
||||
this.referenceCodeLensProvider = new ReferenceCodeLensProvider(client);
|
||||
this.referenceCodeLensProvider.updateConfiguration();
|
||||
this.disposables.push(languages.registerCodeLensProvider(selector, this.referenceCodeLensProvider));
|
||||
|
||||
const implementationCodeLens = new ImplementationCodeLensProvider(client);
|
||||
implementationCodeLens.updateConfiguration();
|
||||
this.disposables.push(languages.registerCodeLensProvider(selector, implementationCodeLens));
|
||||
}
|
||||
|
||||
if (client.apiVersion.has213Features()) {
|
||||
|
||||
Reference in New Issue
Block a user