src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts

175 LOC · 162 covered · 13 uncovered · 38 ranges · 940 concepts · 20 introducers · 503 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentHostFileCompletionProvider.ts ×7
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { CancellationToken } from '../../../base/common/cancellation.js';
7 > import { isCancellationError } from '../../../base/common/errors.js';
8 > import { compareItemsByFuzzyScore, FuzzyScorerCache, IItemAccessor, prepareQuery, scoreItemFuzzy } from '../../../base/common/fuzzyScorer.js';
9 > import { Schemas } from '../../../base/common/network.js';
10 > import { basename, relativePath } from '../../../base/common/resources.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { CompletionItem, CompletionItemKind, CompletionsParams } from '../common/state/protocol/commands.js';
13 > import { MessageAttachmentKind } from '../common/state/protocol/state.js';
14 > import { CompletionTriggerCharacter, IAgentHostCompletionItemProvider } from './agentHostCompletions.js';
15 > import { AgentHostStateManager } from './agentHostStateManager.js';
16 > import { AgentHostWorkspaceFiles } from './agentHostWorkspaceFiles.js';
17 >
18 > /** Maximum number of completion items returned per call. */
19 > const MAX_RESULTS = 50;
20 >
21 > /**
22 > * Result of {@link extractAtToken}.
23 > */
24 > interface IAtToken {
25 > readonly token: string;
26 > readonly triggerChar: string;
27 > readonly rangeStart: number;
28 > readonly rangeEnd: number;
29 > }
30 >
31 > /**
32 > * Walk back from `offset` to find the most recent `@` that is preceded by
33 > * whitespace (or start-of-string) and not interrupted by whitespace. Returns
34 > * the substring after `@` together with the range to replace, or `undefined`
35 > * if no `@`-token is being typed at `offset`.
36 > *
37 > * Exported for unit testing.
38 > */
39 > export function extractAtToken(text: string, offset: number): IAtToken | undefined {
40 > if (offset < 0 || offset > text.length) { agentHostFileCompletionProvider.ts ×1
42 > }
43 > for (let i = offset - 1; i >= 0; i--) { agentHostFileCompletionProvider.ts ×3
44 > const ch = text.charCodeAt(i);
45 > // whitespace terminates the search
46 > if (ch === 0x20 /* space */ || ch === 0x09 /* tab */ || ch === 0x0a /* \n */ || ch === 0x0d /* \r */) {
48 > }
49 > if (text[i] === CompletionTriggerCharacter.File || text[i] === CompletionTriggerCharacter.Hash) { agentHostFileCompletionProvider.ts ×3
50 > // The trigger character must be at start-of-input or preceded by whitespace. agentHostFileCompletionProvider.ts ×1
51 > if (i > 0) {
52 > const prev = text.charCodeAt(i - 1); agentHostFileCompletionProvider.ts ×2
53 > const prevIsWs = prev === 0x20 || prev === 0x09 || prev === 0x0a || prev === 0x0d;
54 > if (!prevIsWs) {
56 > }
58 > return { token: text.slice(i + 1, offset), triggerChar: text[i], rangeStart: i, rangeEnd: offset }; agentHostFileCompletionProvider.ts ×1
59 > }
62 > }
64 > /**
65 > * Item-accessor that exposes a {@link URI} as basename / parent-directory /
66 > * relative path for the {@link scoreItemFuzzy} family.
67 > */
68 > class UriAccessor implements IItemAccessor<URI> {
69 > constructor(private readonly _workingDirectory: URI) { }
70 >
71 > getItemLabel(item: URI): string {
72 > return basename(item); agentHostFileCompletionProvider.ts ×5
73 > }
75 > getItemDescription(item: URI): string | undefined {
76 > const rel = relativePath(this._workingDirectory, item); agentHostFileCompletionProvider.ts ×5
77 > if (!rel) {
78 return undefined;
79 }
80 > const idx = rel.lastIndexOf('/'); agentHostFileCompletionProvider.ts ×5
81 > return idx > 0 ? rel.slice(0, idx) : undefined;
82 > }
84 > getItemPath(item: URI): string | undefined {
85 > const rel = relativePath(this._workingDirectory, item); agentHostFileCompletionProvider.ts ×5
86 > return rel ?? item.fsPath;
87 > }
89 >
90 > /**
91 > * Generic completion provider that contributes workspace file references
92 > * for a {@link CompletionItemKind.UserMessage} input — typically used for
93 > * `@`-mentions in the user message composer.
94 > *
95 > * When the user has typed an `@`-prefixed token at the cursor position,
96 > * this provider enumerates files under the session's working directory
97 > * (via {@link AgentHostWorkspaceFiles}, which uses ripgrep and respects
98 > * `.gitignore`), ranks them with the same fuzzy scorer used by the
99 > * VS Code Quick Open file picker, and returns up to {@link MAX_RESULTS}
100 > * matches.
101 > */
102 > export class AgentHostFileCompletionProvider implements IAgentHostCompletionItemProvider {
103 >
104 > readonly kinds: ReadonlySet<CompletionItemKind> = new Set([CompletionItemKind.UserMessage]);
105 >
106 > readonly triggerCharacters: readonly string[] = [CompletionTriggerCharacter.File, CompletionTriggerCharacter.Hash];
107 >
108 > constructor(
109 > private readonly _stateManager: AgentHostStateManager, agentHostFileCompletionProvider.ts ×1
110 > private readonly _workspaceFiles: AgentHostWorkspaceFiles,
111 > ) { }
113 > async provideCompletionItems(params: CompletionsParams, token: CancellationToken): Promise<readonly CompletionItem[]> {
114 > const workingDirectoryStr = this._stateManager.getSessionState(params.channel)?.workingDirectories?.[0]; agentHostFileCompletionProvider.ts ×3
115 > if (!workingDirectoryStr) {
117 > }
118 > const workingDirectory = URI.parse(workingDirectoryStr); agentHostFileCompletionProvider.ts ×1
119 > if (workingDirectory.scheme !== Schemas.file) {
121 > }
123 > const at = extractAtToken(params.text, params.offset);
124 > if (!at) {
126 > }
128 > let files: readonly URI[];
129 > try {
130 > files = await this._workspaceFiles.getFiles(workingDirectory, token);
131 > } catch (err) {
132 // Cancellation is expected on every keystroke as Monaco cancels
133 // the previous request. Don't let it surface as a provider failure
134 // in {@link AgentHostCompletions} — it would log noisy errors on
135 // normal typing.
136 if (isCancellationError(err)) {
137 return [];
138 }
139 throw err;
140 }
141 > if (token.isCancellationRequested || files.length === 0) { agentHostFileCompletionProvider.ts ×3
142 return [];
143 }
145 > const accessor = new UriAccessor(workingDirectory);
146 > const query = prepareQuery(at.token);
147 > const cache: FuzzyScorerCache = Object.create(null);
148 >
149 > let candidates: URI[];
150 > if (!query.normalized) {
151 > // Empty token: return the first MAX_RESULTS files in enumeration order. agentHostFileCompletionProvider.ts ×1
152 > candidates = files.slice(0, MAX_RESULTS);
154 > // Filter out non-matches first to avoid sorting tens of thousands of zeros. agentHostFileCompletionProvider.ts ×5
155 > const matching = files.filter(f => scoreItemFuzzy(f, query, true, accessor, cache).score > 0);
156 > matching.sort((a, b) => compareItemsByFuzzyScore(a, b, query, true, accessor, cache));
157 > candidates = matching.slice(0, MAX_RESULTS);
158 > }
160 > return candidates.map((uri): CompletionItem => {
161 > const name = basename(uri);
162 > return {
163 > insertText: at.triggerChar + name,
164 > rangeStart: at.rangeStart,
165 > rangeEnd: at.rangeEnd,
166 > attachment: {
167 > type: MessageAttachmentKind.Resource,
168 > uri: uri.toString(),
169 > label: name,
170 > displayKind: 'document',
171 > },
172 > };
173 > });