From cef651242c94fe87ed811f5ad398ac3809c416c2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 4 Feb 2025 11:06:37 -0800 Subject: [PATCH] Add builtin #selection variable Acts as a helper to add the current selection as a reference --- .../chat/browser/actions/chatActions.ts | 2 +- .../browser/actions/chatContextActions.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../contrib/chat/browser/chatVariables.ts | 7 +-- .../browser/contrib/chatInputCompletions.ts | 61 ++++++++++++++++++- .../contrib/chat/common/chatVariables.ts | 2 +- 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d7532a3514b8e..b7177da3eb9b2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -139,7 +139,7 @@ class OpenChatGlobalAction extends Action2 { } } if (opts?.variableIds && opts.variableIds.length > 0) { - const actualVariables = chatVariablesService.getVariables(ChatAgentLocation.Panel); + const actualVariables = chatVariablesService.getVariables(); for (const actualVariable of actualVariables) { if (opts.variableIds.includes(actualVariable.id)) { chatWidget.attachmentModel.addContext({ diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index cdd4135f32b54..f091b1600a3cb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -677,7 +677,7 @@ export class AttachContextAction extends Action2 { const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; const quickPickItems: IAttachmentQuickPickItem[] = []; if (!context || !context.showFilesOnly) { - for (const variable of chatVariablesService.getVariables(widget.location)) { + for (const variable of chatVariablesService.getVariables()) { if (variable.fullName && (!variable.isSlow || slowSupported)) { quickPickItems.push({ kind: 'variable', diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 878728cea2930..824ba88a2f45c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -349,7 +349,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { } const variables = [ - ...chatVariablesService.getVariables(ChatAgentLocation.Panel), + ...chatVariablesService.getVariables(), { name: 'file', description: nls.localize('file', "Choose a file in the workspace") } ]; const variableText = variables diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 4ab91fa76d76b..7d92c1e31d99d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -123,12 +123,9 @@ export class ChatVariablesService implements IChatVariablesService { return this._resolver.get(name.toLowerCase())?.data; } - getVariables(location: ChatAgentLocation): Iterable> { + getVariables(): Iterable> { const all = Iterable.map(this._resolver.values(), data => data.data); - return Iterable.filter(all, data => { - // TODO@jrieken this is improper and should be know from the variable registeration data - return location !== ChatAgentLocation.Editor || !new Set(['selection', 'editor']).has(data.name); - }); + return all; } getDynamicVariables(sessionId: string): ReadonlyArray { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 717ed7e796a4c..646a74400042a 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -11,6 +11,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +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'; @@ -27,6 +28,7 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHistoryService } from '../../../../services/history/common/history.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; @@ -452,6 +454,7 @@ class BuiltinDynamicCompletions extends Disposable { @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IOutlineModelService private readonly outlineService: IOutlineModelService, + @IEditorService private readonly editorService: IEditorService, ) { super(); @@ -490,6 +493,62 @@ class BuiltinDynamicCompletions extends Disposable { } })); + // 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; + } + + 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 currentResource = active.getModel()?.uri; + const currentSelection = active.getSelection(); + if (!currentSelection || !currentResource || currentSelection.isEmpty()) { + return result; + } + + 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 + })] + } + }); + } + + return result; + } + })); + // Symbol completions this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatDynamicSymbolCompletions', @@ -797,7 +856,7 @@ class VariableCompletions extends Disposable { const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); const usedVariableNames = new Set(usedVariables.map(v => v.variableName)); - const variableItems = Array.from(this.chatVariablesService.getVariables(widget.location)) + const variableItems = Array.from(this.chatVariablesService.getVariables()) // This doesn't look at dynamic variables like `file`, where multiple makes sense. .filter(v => !usedVariableNames.has(v.name)) .filter(v => !v.isSlow || slowSupported) diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 1bd1db9c39641..170a2e133671d 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -43,7 +43,7 @@ export interface IChatVariablesService { registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; hasVariable(name: string): boolean; getVariable(name: string): IChatVariableData | undefined; - getVariables(location: ChatAgentLocation): Iterable>; + getVariables(): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void;