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

Skip to content

feat: add "Go To Source Definition" command #560

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 2 commits into from
Aug 18, 2022
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
128 changes: 114 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,32 @@ Based on concepts and ideas from https://github.com/prabirshrestha/typescript-la

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

<!-- MarkdownTOC -->

- [Installing](#installing)
- [Running the language server](#running-the-language-server)
- [CLI Options](#cli-options)
- [initializationOptions](#initializationoptions)
- [workspace/didChangeConfiguration](#workspacedidchangeconfiguration)
- [Code actions on save](#code-actions-on-save)
- [Workspace commands \(`workspace/executeCommand`\)](#workspace-commands-workspaceexecutecommand)
- [Go to Source Definition](#go-to-source-definition)
- [Apply Workspace Edits](#apply-workspace-edits)
- [Apply Code Action](#apply-code-action)
- [Apply Refactoring](#apply-refactoring)
- [Organize Imports](#organize-imports)
- [Rename File](#rename-file)
- [Inlay hints \(`typescript/inlayHints`\) \(experimental\)](#inlay-hints-typescriptinlayhints-experimental)
- [Callers and callees \(`textDocument/calls`\) \(experimental\)](#callers-and-callees-textdocumentcalls-experimental)
- [Supported Protocol features](#supported-protocol-features)
- [Development](#development)
- [Build](#build)
- [Test](#test)
- [Watch](#watch)
- [Publishing](#publishing)

<!-- /MarkdownTOC -->

## Installing

```sh
Expand Down Expand Up @@ -247,34 +273,108 @@ The user can enable it with a setting similar to (can vary per-editor):

## Workspace commands (`workspace/executeCommand`)

See [LSP specification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspace_executeCommand).
See [LSP specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand).

Most of the time, you'll execute commands with arguments retrieved from another request like `textDocument/codeAction`. There are some use cases for calling them manually.

Supported commands:
`lsp` refers to the language server protocol types, `tsp` refers to the typescript server protocol types.

### Go to Source Definition

- Request:
```ts
{
command: `_typescript.goToSourceDefinition`
arguments: [
lsp.DocumentUri, // String URI of the document
lsp.Position, // Line and character position (zero-based)
]
}
```
- Response:
```ts
lsp.Location[] | null
```

(This command is supported from Typescript 4.7.)

### Apply Workspace Edits

- Request:
```ts
{
command: `_typescript.applyWorkspaceEdit`
arguments: [lsp.WorkspaceEdit]
}
```
- Response:
```ts
lsp.ApplyWorkspaceEditResult
```

### Apply Code Action

- Request:
```ts
{
command: `_typescript.applyCodeAction`
arguments: [
tsp.CodeAction, // TypeScript Code Action object
]
}
```
- Response:
```ts
void
```

`lsp` refers to the language server protocol, `tsp` refers to the typescript server protocol.
### Apply Refactoring

* `_typescript.applyWorkspaceEdit`
- Request:
```ts
type Arguments = [lsp.WorkspaceEdit]
{
command: `_typescript.applyRefactoring`
arguments: [
tsp.GetEditsForRefactorRequestArgs,
]
}
```
* `_typescript.applyCodeAction`
- Response:
```ts
type Arguments = [tsp.CodeAction]
void
```
* `_typescript.applyRefactoring`

### Organize Imports

- Request:
```ts
{
command: `_typescript.organizeImports`
arguments: [
// The "skipDestructiveCodeActions" argument is supported from Typescript 4.4+
[string] | [string, { skipDestructiveCodeActions?: boolean }],
]
}
```
- Response:
```ts
type Arguments = [tsp.GetEditsForRefactorRequestArgs]
void
```
* `_typescript.organizeImports`

### Rename File

- Request:
```ts
// The "skipDestructiveCodeActions" argument is supported from Typescript 4.4+
type Arguments = [string] | [string, { skipDestructiveCodeActions?: boolean }]
{
command: `_typescript.applyRenameFile`
arguments: [
{ sourceUri: string; targetUri: string; },
]
}
```
* `_typescript.applyRenameFile`
- Response:
```ts
type Arguments = [{ sourceUri: string; targetUri: string; }]
void
```

## Inlay hints (`typescript/inlayHints`) (experimental)
Expand Down
5 changes: 4 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import { SourceDefinitionCommand } from './features/source-definition.js';

export const Commands = {
APPLY_WORKSPACE_EDIT: '_typescript.applyWorkspaceEdit',
APPLY_CODE_ACTION: '_typescript.applyCodeAction',
Expand All @@ -13,5 +15,6 @@ export const Commands = {
APPLY_RENAME_FILE: '_typescript.applyRenameFile',
APPLY_COMPLETION_CODE_ACTION: '_typescript.applyCompletionCodeAction',
/** Commands below should be implemented by the client */
SELECT_REFACTORING: '_typescript.selectRefactoring'
SELECT_REFACTORING: '_typescript.selectRefactoring',
SOURCE_DEFINITION: SourceDefinitionCommand.id
};
69 changes: 69 additions & 0 deletions src/features/source-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2022 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 * as lsp from 'vscode-languageserver';
import API from '../utils/api.js';
import * as typeConverters from '../utils/typeConverters.js';
import { toLocation, uriToPath } from '../protocol-translation.js';
import type { LspDocuments } from '../document.js';
import type { TspClient } from '../tsp-client.js';
import type { LspClient } from '../lsp-client.js';
import { CommandTypes } from '../tsp-command-types.js';

export class SourceDefinitionCommand {
public static readonly id = '_typescript.goToSourceDefinition';
public static readonly minVersion = API.v470;

public static async execute(
uri: lsp.DocumentUri | undefined,
position: lsp.Position | undefined,
documents: LspDocuments,
tspClient: TspClient,
lspClient: LspClient,
reporter: lsp.WorkDoneProgressReporter
): Promise<lsp.Location[] | void> {
if (tspClient.apiVersion.lt(SourceDefinitionCommand.minVersion)) {
lspClient.showErrorMessage('Go to Source Definition failed. Requires TypeScript 4.7+.');
return;
}

if (!position || typeof position.character !== 'number' || typeof position.line !== 'number') {
lspClient.showErrorMessage('Go to Source Definition failed. Invalid position.');
return;
}

let file: string | undefined;

if (!uri || typeof uri !== 'string' || !(file = uriToPath(uri))) {
lspClient.showErrorMessage('Go to Source Definition failed. No resource provided.');
return;
}

const document = documents.get(file);

if (!document) {
lspClient.showErrorMessage('Go to Source Definition failed. File not opened in the editor.');
return;
}

const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
return await lspClient.withProgress<lsp.Location[] | void>({
message: 'Finding source definitions…',
reporter
}, async () => {
const response = await tspClient.request(CommandTypes.FindSourceDefinition, args);
if (response.type === 'response' && response.body?.length) {
return response.body.map(reference => toLocation(reference, documents));
}
lspClient.showErrorMessage('No source definitions found.');
});
}
}
73 changes: 30 additions & 43 deletions src/lsp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,50 @@
*/

import * as lsp from 'vscode-languageserver';
import { MessageType } from 'vscode-languageserver';
import { attachWorkDone } from 'vscode-languageserver/lib/common/progress.js';
import { TypeScriptRenameRequest } from './ts-protocol.js';

export interface ProgressReporter {
begin(message?: string): void;
report(message: string): void;
end(): void;
export interface WithProgressOptions {
message: string;
reporter: lsp.WorkDoneProgressReporter;
}

export interface LspClient {
setClientCapabilites(capabilites: lsp.ClientCapabilities): void;
createProgressReporter(): ProgressReporter;
createProgressReporter(token?: lsp.CancellationToken, workDoneProgress?: lsp.WorkDoneProgressReporter): Promise<lsp.WorkDoneProgressReporter>;
withProgress<R>(options: WithProgressOptions, task: (progress: lsp.WorkDoneProgressReporter) => Promise<R>): Promise<R>;
publishDiagnostics(args: lsp.PublishDiagnosticsParams): void;
showMessage(args: lsp.ShowMessageParams): void;
showErrorMessage(message: string): void;
logMessage(args: lsp.LogMessageParams): void;
applyWorkspaceEdit(args: lsp.ApplyWorkspaceEditParams): Promise<lsp.ApplyWorkspaceEditResult>;
telemetry(args: any): void;
rename(args: lsp.TextDocumentPositionParams): Promise<any>;
}

export class LspClientImpl implements LspClient {
private clientCapabilities?: lsp.ClientCapabilities;
// Hack around the LSP library that makes it otherwise impossible to differentiate between Null and Client-initiated reporter.
const nullProgressReporter = attachWorkDone(undefined as any, /* params */ undefined);

export class LspClientImpl implements LspClient {
constructor(protected connection: lsp.Connection) {}

setClientCapabilites(capabilites: lsp.ClientCapabilities): void {
this.clientCapabilities = capabilites;
async createProgressReporter(_?: lsp.CancellationToken, workDoneProgress?: lsp.WorkDoneProgressReporter): Promise<lsp.WorkDoneProgressReporter> {
let reporter: lsp.WorkDoneProgressReporter;
if (workDoneProgress && workDoneProgress.constructor !== nullProgressReporter.constructor) {
reporter = workDoneProgress;
} else {
reporter = workDoneProgress || await this.connection.window.createWorkDoneProgress();
}
return reporter;
}

createProgressReporter(): ProgressReporter {
let workDoneProgress: Promise<lsp.WorkDoneProgressServerReporter> | undefined;
return {
begin: (message = '') => {
if (this.clientCapabilities?.window?.workDoneProgress) {
workDoneProgress = this.connection.window.createWorkDoneProgress();
workDoneProgress
.then((progress) => {
progress.begin(message);
})
.catch(() => {});
}
},
report: (message: string) => {
if (workDoneProgress) {
workDoneProgress
.then((progress) => {
progress.report(message);
})
.catch(() => {});
}
},
end: () => {
if (workDoneProgress) {
workDoneProgress
.then((progress) => {
progress.done();
})
.catch(() => {});
workDoneProgress = undefined;
}
}
};
async withProgress<R = unknown>(options: WithProgressOptions, task: (progress: lsp.WorkDoneProgressReporter) => Promise<R>): Promise<R> {
const { message, reporter } = options;
reporter.begin(message);
return task(reporter).then(result => {
reporter.done();
return result;
});
}

publishDiagnostics(args: lsp.PublishDiagnosticsParams): void {
Expand All @@ -77,6 +60,10 @@ export class LspClientImpl implements LspClient {
this.connection.sendNotification(lsp.ShowMessageNotification.type, args);
}

showErrorMessage(message: string): void {
this.connection.sendNotification(lsp.ShowMessageNotification.type, { type: MessageType.Error, message });
}

logMessage(args: lsp.LogMessageParams): void {
this.connection.sendNotification(lsp.LogMessageNotification.type, args);
}
Expand Down
36 changes: 35 additions & 1 deletion src/lsp-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ let server: TestLspServer;

before(async () => {
server = await createServer({
rootUri: null,
rootUri: uri(),
publishDiagnostics: args => diagnostics.set(args.uri, args)
});
server.didChangeConfiguration({
Expand Down Expand Up @@ -1504,6 +1504,40 @@ describe('executeCommand', () => {
]
);
});

it('go to source definition', async () => {
// NOTE: This test needs to reference files that physically exist for the feature to work.
const indexUri = uri('source-definition', 'index.ts');
const indexDoc = {
uri: indexUri,
languageId: 'typescript',
version: 1,
text: readContents(filePath('source-definition', 'index.ts'))
};
server.didOpenTextDocument({ textDocument: indexDoc });
const result: lsp.Location[] | null = await server.executeCommand({
command: Commands.SOURCE_DEFINITION,
arguments: [
indexUri,
position(indexDoc, '/*identifier*/')
]
});
assert.isNotNull(result);
assert.equal(result!.length, 1);
assert.deepEqual(result![0], {
uri: uri('source-definition', 'a.js'),
range: {
start: {
line: 0,
character: 13
},
end: {
line: 0,
character: 14
}
}
});
});
});

describe('documentHighlight', () => {
Expand Down
Loading