typescript-language-server
Version:
Language Server Protocol (LSP) implementation for TypeScript using tsserver
190 lines • 7.43 kB
JavaScript
import * as lsp from 'vscode-languageserver';
import * as lspcalls from './lsp-protocol.calls.proposed.js';
import { uriToPath, toLocation, toSymbolKind, pathToUri } from './protocol-translation.js';
import { Range } from './utils/typeConverters.js';
export async function computeCallers(tspClient, args) {
const nullResult = { calls: [] };
const contextDefinition = await getDefinition(tspClient, args);
if (!contextDefinition) {
return nullResult;
}
const contextSymbol = await findEnclosingSymbol(tspClient, contextDefinition);
if (!contextSymbol) {
return nullResult;
}
const callerReferences = await findNonDefinitionReferences(tspClient, contextDefinition);
const calls = [];
for (const callerReference of callerReferences) {
const symbol = await findEnclosingSymbol(tspClient, callerReference);
if (!symbol) {
continue;
}
const location = toLocation(callerReference, undefined);
calls.push({
location,
symbol,
});
}
return { calls, symbol: contextSymbol };
}
export async function computeCallees(tspClient, args, documentProvider) {
const nullResult = { calls: [] };
const contextDefinition = await getDefinition(tspClient, args);
if (!contextDefinition) {
return nullResult;
}
const contextSymbol = await findEnclosingSymbol(tspClient, contextDefinition);
if (!contextSymbol) {
return nullResult;
}
const outgoingCallReferences = await findOutgoingCalls(tspClient, contextSymbol, documentProvider);
const calls = [];
for (const reference of outgoingCallReferences) {
const definitionReferences = await findDefinitionReferences(tspClient, reference);
const definitionReference = definitionReferences[0];
if (!definitionReference) {
continue;
}
const definitionSymbol = await findEnclosingSymbol(tspClient, definitionReference);
if (!definitionSymbol) {
continue;
}
const location = toLocation(reference, undefined);
calls.push({
location,
symbol: definitionSymbol,
});
}
return { calls, symbol: contextSymbol };
}
async function findOutgoingCalls(tspClient, contextSymbol, documentProvider) {
/**
* The TSP does not provide call references.
* As long as we are not able to access the AST in a tsserver plugin and return the information necessary as metadata to the reponse,
* we need to test possible calls.
*/
const computeCallCandidates = (document, range) => {
const symbolText = document.getText(range);
const regex = /\W([$_a-zA-Z0-9\u{00C0}-\u{E007F}]+)(<.*>)?\(/gmu; // Example: matches `candidate` in " candidate()", "Foo.candidate<T>()", etc.
let match = regex.exec(symbolText);
const candidates = [];
while (match) {
const identifier = match[1];
if (identifier) {
const start = match.index + match[0].indexOf(identifier);
const end = start + identifier.length;
candidates.push({ identifier, start, end });
}
match = regex.exec(symbolText);
}
const offset = document.offsetAt(range.start);
const candidateRanges = candidates.map(c => lsp.Range.create(document.positionAt(offset + c.start), document.positionAt(offset + c.end)));
return candidateRanges;
};
/**
* This function tests a candidate and returns a locaion for a valid call.
*/
const validateCall = async (file, candidateRange) => {
const tspPosition = { line: candidateRange.start.line + 1, offset: candidateRange.start.character + 1 };
const references = await findNonDefinitionReferences(tspClient, { file, start: tspPosition, end: tspPosition });
for (const reference of references) {
const tspPosition = { line: candidateRange.start.line + 1, offset: candidateRange.start.character + 1 };
if (tspPosition.line === reference.start.line) {
return reference;
}
}
};
const calls = [];
const file = uriToPath(contextSymbol.location.uri);
const document = documentProvider(file);
if (!document) {
return calls;
}
const candidateRanges = computeCallCandidates(document, contextSymbol.location.range);
for (const candidateRange of candidateRanges) {
const call = await validateCall(file, candidateRange);
if (call) {
calls.push(call);
}
}
return calls;
}
async function getDefinition(tspClient, args) {
const file = uriToPath(args.textDocument.uri);
if (!file) {
return undefined;
}
const definitionResult = await tspClient.request("definition" /* CommandTypes.Definition */, {
file,
line: args.position.line + 1,
offset: args.position.character + 1,
});
return definitionResult.body ? definitionResult.body[0] : undefined;
}
async function findEnclosingSymbol(tspClient, args) {
const file = args.file;
const response = await tspClient.request("navtree" /* CommandTypes.NavTree */, { file });
const tree = response.body;
if (!tree || !tree.childItems) {
return undefined;
}
const pos = lsp.Position.create(args.start.line - 1, args.start.offset - 1);
const symbol = findEnclosingSymbolInTree(tree, lsp.Range.create(pos, pos));
if (!symbol) {
return undefined;
}
const uri = pathToUri(file, undefined);
return lspcalls.DefinitionSymbol.create(uri, symbol);
}
function findEnclosingSymbolInTree(parent, range) {
const inSpan = (span) => !!Range.intersection(Range.fromTextSpan(span), range);
const inTree = (tree) => tree.spans.some(span => inSpan(span));
let candidate = inTree(parent) ? parent : undefined;
outer: while (candidate) {
const children = candidate.childItems || [];
for (const child of children) {
if (inTree(child)) {
candidate = child;
continue outer;
}
}
break;
}
if (!candidate) {
return undefined;
}
const span = candidate.spans.find(span => inSpan(span));
const spanRange = Range.fromTextSpan(span);
let selectionRange = spanRange;
if (candidate.nameSpan) {
const nameRange = Range.fromTextSpan(candidate.nameSpan);
if (Range.intersection(spanRange, nameRange)) {
selectionRange = nameRange;
}
}
return {
name: candidate.text,
kind: toSymbolKind(candidate.kind),
range: spanRange,
selectionRange: selectionRange,
};
}
async function findDefinitionReferences(tspClient, args) {
return (await findReferences(tspClient, args)).filter(ref => ref.isDefinition);
}
async function findNonDefinitionReferences(tspClient, args) {
return (await findReferences(tspClient, args)).filter(ref => !ref.isDefinition);
}
async function findReferences(tspClient, args) {
const file = args.file;
const result = await tspClient.request("references" /* CommandTypes.References */, {
file,
line: args.start.line,
offset: args.start.offset,
});
if (!result.body) {
return [];
}
return result.body.refs;
}
//# sourceMappingURL=calls.js.map