UNPKG

typescript-language-server

Version:

Language Server Protocol (LSP) implementation for TypeScript using tsserver

190 lines 7.43 kB
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