Path: blob/main/extensions/copilot/src/extension/context/node/resolvers/selectionContextHelpers.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { VsCodeTextDocument } from '../../../../platform/editing/common/abstractText';7import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';8import { ILanguageFeaturesService, isLocationLink } from '../../../../platform/languages/common/languageFeaturesService';9import { ILogService } from '../../../../platform/log/common/logService';10import { getStructureUsingIndentation } from '../../../../platform/parser/node/indentationStructure';11import { TreeSitterExpressionInfo } from '../../../../platform/parser/node/nodes';12import { IParserService, ParserWorkerTimeoutError, vscodeToTreeSitterOffsetRange } from '../../../../platform/parser/node/parserService';13import { TreeSitterUnknownLanguageError } from '../../../../platform/parser/node/treeSitterLanguages';14import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';15import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';16import { ILanguage } from '../../../../util/common/languages';1718/**19* @param timeoutMs This function makes several async computations, and each gets up to `3 * timeoutMs`. No guarantee is made about the total time.20*/21export async function findAllReferencedFunctionImplementationsInSelection(22parserService: IParserService,23logService: ILogService,24telemetryService: ITelemetryService,25languageFeaturesService: ILanguageFeaturesService,26workspaceService: IWorkspaceService,27document: TextDocumentSnapshot,28selection: vscode.Range,29timeoutMs: number30) {31const currentDocAST = parserService.getTreeSitterAST(document);32if (!currentDocAST) {33return [];34}3536// Parse all function calls in given selection37const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(selection, document);38const callExprs = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getCallExpressions(treeSitterOffsetRange), []);3940// find implementation or, if not found, definition for a call expression41async function findImplementation(callExpr: TreeSitterExpressionInfo) {42const position = document.positionAt(callExpr.startIndex);43try {44const impls = await languageFeaturesService.getImplementations(document.uri, position);45if (impls.length) {46return impls;47}48return await languageFeaturesService.getDefinitions(document.uri, position);49} catch {50return [];51}52}5354const implementations = await asyncComputeWithTimeBudget(55logService,56telemetryService,57document,58timeoutMs * 3, // apply a more generous timeout for language server results59() => Promise.all(callExprs.map(findImplementation)),60[]61);6263// since language service gives us only links to identifiers, expand to whole implementation/definition using tree-sitter64const functionImplementations = [];65for (let i = 0; i < implementations.length; i++) {66const callExpr = callExprs[i];67const impl = implementations[i];68for (const link of impl) {69const { uri, range } = isLocationLink(link) ? { uri: link.targetUri, range: link.targetRange } : link;70const textDocument = await workspaceService.openTextDocumentAndSnapshot(uri);71const treeSitterAST = parserService.getTreeSitterAST(textDocument);72if (treeSitterAST) {73const functionDefinitions = await treeSitterAST.getFunctionDefinitions(); // TODO: we should do this once per document, not once per call expression74const functionDefinition = functionDefinitions.find((fn) => fn.identifier === callExpr.identifier); // FIXME: this's incorrect because it doesn't count for import aliases (e.g., `import { foo as bar } from 'baz'`)75if (functionDefinition) {76const treeSitterRange = vscodeToTreeSitterOffsetRange(range, textDocument);77functionImplementations.push({78uri,79range,80version: textDocument.version,81identifier: callExpr.identifier,82startIndex: treeSitterRange.startIndex,83endIndex: treeSitterRange.endIndex,84text: functionDefinition.text,85});86}87}88}89}90if (functionImplementations.length !== 0) {91return functionImplementations;92}9394// For now, just search the current file for all functions95const allFunctions = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getFunctionDefinitions(), []);9697// Collect all function implementations referenced in the current selection98const allFunctionImplementations: TreeSitterExpressionInfo[] = [];99for (const fn of allFunctions) {100for (const callExpr of callExprs) {101if (fn.identifier === callExpr.identifier) {102allFunctionImplementations.push(fn);103}104}105}106107// Sort the function positions by start index108return allFunctionImplementations.sort((a, b) => a.startIndex - b.startIndex);109}110111export async function findAllReferencedClassDeclarationsInSelection(112parserService: IParserService,113logService: ILogService,114telemetryService: ITelemetryService,115languageFeaturesService: ILanguageFeaturesService,116workspaceService: IWorkspaceService,117document: TextDocumentSnapshot,118selection: vscode.Range,119timeoutMs: number120) {121const currentDocAST = parserService.getTreeSitterAST(document);122if (!currentDocAST) {123return [];124}125126// Parse all new expressions in active selection127const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(selection, document);128const matches = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getClassReferences(treeSitterOffsetRange), []);129130const implementations = await asyncComputeWithTimeBudget(131logService,132telemetryService,133document,134timeoutMs * 3, // apply a more generous timeout for language server results135async () => await Promise.all(matches.map(async (match) => {136try {137const position = document.positionAt(match.startIndex);138const impls = await languageFeaturesService.getImplementations(document.uri, position);139if (impls.length) {140return impls;141}142return await languageFeaturesService.getDefinitions(document.uri, position);143} catch {144return [];145}146})),147[]148);149const classDeclarations = [];150for (let i = 0; i < implementations.length; i++) {151const match = matches[i];152const impl = implementations[i];153for (const link of impl) {154const { uri, range } = isLocationLink(link) ? { uri: link.targetUri, range: link.targetRange } : link;155const textDocument = await workspaceService.openTextDocumentAndSnapshot(uri);156const treeSitterAST = parserService.getTreeSitterAST(textDocument);157if (treeSitterAST) {158const classDeclaration = (await treeSitterAST.getClassDeclarations()).find((fn) => fn.identifier === match.identifier);159if (classDeclaration) {160const treeSitterRange = vscodeToTreeSitterOffsetRange(range, textDocument);161classDeclarations.push({162uri,163range,164version: textDocument.version,165identifier: match.identifier,166startIndex: treeSitterRange.startIndex,167endIndex: treeSitterRange.endIndex,168text: classDeclaration.text,169});170}171}172}173}174if (classDeclarations.length !== 0) {175return classDeclarations;176}177178// For now, just search the current file for all class declarations179const allClasses = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getClassDeclarations(), []);180181// Collect all class declarations referenced in the current selection182const allClassDeclarations: TreeSitterExpressionInfo[] = [];183for (const fn of allClasses) {184for (const match of matches) {185if (fn.identifier === match.identifier) {186allClassDeclarations.push(fn);187}188}189}190191// Sort the class declaration positions by start index192return allClassDeclarations.sort((a, b) => a.startIndex - b.startIndex);193}194195export async function findAllReferencedTypeDeclarationsInSelection(196parserService: IParserService,197logService: ILogService,198telemetryService: ITelemetryService,199_languageFeaturesService: ILanguageFeaturesService,200_workspaceService: IWorkspaceService,201document: TextDocumentSnapshot,202selection: vscode.Range,203timeoutMs: number204) {205const currentDocAST = parserService.getTreeSitterAST(document);206if (!currentDocAST) {207return [];208}209210// Parse all type references in active selection211const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(selection, document);212const matches = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getTypeReferences(treeSitterOffsetRange), []);213214// For now, just search the current file for all type declarations215const allFunctions = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getTypeDeclarations(), []);216217// Collect all type declarations referenced in the current selection218const allTypeDeclarations: TreeSitterExpressionInfo[] = [];219for (const fn of allFunctions) {220for (const match of matches) {221if (fn.identifier === match.identifier) {222allTypeDeclarations.push(fn);223}224}225}226227// Sort the type declaration positions by start index228return allTypeDeclarations.sort((a, b) => a.startIndex - b.startIndex);229}230231/**232* Races the promise with a timeout.233*/234function raceWithTimeout<T>(235executor: Promise<T>,236timeoutMs: number237): Promise<{ type: 'success'; value: T } | { type: 'timeout' }> {238if (timeoutMs === 0) {239// no timeout240return executor.then(value => ({ type: 'success', value }));241}242243return new Promise((resolve, reject) => {244const timeoutId = setTimeout(() => resolve({ type: 'timeout' }), timeoutMs);245executor246.then(value => {247clearTimeout(timeoutId);248resolve({ type: 'success', value });249})250.catch(err => {251clearTimeout(timeoutId);252reject(err);253});254});255}256257/**258* @returns a promise that resolves to the result of `computation` if the document version is valid, otherwise to `defaultValue`259*/260export async function asyncComputeWithTimeBudget<T>(261logService: ILogService,262telemetryService: ITelemetryService,263document: TextDocumentSnapshot,264timeoutMs: number,265computation: () => Promise<T>,266defaultValue: T267): Promise<T> {268try {269const functionPositionsResult = await raceWithTimeout(270asyncComputeWithValidDocumentVersion(document, computation, defaultValue),271timeoutMs272);273274if (functionPositionsResult.type === 'success') {275return functionPositionsResult.value;276} else {277logService.warn(`Computing async parser based result took longer than ${timeoutMs}ms`);278return defaultValue;279}280} catch (err) {281if (!(err instanceof TreeSitterUnknownLanguageError)) {282logService.error(err, `Failed to compute async parser based result`);283telemetryService.sendGHTelemetryException(err, 'Failed to compute async parser based result');284}285return defaultValue;286}287}288289/**290* This function attempts to compute a value based on the provided document, ensuring that the document version remains consistent during the computation.291* If the document version changes during the computation, it will retry up to 3 times.292* If the document version continues to change after 3 attempts, it will return a default value.293*/294async function asyncComputeWithValidDocumentVersion<T>(295document: TextDocumentSnapshot,296computation: () => Promise<T>,297defaultValue: T,298attempt = 0299): Promise<T> {300const version = document.version;301const positions = await computation();302if (document.version !== version) {303// the document was changed in the meantime304if (attempt < 3) {305return asyncComputeWithValidDocumentVersion(document, computation, defaultValue, attempt + 1);306}307// we tried 3 times, but the document keeps changing308return defaultValue;309}310return positions;311}312313/**314* Artificial marker used to identify code blocks inside prompts315*/316export class FilePathCodeMarker {317318public static forDocument(language: ILanguage, document: TextDocumentSnapshot): string {319return this.forUri(language, document.uri);320}321322public static forUri(language: ILanguage, uri: vscode.Uri): string {323return `${this.forLanguage(language)}: ${uri.path}`;324}325326public static forLanguage(language: ILanguage): string {327return `${language.lineComment.start} FILEPATH`;328}329330/**331* Checks if the given code starts with a file path marker332*/333public static testLine(language: ILanguage, code: string): boolean {334const filenameMarker = FilePathCodeMarker.forLanguage(language);335return code.trimStart().startsWith(filenameMarker);336}337338}339340export async function getStructure(parserService: IParserService, document: TextDocumentSnapshot, formattingOptions: vscode.FormattingOptions | undefined) {341const currentDocAST = parserService.getTreeSitterAST(document);342if (currentDocAST) {343try {344const result = await currentDocAST.getStructure();345if (result) {346return result;347}348} catch (e) {349if (!(e instanceof ParserWorkerTimeoutError)) {350throw e;351}352}353}354return getStructureUsingIndentation(new VsCodeTextDocument(document), document.languageId, formattingOptions);355}356357358