Skip to content

Commit

Permalink
Simplify variable completions registration
Browse files Browse the repository at this point in the history
  • Loading branch information
roblourens committed Feb 4, 2025
1 parent cef6512 commit a6c8016
Showing 1 changed file with 108 additions and 105 deletions.
213 changes: 108 additions & 105 deletions src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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<CompletionList>) {
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 };
Expand Down

0 comments on commit a6c8016

Please sign in to comment.