diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index 201ddf039de72ddfc08fe9a605ea8b3a58b18ba1..9f30f48b4f46509f22b124ebfd5af4d1182df3e7 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -99,6 +99,11 @@ "default": true, "description": "%typescript.check.tscVersion%" }, + "typescript.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.referencesCodeLens.enabled%" + }, "typescript.tsserver.trace": { "type": "string", "enum": [ diff --git a/extensions/typescript/package.nls.json b/extensions/typescript/package.nls.json index 51b9649ab91df8bc1662be27a61b3d2527fdc839..1834876a6d996a7545fd43dfae79f4a711ccc914 100644 --- a/extensions/typescript/package.nls.json +++ b/extensions/typescript/package.nls.json @@ -24,5 +24,6 @@ "format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": "Defines space handling after opening and before closing JSX expression braces. Requires TypeScript >= 2.0.6.", "format.placeOpenBraceOnNewLineForFunctions": "Defines whether an open brace is put onto a new line for functions or not.", "format.placeOpenBraceOnNewLineForControlBlocks": "Defines whether an open brace is put onto a new line for control blocks or not.", - "javascript.validate.enable": "Enable/disable JavaScript validation." + "javascript.validate.enable": "Enable/disable JavaScript validation.", + "typescript.referencesCodeLens.enabled": "Enable/disable the references code lens" } \ No newline at end of file diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts new file mode 100644 index 0000000000000000000000000000000000000000..145859cecb264bc6ed547fad79a9f89e8666c41a --- /dev/null +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * 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, Location, Position, workspace, WorkspaceConfiguration } from 'vscode'; +import * as Proto from '../protocol'; +import * as PConst from '../protocol.const'; + +import { ITypescriptServiceClient } from '../typescriptService'; + +import * as nls from 'vscode-nls'; +let localize = nls.loadMessageBundle(); + + +class ReferencesCodeLens extends CodeLens { + public document: Uri; + public file: string; + + constructor(document: Uri, file: string, range: Range) { + super(range); + this.document = document; + this.file = file; + } +} + +export default class TypeScriptReferencesCodeLensProvider implements CodeLensProvider { + private client: ITypescriptServiceClient; + private enabled = false; + + constructor(client: ITypescriptServiceClient) { + this.client = client; + } + + public updateConfiguration(config: WorkspaceConfiguration): void { + let typeScriptConfig = workspace.getConfiguration('typescript'); + this.enabled = typeScriptConfig.get('referencesCodeLens.enabled', false); + } + + provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + if (!this.enabled) { + return Promise.resolve([]); + } + + const filepath = this.client.asAbsolutePath(document.uri); + if (!filepath) { + return Promise.resolve([]); + } + return this.client.execute('navtree', { file: filepath }, token).then(response => { + const tree = response.body; + const referenceableSpans: Range[] = []; + if (tree && tree.childItems) { + tree.childItems.forEach(item => this.extractReferenceableSymbols(document, item, referenceableSpans)); + } + return Promise.resolve(referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span))); + }); + } + + resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { + const codeLens = inputCodeLens as ReferencesCodeLens; + if (!codeLens.document) { + return Promise.reject(codeLens); + } + const args: Proto.FileLocationRequestArgs = { + file: codeLens.file, + line: codeLens.range.start.line + 1, + offset: codeLens.range.start.character + 1 + }; + return this.client.execute('references', args, token).then(response => { + if (response && response.body) { + const referenceCount = Math.max(0, response.body.refs.length - 1); + const locations = response.body.refs.map(reference => + new Location(Uri.file(reference.file), + new Range( + new Position(reference.start.line - 1, reference.start.offset - 1), + new Position(reference.end.line - 1, reference.end.offset - 1)))); + + codeLens.command = { + title: referenceCount + ' ' + (referenceCount === 1 ? localize('oneReferenceLabel', 'reference') : localize('manyReferenceLabel', 'references')), + command: 'editor.action.showReferences', + arguments: [codeLens.document, codeLens.range.start, locations] + }; + return Promise.resolve(codeLens); + } + return Promise.reject(codeLens); + }).catch(() => { + codeLens.command = { + title: localize('referenceErrorLabel', 'Could not determine references'), + command: '' + }; + return Promise.resolve(codeLens); + }); + } + + 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( + new Position(span.start.line - 1, span.start.offset - 1), + new Position(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.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + case PConst.Kind.constructorImplementation: + case PConst.Kind.class: + case PConst.Kind.interface: + case PConst.Kind.type: + case PConst.Kind.enum: + const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${item.text}`, 'g'); + const match = identifierMatch.exec(text); + const start = match ? match.index + match[1].length : 0; + results.push(new Range( + new Position(range.start.line, range.start.character + start), + new Position(range.start.line, range.start.character + start + item.text.length))); + break; + } + } + + (item.childItems || []).forEach(item => this.extractReferenceableSymbols(document, item, results)); + } +}; \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 16080631a3d0d2bfc44318bbd24071ba98d4d521..02891af3f946ff873415e6bceac7971a8474e118 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -35,6 +35,7 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; +import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import * as BuildStatus from './utils/buildStatus'; import * as ProjectStatus from './utils/projectStatus'; @@ -107,6 +108,7 @@ class LanguageProvider { private formattingProvider: FormattingProvider; private formattingProviderRegistration: Disposable | null; private typingsStatus: TypingsStatus; + private referenceCodeLensProvider: ReferenceCodeLensProvider; private _validate: boolean; @@ -156,6 +158,12 @@ class LanguageProvider { this.formattingProviderRegistration = languages.registerDocumentRangeFormattingEditProvider(this.description.modeIds, this.formattingProvider); } + this.referenceCodeLensProvider = new ReferenceCodeLensProvider(client); + this.referenceCodeLensProvider.updateConfiguration(config); + if (client.apiVersion.has206Features()) { + languages.registerCodeLensProvider(this.description.modeIds, this.referenceCodeLensProvider); + } + this.description.modeIds.forEach(modeId => { let selector: DocumentFilter = { scheme: 'file', language: modeId }; languages.registerCompletionItemProvider(selector, this.completionItemProvider, '.'); @@ -171,6 +179,7 @@ class LanguageProvider { if (client.apiVersion.has213Features()) { languages.registerCodeActionsProvider(selector, new CodeActionProvider(client, modeId)); } + languages.setLanguageConfiguration(modeId, { indentationRules: { // ^(.*\*/)?\s*\}.*$ @@ -217,6 +226,9 @@ class LanguageProvider { if (this.completionItemProvider) { this.completionItemProvider.updateConfiguration(config); } + if (this.referenceCodeLensProvider) { + this.referenceCodeLensProvider.updateConfiguration(config); + } if (this.formattingProvider) { this.formattingProvider.updateConfiguration(config); if (!this.formattingProvider.isEnabled() && this.formattingProviderRegistration) {