diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 646a74400042a..9ab466c60a441 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -15,7 +15,7 @@ import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DocumentSymbol, Location, SymbolKind, SymbolKinds } from '../../../../../editor/common/languages.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DocumentSymbol, Location, ProviderResult, SymbolKind, SymbolKinds } from '../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; @@ -438,6 +438,14 @@ class ReferenceArgument { ) { } } +interface IVariableCompletionsDetails { + model: ITextModel; + position: Position; + context: CompletionContext; + widget: IChatWidget; + range: IChatCompletionRangeResult; +} + class BuiltinDynamicCompletions extends Disposable { private static readonly addReferenceCommand = '_addReferenceCmd'; private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag @@ -459,134 +467,129 @@ class BuiltinDynamicCompletions extends Disposable { super(); // File completions - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicFileCompletions', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences) { - return null; - } - - const result: CompletionList = { suggestions: [] }; - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); - - if (range) { - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - documentation: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - }); - } + this.registerVariableCompletions('file', async ({ widget, range, position, model }, token) => { + if (!widget.supportsFileReferences) { + return null; + } - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - await this.addFileEntries(widget, result, range2, token); - } + const result: CompletionList = { suggestions: [] }; + + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); + result.suggestions.push({ + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + documentation: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + }); - return result; + const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); + if (range2) { + await this.addFileEntries(widget, result, range2, token); } - })); + + return result; + }); // Selection completion - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicSelectionCompletions', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences) { - return null; - } + this.registerVariableCompletions('selection', ({ widget, range }, token) => { + if (!widget.supportsFileReferences) { + return; + } - if (widget.location === ChatAgentLocation.Editor) { - return; - } + if (widget.location === ChatAgentLocation.Editor) { + return; + } - const result: CompletionList = { suggestions: [] }; - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); - if (range) { - const active = this.editorService.activeTextEditorControl; - if (!isCodeEditor(active)) { - return result; - } + const active = this.editorService.activeTextEditorControl; + if (!isCodeEditor(active)) { + return; + } - const currentResource = active.getModel()?.uri; - const currentSelection = active.getSelection(); - if (!currentSelection || !currentResource || currentSelection.isEmpty()) { - return result; - } + const currentResource = active.getModel()?.uri; + const currentSelection = active.getSelection(); + if (!currentSelection || !currentResource || currentSelection.isEmpty()) { + return; + } - const basename = this.labelService.getUriBasenameLabel(currentResource); - const text = `${chatVariableLeader}file:${basename}:${currentSelection.startLineNumber}-${currentSelection.endLineNumber}`; - const fullRangeText = `:${currentSelection.startLineNumber}:${currentSelection.startColumn}-${currentSelection.endLineNumber}:${currentSelection.endColumn}`; - const description = this.labelService.getUriLabel(currentResource, { relative: true }) + fullRangeText; - - result.suggestions.push({ - label: { label: `${chatVariableLeader}selection`, description }, - filterText: `${chatVariableLeader}selection`, - insertText: range.varWord?.endColumn === range.replace.endColumn ? `${text} ` : text, - range, - kind: CompletionItemKind.Text, - sortText: 'z', - command: { - id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { - id: 'vscode.file', - prefix: 'file', - isFile: true, - range: { startLineNumber: range.replace.startLineNumber, startColumn: range.replace.startColumn, endLineNumber: range.replace.endLineNumber, endColumn: range.replace.startColumn + text.length }, - data: { range: currentSelection, uri: currentResource } satisfies Location - })] - } - }); + const basename = this.labelService.getUriBasenameLabel(currentResource); + const text = `${chatVariableLeader}file:${basename}:${currentSelection.startLineNumber}-${currentSelection.endLineNumber}`; + const fullRangeText = `:${currentSelection.startLineNumber}:${currentSelection.startColumn}-${currentSelection.endLineNumber}:${currentSelection.endColumn}`; + const description = this.labelService.getUriLabel(currentResource, { relative: true }) + fullRangeText; + + const result: CompletionList = { suggestions: [] }; + result.suggestions.push({ + label: { label: `${chatVariableLeader}selection`, description }, + filterText: `${chatVariableLeader}selection`, + insertText: range.varWord?.endColumn === range.replace.endColumn ? `${text} ` : text, + range, + kind: CompletionItemKind.Text, + sortText: 'z', + command: { + id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { + id: 'vscode.file', + prefix: 'file', + isFile: true, + range: { startLineNumber: range.replace.startLineNumber, startColumn: range.replace.startColumn, endLineNumber: range.replace.endLineNumber, endColumn: range.replace.startColumn + text.length }, + data: { range: currentSelection, uri: currentResource } satisfies Location + })] } + }); + return result; + }); - return result; + // Symbol completions + this.registerVariableCompletions('symbol', ({ widget, range, position, model }, token) => { + if (!widget.supportsFileReferences) { + return null; } - })); - // Symbol completions + const result: CompletionList = { suggestions: [] }; + + const afterRangeSym = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#sym:'.length); + result.suggestions.push({ + label: `${chatVariableLeader}sym`, + insertText: `${chatVariableLeader}sym:`, + documentation: localize('pickSymbolLabel', "Pick a symbol"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertSymAction.ID, title: SelectAndInsertSymAction.ID, arguments: [{ widget, range: afterRangeSym }] }, + sortText: 'z' + }); + + const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); + if (range2) { + this.addSymbolEntries(widget, result, range2, token); + } + + return result; + }); + + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); + + this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); + } + + private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult) { this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicSymbolCompletions', + _debugDisplayName: `chatVarCompletions-${debugName}`, triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + provideCompletionItems: async (model: ITextModel, position: Position, context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences) { - return null; + if (!widget) { + return; } - const result: CompletionList = { suggestions: [] }; const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); - if (range) { - const afterRangeSym = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#sym:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}sym`, - insertText: `${chatVariableLeader}sym:`, - documentation: localize('pickSymbolLabel', "Pick a symbol"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertSymAction.ID, title: SelectAndInsertSymAction.ID, arguments: [{ widget, range: afterRangeSym }] }, - sortText: 'z' - }); - } - - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - this.addSymbolEntries(widget, result, range2, token); + return provider({ model, position, widget, range, context }, token); } - return result; + return; } })); - - this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); - - this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); } private cacheKey?: { key: string; time: number };