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:
Matt Bierner
2017-02-28 17:12:16 -08:00
committed by GitHub
parent fee05b2bf6
commit 223ea4bbc0
6 changed files with 270 additions and 107 deletions

View 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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
};
}

View File

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