Path: blob/main/extensions/copilot/src/extension/linkify/vscode-node/findWord.ts
13399 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 * as vscode from 'vscode';6import { TextDocument } from 'vscode-languageserver-textdocument';7import { TreeSitterExpressionInfo } from '../../../platform/parser/node/nodes';8import { IParserService } from '../../../platform/parser/node/parserService';9import { getWasmLanguage } from '../../../platform/parser/node/treeSitterLanguages';10import { getLanguageForResource } from '../../../util/common/languages';11import { Limiter } from '../../../util/vs/base/common/async';12import { CancellationToken } from '../../../util/vs/base/common/cancellation';13import { escapeRegExpCharacters } from '../../../util/vs/base/common/strings';14import { isUriComponents, URI } from '../../../util/vs/base/common/uri';15import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';16import { PromptReference } from '../../prompt/common/conversation';1718/**19* How the word was resolved.20*/21enum ResolvedWordLocationType {22// Ordered by priority. Higher properties are preferred.2324/** Resolve using string matching */25TextualMatch = 1,2627/** Resolve by matching a symbol name in code */28SymbolMatch = 2,2930/** Resolve by matching a definition in code */31// TODO: not implemented yet32Definition = 3,33}3435interface ResolvedWordLocation {36readonly type: ResolvedWordLocationType;37readonly location: vscode.Location;38}3940interface FindWordOptions {41readonly symbolMatchesOnly?: boolean;42readonly maxResultCount?: number;43}4445export async function findWordInReferences(46accessor: ServicesAccessor,47references: readonly PromptReference[],48word: string,49options: FindWordOptions,50token: CancellationToken,51documentCache?: Map<string, Promise<SimpleTextDocument | undefined>>,52): Promise<vscode.Location[]> {53const parserService = accessor.get(IParserService);5455const out: ResolvedWordLocation[] = [];56const maxResultCount = options.maxResultCount ?? Infinity;57const limiter = new Limiter<void>(10);58try {59await Promise.all(references.map(ref =>60limiter.queue(async () => {61if (out.length >= maxResultCount || token.isCancellationRequested) {62return;63}6465let loc: ResolvedWordLocation | undefined;66if (isUriComponents(ref.anchor)) {67loc = await findWordInDoc(parserService, word, ref.anchor, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), options, token, documentCache);68} else if ('range' in ref.anchor) {69loc = await findWordInDoc(parserService, word, ref.anchor.uri, ref.anchor.range, options, token, documentCache);70} else if ('value' in ref.anchor && URI.isUri(ref.anchor.value)) {71loc = await findWordInDoc(parserService, word, ref.anchor.value, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), options, token, documentCache);72}7374if (loc) {75out.push(loc);76}77})));78} finally {79limiter.dispose();80}8182return out83.sort((a, b) => b.type - a.type)84.map(x => x.location)85.slice(0, options.maxResultCount);86}8788async function findWordInDoc(parserService: IParserService, word: string, uri: vscode.Uri, range: vscode.Range, options: FindWordOptions, token: vscode.CancellationToken, documentCache?: Map<string, Promise<SimpleTextDocument | undefined>>): Promise<ResolvedWordLocation | undefined> {89if (options.symbolMatchesOnly) {90const languageId = getLanguageForResource(uri).languageId;91if (!getWasmLanguage(languageId)) {92return;93}94}9596const doc = await openDocument(uri, documentCache);97if (!doc || token.isCancellationRequested) {98return;99}100101const symbols = await getSymbolsInRange(parserService, doc, range, token);102if (token.isCancellationRequested) {103return;104}105106for (const symbol of symbols) {107if (symbol.identifier === word) {108const pos = doc.positionAt(symbol.startIndex);109return { type: ResolvedWordLocationType.SymbolMatch, location: new vscode.Location(uri, pos) };110}111}112113if (options.symbolMatchesOnly) {114return;115}116117// Fall back to word based118const text = doc.getText(range);119const startOffset = doc.offsetAt(range.start);120for (const match of text.matchAll(new RegExp(escapeRegExpCharacters(word), 'g'))) {121if (match.index) {122const wordPos = doc.positionAt(startOffset + match.index);123if ('getWordRangeAtPosition' in doc) {124const wordInDoc = doc.getText((doc as vscode.TextDocument).getWordRangeAtPosition(wordPos));125if (word === wordInDoc) {126return { type: ResolvedWordLocationType.TextualMatch, location: new vscode.Location(uri, wordPos) };127}128} else {129const wordInDoc = doc.getText(new vscode.Range(wordPos, doc.positionAt(doc.offsetAt(wordPos) + word.length)));130if (word === wordInDoc) {131return { type: ResolvedWordLocationType.TextualMatch, location: new vscode.Location(uri, wordPos) };132}133}134}135}136137return undefined;138}139140141interface SimpleTextDocument {142readonly languageId: string;143144getText(range?: vscode.Range): string;145146offsetAt(position: vscode.Position): number;147148positionAt(offset: number): vscode.Position;149}150151152async function openDocument(uri: vscode.Uri, documentCache?: Map<string, Promise<SimpleTextDocument | undefined>>): Promise<SimpleTextDocument | undefined> {153const vsCodeDoc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());154if (vsCodeDoc) {155return vsCodeDoc;156}157158if (documentCache) {159const key = uri.toString();160const existing = documentCache.get(key);161if (existing) {162return existing;163}164165const pending = doOpenDocument(uri);166documentCache.set(key, pending);167return pending;168}169170return doOpenDocument(uri);171}172173async function doOpenDocument(uri: vscode.Uri): Promise<SimpleTextDocument | undefined> {174try {175const contents = await vscode.workspace.fs.readFile(uri);176const languageId = getLanguageForResource(uri).languageId;177const doc = TextDocument.create(uri.toString(), languageId, 0, new TextDecoder().decode(contents));178return new class implements SimpleTextDocument {179readonly languageId = languageId;180getText(range?: vscode.Range): string {181return doc.getText(range);182}183offsetAt(position: vscode.Position): number {184return doc.offsetAt(position);185}186positionAt(offset: number): vscode.Position {187const pos = doc.positionAt(offset);188return new vscode.Position(pos.line, pos.character);189}190};191} catch {192return undefined;193}194}195196async function getSymbolsInRange(parserService: IParserService, doc: SimpleTextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise<TreeSitterExpressionInfo[]> {197const wasmLanguage = getWasmLanguage(doc.languageId);198if (!wasmLanguage) {199return [];200}201202const ast = parserService.getTreeSitterASTForWASMLanguage(wasmLanguage, doc.getText());203if (!ast) {204return [];205}206207return ast.getSymbols({208startIndex: doc.offsetAt(range.start),209endIndex: doc.offsetAt(range.end),210});211}212213export class ReferencesSymbolResolver {214/** Symbols which we have already tried to resolve */215private readonly cache = new Map<string, Promise<vscode.Location[] | undefined>>();216private readonly documentCache = new Map<string, Promise<SimpleTextDocument | undefined>>();217218constructor(219private readonly findWordOptions: FindWordOptions,220@IInstantiationService private readonly instantiationService: IInstantiationService221) { }222223async resolve(codeText: string, references: readonly PromptReference[], token: CancellationToken): Promise<vscode.Location[] | undefined> {224if (!references.length) {225return;226}227228const existing = this.cache.get(codeText);229if (existing) {230return existing;231} else {232const p = this.doResolve(codeText, references, token);233this.cache.set(codeText, p);234return p;235}236}237238private async doResolve(codeText: string, references: readonly PromptReference[], token: CancellationToken): Promise<vscode.Location[] | undefined> {239// Prefer exact match240let wordMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, codeText, this.findWordOptions, token, this.documentCache));241if (token.isCancellationRequested) {242return;243}244245// But then try breaking up inline code into symbol parts246if (!wordMatches.length) {247// Extract all symbol parts from the code text248// For example: `TextModel.undo()` -> ['TextModel', 'undo']249const symbolParts = Array.from(codeText.matchAll(/[#\w$][\w\d$]*/g), x => x[0]);250251if (symbolParts.length >= 2) {252// For qualified names like `Class.method()`, search for both parts together253// This helps disambiguate when there are multiple methods with the same name254const firstPart = symbolParts[0];255const lastPart = symbolParts[symbolParts.length - 1];256257// First, try to find the class258const classMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, firstPart, {259symbolMatchesOnly: true,260maxResultCount: this.findWordOptions.maxResultCount,261}, token, this.documentCache));262263// If we found the class, we'll rely on the click-time resolution to find the method264if (classMatches.length) {265wordMatches = classMatches;266} else {267// If no class found, try just the method name as fallback268wordMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, lastPart, {269symbolMatchesOnly: true,270maxResultCount: this.findWordOptions.maxResultCount,271}, token));272}273} else if (symbolParts.length > 0) {274// For single names like `undo`, try to find the method directly275const lastPart = symbolParts[symbolParts.length - 1];276277if (lastPart && lastPart !== codeText) {278wordMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, lastPart, {279symbolMatchesOnly: true,280maxResultCount: this.findWordOptions.maxResultCount,281}, token, this.documentCache));282}283}284}285286return wordMatches.slice(0, this.findWordOptions.maxResultCount);287}288}289290291