Skip to content

Commit

Permalink
feat: support LocationLink[] for textDocument/definition response (#563)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl committed Aug 19, 2022
1 parent 7c0ad9f commit 196f328
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 158 deletions.
13 changes: 7 additions & 6 deletions src/calls.ts
@@ -1,11 +1,12 @@

import type tsp from 'typescript/lib/protocol.d.js';
import * as lsp from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as lspcalls from './lsp-protocol.calls.proposed.js';
import { TspClient } from './tsp-client.js';
import { CommandTypes } from './tsp-command-types.js';
import { uriToPath, toLocation, asRange, Range, toSymbolKind, pathToUri } from './protocol-translation.js';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { uriToPath, toLocation, toSymbolKind, pathToUri } from './protocol-translation.js';
import { Range } from './utils/typeConverters.js';

export async function computeCallers(tspClient: TspClient, args: lsp.TextDocumentPositionParams): Promise<lspcalls.CallsResult> {
const nullResult = { calls: [] };
Expand Down Expand Up @@ -140,7 +141,7 @@ async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Pr
return undefined;
}
const pos = lsp.Position.create(args.start.line - 1, args.start.offset - 1);
const symbol = await findEnclosingSymbolInTree(tree, lsp.Range.create(pos, pos));
const symbol = findEnclosingSymbolInTree(tree, lsp.Range.create(pos, pos));
if (!symbol) {
return undefined;
}
Expand All @@ -149,7 +150,7 @@ async function findEnclosingSymbol(tspClient: TspClient, args: tsp.FileSpan): Pr
}

