Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: support code lens for references and implementations #785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 6, 2023
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
56 changes: 50 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
[![Build Status](https://travis-ci.org/theia-ide/typescript-language-server.svg?branch=master)](https://travis-ci.org/theia-ide/typescript-language-server)
[![Discord](https://img.shields.io/discord/873659987413573634)](https://discord.gg/AC7Vs6hwFa)
[![Discord][discord-src]][discord-href]
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]

# TypeScript Language Server

[Language Server Protocol](https://github.com/Microsoft/language-server-protocol) implementation for TypeScript wrapping `tsserver`.

[![https://nodei.co/npm/typescript-language-server.png?downloads=true&downloadRank=true&stars=true](https://nodei.co/npm/typescript-language-server.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/typescript-language-server)
Based on concepts and ideas from https://github.com/prabirshrestha/typescript-language-server and originally maintained by [TypeFox](https://typefox.io).

Based on concepts and ideas from https://github.com/prabirshrestha/typescript-language-server and originally maintained by [TypeFox](https://typefox.io)

Maintained by a [community of contributors](https://github.com/typescript-language-server/typescript-language-server/graphs/contributors) like you
Maintained by a [community of contributors](https://github.com/typescript-language-server/typescript-language-server/graphs/contributors) like you.

<!-- MarkdownTOC -->

Expand All @@ -27,6 +26,7 @@ Maintained by a [community of contributors](https://github.com/typescript-langua
- [Organize Imports](#organize-imports)
- [Rename File](#rename-file)
- [Configure plugin](#configure-plugin)
- [Code Lenses \(`textDocument/codeLens`\)](#code-lenses-textdocumentcodelens)
- [Inlay hints \(`textDocument/inlayHint`\)](#inlay-hints-textdocumentinlayhint)
- [TypeScript Version Notification](#typescript-version-notification)
- [Supported Protocol features](#supported-protocol-features)
Expand Down Expand Up @@ -361,6 +361,11 @@ Some of the preferences can be controlled through the `workspace/didChangeConfig
[language].inlayHints.includeInlayPropertyDeclarationTypeHints: boolean;
[language].inlayHints.includeInlayVariableTypeHints: boolean;
[language].inlayHints.includeInlayVariableTypeHintsWhenTypeMatchesName: boolean;
// Code Lens preferences
[language].implementationsCodeLens.enabled: boolean;
[language].referencesCodeLens.enabled: boolean;
[language].referencesCodeLens.showOnAllFunctions: boolean;

/**
* Complete functions with their parameter signature.
*
Expand Down Expand Up @@ -557,6 +562,38 @@ Most of the time, you'll execute commands with arguments retrieved from another
void
```

## Code Lenses (`textDocument/codeLens`)

Code lenses can be enabled using the `implementationsCodeLens` and `referencesCodeLens` [workspace configuration options](#workspacedidchangeconfiguration).

Code lenses provide a count of **references** and/or **implemenations** for symbols in the document. For clients that support it it's also possible to click on those to navigate to the relevant locations in the the project. Do note that clicking those trigger a `editor.action.showReferences` command which is something that client needs to have explicit support for. Many do by default but some don't. An example command will look like this:

```ts
command: {
title: '1 reference',
command: 'editor.action.showReferences',
arguments: [
'file://project/foo.ts', // URI
{ line: 1, character: 1 }, // Position
[ // A list of Location objects.
{
uri: 'file://project/bar.ts',
range: {
start: {
line: 7,
character: 24,
},
end: {
line: 7,
character: 28,
},
},
},
],
],
}
```

## Inlay hints (`textDocument/inlayHint`)

For the request to return any results, some or all of the following options need to be enabled through `preferences`:
Expand Down Expand Up @@ -636,3 +673,10 @@ yarn watch
### Publishing

New version of the package is published automatically on pushing new tag to the upstream repo.

[npm-version-src]: https://img.shields.io/npm/dt/typescript-language-server.svg?style=flat-square
[npm-version-href]: https://npmjs.com/package/typescript-language-server
[npm-downloads-src]: https://img.shields.io/npm/v/typescript-language-server/latest.svg?style=flat-square
[npm-downloads-href]: https://npmjs.com/package/typescript-language-server
[discord-src]: https://img.shields.io/discord/873659987413573634?style=flat-square
[discord-href]: https://discord.gg/AC7Vs6hwFa
121 changes: 121 additions & 0 deletions src/features/code-lens/baseCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2023 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type * as lsp from 'vscode-languageserver-protocol';
import { Range as LspRange, CodeLens } from 'vscode-languageserver-protocol';
import FileConfigurationManager from '../fileConfigurationManager.js';
import type { LspDocument } from '../../document.js';
import { CachedResponse } from '../../tsServer/cachedResponse.js';
import type { ts } from '../../ts-protocol.js';
import { CommandTypes } from '../../ts-protocol.js';
import { Range } from '../../utils/typeConverters.js';
import { ITypeScriptServiceClient } from '../../typescriptService.js';
import { escapeRegExp } from '../../utils/regexp.js';

export enum CodeLensType {
Reference,
Implementation
}

export interface ReferencesCodeLens extends CodeLens {
data?: {
type: CodeLensType;
uri: string;
};
}

export abstract class TypeScriptBaseCodeLensProvider {
public static readonly cancelledCommand: lsp.Command = {
// Cancellation is not an error. Just show nothing until we can properly re-compute the code lens
title: '',
command: '',
};

public static readonly errorCommand: lsp.Command = {
title: 'Could not determine references',
command: '',
};

protected abstract get type(): CodeLensType;

public constructor(
protected client: ITypeScriptServiceClient,
private readonly cachedResponse: CachedResponse<ts.server.protocol.NavTreeResponse>,
protected fileConfigurationManager: FileConfigurationManager,
) { }

async provideCodeLenses(document: LspDocument, token: lsp.CancellationToken): Promise<ReferencesCodeLens[]> {
const configuration = this.fileConfigurationManager.getWorkspacePreferencesForFile(document);
if (this.type === CodeLensType.Implementation && !configuration.implementationsCodeLens?.enabled
|| this.type === CodeLensType.Reference && !configuration.referencesCodeLens?.enabled) {
return [];
}

const response = await this.cachedResponse.execute(
document,
() => this.client.execute(CommandTypes.NavTree, { file: document.filepath }, token),
);
if (response.type !== 'response') {
return [];
}

const referenceableSpans: lsp.Range[] = [];
response.body?.childItems?.forEach(item => this.walkNavTree(document, item, undefined, referenceableSpans));
return referenceableSpans.map(span => CodeLens.create(span, { uri: document.uri.toString(), type: this.type }));
}

protected abstract extractSymbol(
document: LspDocument,
item: ts.server.protocol.NavigationTree,
parent: ts.server.protocol.NavigationTree | undefined
): lsp.Range | undefined;

private walkNavTree(
document: LspDocument,
item: ts.server.protocol.NavigationTree,
parent: ts.server.protocol.NavigationTree | undefined,
results: lsp.Range[],
): void {
const range = this.extractSymbol(document, item, parent);
if (range) {
results.push(range);
}

item.childItems?.forEach(child => this.walkNavTree(document, child, item, results));
}
}

export function getSymbolRange(
document: LspDocument,
item: ts.server.protocol.NavigationTree,
): lsp.Range | undefined {
if (item.nameSpan) {
return Range.fromTextSpan(item.nameSpan);
}

// In older versions, we have to calculate this manually. See #23924
const span = item.spans?.[0];
if (!span) {
return undefined;
}

const range = Range.fromTextSpan(span);
const text = document.getText(range);

const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, 'gm');
const match = identifierMatch.exec(text);
const prefixLength = match ? match.index + match[1].length : 0;
const startOffset = document.offsetAt(range.start) + prefixLength;
return LspRange.create(
document.positionAt(startOffset),
document.positionAt(startOffset + item.text.length),
);
}
105 changes: 105 additions & 0 deletions src/features/code-lens/implementationsCodeLens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2023 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type * as lsp from 'vscode-languageserver-protocol';
import { Location, Position, Range } from 'vscode-languageserver-protocol';
import type { LspDocument } from '../../document.js';
import { CommandTypes, ScriptElementKind, type ts } from '../../ts-protocol.js';
import * as typeConverters from '../../utils/typeConverters.js';
import { CodeLensType, ReferencesCodeLens, TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider.js';
import { ExecutionTarget } from '../../tsServer/server.js';

export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider {
protected get type(): CodeLensType {
return CodeLensType.Implementation;
}

public async resolveCodeLens(
codeLens: ReferencesCodeLens,
token: lsp.CancellationToken,
): Promise<ReferencesCodeLens> {
const document = this.client.toOpenDocument(codeLens.data!.uri);
if (!document) {
return codeLens;
}

if (!this.fileConfigurationManager.getWorkspacePreferencesForFile(document).implementationsCodeLens?.enabled) {
return codeLens;
}

const args = typeConverters.Position.toFileLocationRequestArgs(document.filepath, codeLens.range.start);
const response = await this.client.execute(CommandTypes.Implementation, args, token, {
lowPriority: true,
executionTarget: ExecutionTarget.Semantic,
cancelOnResourceChange: codeLens.data!.uri,
});
if (response.type !== 'response' || !response.body) {
codeLens.command = response.type === 'cancelled'
? TypeScriptBaseCodeLensProvider.cancelledCommand
: TypeScriptBaseCodeLensProvider.errorCommand;
return codeLens;
}

const locations = response.body
.map(reference =>
// Only take first line on implementation: https://github.com/microsoft/vscode/issues/23924
Location.create(this.client.toResource(reference.file).toString(),
reference.start.line === reference.end.line
? typeConverters.Range.fromTextSpan(reference)
: Range.create(
typeConverters.Position.fromLocation(reference.start),
Position.create(reference.start.line, 0))))
// Exclude original from implementations
.filter(location =>
!(location.uri.toString() === codeLens.data!.uri &&
location.range.start.line === codeLens.range.start.line &&
location.range.start.character === codeLens.range.start.character));

codeLens.command = this.getCommand(locations, codeLens);
return codeLens;
}

private getCommand(locations: Location[], codeLens: ReferencesCodeLens): lsp.Command | undefined {
return {
title: this.getTitle(locations),
command: locations.length ? 'editor.action.showReferences' : '',
arguments: [codeLens.data!.uri, codeLens.range.start, locations],
};
}

private getTitle(locations: Location[]): string {
return locations.length === 1
? '1 implementation'
: `${locations.length} implementations`;
}

protected extractSymbol(
document: LspDocument,
item: ts.server.protocol.NavigationTree,
_parent: ts.server.protocol.NavigationTree | undefined,
): lsp.Range | undefined {
switch (item.kind) {
case ScriptElementKind.interfaceElement:
return getSymbolRange(document, item);

case ScriptElementKind.classElement:
case ScriptElementKind.memberFunctionElement:
case ScriptElementKind.memberVariableElement:
case ScriptElementKind.memberGetAccessorElement:
case ScriptElementKind.memberSetAccessorElement:
if (item.kindModifiers.match(/\babstract\b/g)) {
return getSymbolRange(document, item);
}
break;
}
return undefined;
}
}
Loading