function findEnclosingSymbolInTree(parent: tsp.NavigationTree, range: lsp.Range): lsp.DocumentSymbol | undefined {
const inSpan = (span: tsp.TextSpan) => !!Range.intersection(asRange(span), range);
const inSpan = (span: tsp.TextSpan) => !!Range.intersection(Range.fromTextSpan(span), range);
const inTree = (tree: tsp.NavigationTree) => tree.spans.some(span => inSpan(span));

let candidate = inTree(parent) ? parent : undefined;
Expand All @@ -167,10 +168,10 @@ function findEnclosingSymbolInTree(parent: tsp.NavigationTree, range: lsp.Range)
return undefined;
}
const span = candidate.spans.find(span => inSpan(span))!;
const spanRange = asRange(span);
const spanRange = Range.fromTextSpan(span);
let selectionRange = spanRange;
if (candidate.nameSpan) {
const nameRange = asRange(candidate.nameSpan);
const nameRange = Range.fromTextSpan(candidate.nameSpan);
if (Range.intersection(spanRange, nameRange)) {
selectionRange = nameRange;
}
Expand Down
10 changes: 5 additions & 5 deletions src/completion.ts
Expand Up @@ -9,12 +9,12 @@ import * as lsp from 'vscode-languageserver';
import type tsp from 'typescript/lib/protocol.js';
import { LspDocument } from './document.js';
import { CommandTypes, KindModifiers, ScriptElementKind } from './tsp-command-types.js';
import { asRange, toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation.js';
import { toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation.js';
import { Commands } from './commands.js';
import { TspClient } from './tsp-client.js';
import { CompletionOptions, DisplayPartKind, SupportedFeatures } from './ts-protocol.js';
import SnippetString from './utils/SnippetString.js';
import * as typeConverters from './utils/typeConverters.js';
import { Range, Position } from './utils/typeConverters.js';

interface ParameterListParts {
readonly parts: ReadonlyArray<tsp.SymbolDisplayPart>;
Expand Down Expand Up @@ -62,7 +62,7 @@ export function asCompletionItem(entry: tsp.CompletionEntry, file: string, posit
}

let insertText = entry.insertText;
let replacementRange = entry.replacementSpan && asRange(entry.replacementSpan);
let replacementRange = entry.replacementSpan && Range.fromTextSpan(entry.replacementSpan);
// Make sure we only replace a single line at most
if (replacementRange && replacementRange.start.line !== replacementRange.end.line) {
replacementRange = lsp.Range.create(replacementRange.start, document.getLineEnd(replacementRange.start.line));
Expand Down Expand Up @@ -202,7 +202,7 @@ export async function asResolvedCompletionItem(
}
if (features.completionSnippets && options.completeFunctionCalls && (item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) {
const { line, offset } = item.data;
const position = typeConverters.Position.fromLocation({ line, offset });
const position = Position.fromLocation({ line, offset });
const shouldCompleteFunction = await isValidFunctionCompletionContext(filepath, position, client);
if (shouldCompleteFunction) {
createSnippetOfFunctionCall(item, details);
Expand All @@ -216,7 +216,7 @@ export async function isValidFunctionCompletionContext(filepath: string, positio
// Workaround for https://github.com/Microsoft/TypeScript/issues/12677
// Don't complete function calls inside of destructive assigments or imports
try {
const args: tsp.FileLocationRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const args: tsp.FileLocationRequestArgs = Position.toFileLocationRequestArgs(filepath, position);
const response = await client.request(CommandTypes.Quickinfo, args);
if (response.type !== 'response') {
return true;
Expand Down
15 changes: 8 additions & 7 deletions src/document-symbol.ts
Expand Up @@ -7,34 +7,35 @@

import * as lsp from 'vscode-languageserver';
import type tsp from 'typescript/lib/protocol.d.js';
import { asRange, toSymbolKind, Range } from './protocol-translation.js';
import { toSymbolKind } from './protocol-translation.js';
import { ScriptElementKind } from './tsp-command-types.js';
import { Range } from './utils/typeConverters.js';

export function collectDocumentSymbols(parent: tsp.NavigationTree, symbols: lsp.DocumentSymbol[]): boolean {
return collectDocumentSymbolsInRange(parent, symbols, { start: asRange(parent.spans[0]).start, end: asRange(parent.spans[parent.spans.length - 1]).end });
return collectDocumentSymbolsInRange(parent, symbols, { start: Range.fromTextSpan(parent.spans[0]).start, end: Range.fromTextSpan(parent.spans[parent.spans.length - 1]).end });
}

function collectDocumentSymbolsInRange(parent: tsp.NavigationTree, symbols: lsp.DocumentSymbol[], range: lsp.Range): boolean {
let shouldInclude = shouldIncludeEntry(parent);

for (const span of parent.spans) {
const spanRange = asRange(span);
const spanRange = Range.fromTextSpan(span);
if (!Range.intersection(range, spanRange)) {
continue;
}

const children: lsp.DocumentSymbol[] = [];
if (parent.childItems) {
for (const child of parent.childItems) {
if (child.spans.some(childSpan => !!Range.intersection(spanRange, asRange(childSpan)))) {
if (child.spans.some(childSpan => !!Range.intersection(spanRange, Range.fromTextSpan(childSpan)))) {
const includedChild = collectDocumentSymbolsInRange(child, children, spanRange);
shouldInclude = shouldInclude || includedChild;
}
}
}
let selectionRange = spanRange;
if (parent.nameSpan) {
const nameRange = asRange(parent.nameSpan);
const nameRange = Range.fromTextSpan(parent.nameSpan);
// In the case of mergeable definitions, the nameSpan is only correct for the first definition.
if (Range.intersection(spanRange, nameRange)) {
selectionRange = nameRange;
Expand All @@ -59,11 +60,11 @@ export function collectSymbolInformation(uri: string, current: tsp.NavigationTre
let shouldInclude = shouldIncludeEntry(current);
const name = current.text;
for (const span of current.spans) {
const range = asRange(span);
const range = Range.fromTextSpan(span);
const children: lsp.SymbolInformation[] = [];
if (current.childItems) {
for (const child of current.childItems) {
if (child.spans.some(span => !!Range.intersection(range, asRange(span)))) {
if (child.spans.some(span => !!Range.intersection(range, Range.fromTextSpan(span)))) {
const includedChild = collectSymbolInformation(uri, child, children, name);
shouldInclude = shouldInclude || includedChild;
}
Expand Down
7 changes: 4 additions & 3 deletions src/features/fix-all.ts
Expand Up @@ -6,12 +6,13 @@
import type tsp from 'typescript/lib/protocol.d.js';
import * as lsp from 'vscode-languageserver';
import { LspDocuments } from '../document.js';
import { toFileRangeRequestArgs, toTextDocumentEdit } from '../protocol-translation.js';
import { toTextDocumentEdit } from '../protocol-translation.js';
import { TspClient } from '../tsp-client.js';
import { CommandTypes } from '../tsp-command-types.js';
import * as errorCodes from '../utils/errorCodes.js';
import * as fixNames from '../utils/fixNames.js';
import { CodeActionKind } from '../utils/types.js';
import { Range } from '../utils/typeConverters.js';

interface AutoFix {
readonly codes: Set<number>;
Expand All @@ -33,7 +34,7 @@ async function buildIndividualFixes(
}

const args: tsp.CodeFixRequestArgs = {
...toFileRangeRequestArgs(file, diagnostic.range),
...Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+diagnostic.code!]
};

Expand Down Expand Up @@ -67,7 +68,7 @@ async function buildCombinedFix(
}

const args: tsp.CodeFixRequestArgs = {
...toFileRangeRequestArgs(file, diagnostic.range),
...Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+diagnostic.code!]
};

Expand Down
4 changes: 2 additions & 2 deletions src/features/source-definition.ts
Expand Up @@ -11,7 +11,7 @@

import * as lsp from 'vscode-languageserver';
import API from '../utils/api.js';
import * as typeConverters from '../utils/typeConverters.js';
import { Position } from '../utils/typeConverters.js';
import { toLocation, uriToPath } from '../protocol-translation.js';
import type { LspDocuments } from '../document.js';
import type { TspClient } from '../tsp-client.js';
Expand Down Expand Up @@ -54,7 +54,7 @@ export class SourceDefinitionCommand {
return;
}

const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
const args = Position.toFileLocationRequestArgs(file, position);
return await lspClient.withProgress<lsp.Location[] | void>({
message: 'Finding source definitions…',
reporter
Expand Down
115 changes: 115 additions & 0 deletions src/lsp-server.spec.ts
Expand Up @@ -406,6 +406,121 @@ describe('completion', () => {
});
});

describe('definition', () => {
it('goes to 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 definitions = await server.definition({
textDocument: indexDoc,
position: position(indexDoc, 'a/*identifier*/')
}) as lsp.Location[];
assert.isArray(definitions);
assert.equal(definitions!.length, 1);
assert.deepEqual(definitions![0], {
uri: uri('source-definition', 'a.d.ts'),
range: {
start: {
line: 0,
character: 21
},
end: {
line: 0,
character: 22
}
}
});
});
});

describe('definition (definition link supported)', () => {
let localServer: TestLspServer;

before(async () => {
const clientCapabilitiesOverride: lsp.ClientCapabilities = {
textDocument: {
definition: {
linkSupport: true
}
}
};
localServer = await createServer({
rootUri: uri('source-definition'),
publishDiagnostics: args => diagnostics.set(args.uri, args),
clientCapabilitiesOverride
});
});

beforeEach(() => {
localServer.closeAll();
// "closeAll" triggers final publishDiagnostics with an empty list so clear last.
diagnostics.clear();
localServer.workspaceEdits = [];
});

after(() => {
localServer.closeAll();
localServer.shutdown();
});

it('goes to 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'))
};
localServer.didOpenTextDocument({ textDocument: indexDoc });
const definitions = await localServer.definition({
textDocument: indexDoc,
position: position(indexDoc, 'a/*identifier*/')
}) as lsp.DefinitionLink[];
assert.isArray(definitions);
assert.equal(definitions!.length, 1);
assert.deepEqual(definitions![0], {
originSelectionRange: {
start: {
line: 1,
character: 0
},
end: {
line: 1,
character: 1
}
},
targetRange: {
start: {
line: 0,
character: 0
},
end: {
line: 0,
character: 30
}
},
targetUri: uri('source-definition', 'a.d.ts'),
targetSelectionRange: {
start: {
line: 0,
character: 21
},
end: {
line: 0,
character: 22
}
}
});
});
});

describe('diagnostics', () => {
it('simple test', async () => {
const doc = {
Expand Down

0 comments on commit 196f328

Please sign in to comment.