Skip to content

Commit

Permalink
Fix: Prevent autocompletion from triggering incorrectly within words
Browse files Browse the repository at this point in the history
This commit fixes an issue where autocompletion would incorrectly trigger when typing a variable character '#' in the middle of words (e.g., "Hello#").

Changes:
- Removed '#' from trigger characters in the variable argument completion provider
- Improved boundary detection in the variable argument picker to prevent triggering when '#' appears within words
- Added better context validation to ensure completion only happens in appropriate cases
- Fixed the implementation to maintain correct functionality for legitimate variable references like "#file:"

The fix ensures that autocompletion is only triggered in intended contexts while preserving all existing functionality.
Fixed eclipse-theia#15028
  • Loading branch information
eneufeld committed Feb 25, 2025
1 parent 19bde8c commit 8770084
Showing 1 changed file with 110 additions and 72 deletions.
182 changes: 110 additions & 72 deletions packages/ai-chat-ui/src/browser/chat-view-language-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ const VARIABLE_RESOLUTION_CONTEXT = { context: 'chat-input-autocomplete' };
const VARIABLE_ARGUMENT_PICKER_COMMAND = 'trigger-variable-argument-picker';
const VARIABLE_ADD_CONTEXT_COMMAND = 'add-context-variable';

// Define a consistent interface for completion sources
interface CompletionSource<T> {
triggerCharacter: string;
getItems: () => T[];
kind: monaco.languages.CompletionItemKind;
getId: (item: T) => string;
getName: (item: T) => string;
getDescription: (item: T) => string;
command?: monaco.languages.Command;
}

@injectable()
export class ChatViewLanguageContribution implements FrontendApplicationContribution {

Expand All @@ -49,34 +60,77 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
onStart(_app: FrontendApplication): MaybePromise<void> {
monaco.languages.register({ id: CHAT_VIEW_LANGUAGE_ID, extensions: [CHAT_VIEW_LANGUAGE_EXTENSION] });

monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
triggerCharacters: [PromptText.AGENT_CHAR],
provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideAgentCompletions(model, position),
// Register all completion providers
this.registerCompletionProviders();

// Register editor commands
monaco.editor.registerCommand(VARIABLE_ARGUMENT_PICKER_COMMAND, this.triggerVariableArgumentPicker.bind(this));
monaco.editor.registerCommand(VARIABLE_ADD_CONTEXT_COMMAND, (_, ...args) => args.length > 1 ? this.addContextVariable(args[0], args[1]) : undefined);
}

protected registerCompletionProviders(): void {
// Register standard completion providers
this.registerStandardCompletionProvider({
triggerCharacter: PromptText.AGENT_CHAR,
getItems: () => this.agentService.getAgents(), // Use function to fetch the latest items
kind: monaco.languages.CompletionItemKind.Value,
getId: agent => agent.id,
getName: agent => agent.name,
getDescription: agent => agent.description
});
monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
triggerCharacters: [PromptText.VARIABLE_CHAR],
provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideVariableCompletions(model, position),

this.registerStandardCompletionProvider({
triggerCharacter: PromptText.VARIABLE_CHAR,
getItems: () => this.variableService.getVariables(), // Use function to fetch the latest items
kind: monaco.languages.CompletionItemKind.Variable,
getId: variable => variable.args?.some(arg => !arg.isOptional) ? variable.name + PromptText.VARIABLE_SEPARATOR_CHAR : variable.name,
getName: variable => variable.name,
getDescription: variable => variable.description,
command: {
title: nls.localize('theia/ai/chat-ui/selectVariableArguments', 'Select variable arguments'),
id: VARIABLE_ARGUMENT_PICKER_COMMAND,
}
});
monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
triggerCharacters: [PromptText.VARIABLE_CHAR, PromptText.VARIABLE_SEPARATOR_CHAR],
provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideVariableWithArgCompletions(model, position),

this.registerStandardCompletionProvider({
triggerCharacter: PromptText.FUNCTION_CHAR,
getItems: () => this.toolInvocationRegistry.getAllFunctions(), // Use function to fetch the latest items
kind: monaco.languages.CompletionItemKind.Function,
getId: tool => tool.id,
getName: tool => tool.name,
getDescription: tool => tool.description ?? ''
});

// Register the variable argument completion provider (special case)
monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
triggerCharacters: [PromptText.FUNCTION_CHAR],
provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideToolCompletions(model, position),
triggerCharacters: [PromptText.VARIABLE_SEPARATOR_CHAR],
provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> =>
this.provideVariableWithArgCompletions(model, position),
});
}

monaco.editor.registerCommand(VARIABLE_ARGUMENT_PICKER_COMMAND, this.triggerVariableArgumentPicker.bind(this));
monaco.editor.registerCommand(VARIABLE_ADD_CONTEXT_COMMAND, (_, ...args) => args.length > 1 ? this.addContextVariable(args[0], args[1]) : undefined);
protected registerStandardCompletionProvider<T>(source: CompletionSource<T>): void {
monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
triggerCharacters: [source.triggerCharacter],
provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> =>
this.provideCompletions(model, position, source),
});
}

getCompletionRange(model: monaco.editor.ITextModel, position: monaco.Position, triggerCharacter: string): monaco.Range | undefined {
const wordInfo = model.getWordUntilPosition(position);
const lineContent = model.getLineContent(position.lineNumber);
// one to the left, and -1 for 0-based index
const characterBeforeCurrentWord = lineContent[wordInfo.startColumn - 1 - 1];
// return suggestions only if the word is directly preceded by the trigger character
if (characterBeforeCurrentWord !== triggerCharacter) {

// Check if we're in the middle of a word, not at the beginning
if (wordInfo.startColumn > 1 && characterBeforeCurrentWord !== triggerCharacter) {
return undefined;
}

// Check if the trigger character is actually part of another word
const wordAtTriggerPosition = model.getWordAtPosition({ lineNumber: position.lineNumber, column: wordInfo.startColumn - 1 });
if (wordAtTriggerPosition && wordAtTriggerPosition.word.length > 1) {
return undefined;
}

Expand All @@ -88,65 +142,35 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
);
}

private getSuggestions<T>(
protected provideCompletions<T>(
model: monaco.editor.ITextModel,
position: monaco.Position,
triggerChar: string,
items: T[],
kind: monaco.languages.CompletionItemKind,
getId: (item: T) => string,
getName: (item: T) => string,
getDescription: (item: T) => string,
command?: monaco.languages.Command
source: CompletionSource<T>
): ProviderResult<monaco.languages.CompletionList> {
const completionRange = this.getCompletionRange(model, position, triggerChar);
const completionRange = this.getCompletionRange(model, position, source.triggerCharacter);
if (completionRange === undefined) {
return { suggestions: [] };
}

// Get the latest items at the time of completion request
const items = source.getItems();
const suggestions = items.map(item => ({
insertText: getId(item),
kind: kind,
label: getName(item),
insertText: source.getId(item),
kind: source.kind,
label: source.getName(item),
range: completionRange,
detail: getDescription(item),
command
detail: source.getDescription(item),
command: source.command
}));
return { suggestions };
}

provideAgentCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult<monaco.languages.CompletionList> {
return this.getSuggestions(
model,
position,
PromptText.AGENT_CHAR,
this.agentService.getAgents(),
monaco.languages.CompletionItemKind.Value,
agent => agent.id,
agent => agent.name,
agent => agent.description
);
}

provideVariableCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult<monaco.languages.CompletionList> {
return this.getSuggestions(
model,
position,
PromptText.VARIABLE_CHAR,
this.variableService.getVariables(),
monaco.languages.CompletionItemKind.Variable,
variable => variable.args?.some(arg => !arg.isOptional) ? variable.name + PromptText.VARIABLE_SEPARATOR_CHAR : variable.name,
variable => variable.name,
variable => variable.description,
{
title: nls.localize('theia/ai/chat-ui/selectVariableArguments', 'Select variable arguments'),
id: VARIABLE_ARGUMENT_PICKER_COMMAND,
}
);
return { suggestions };
}

async provideVariableWithArgCompletions(model: monaco.editor.ITextModel, position: monaco.Position): Promise<monaco.languages.CompletionList> {
// Get the latest variables at the time of completion request
const variables = this.variableService.getVariables();
const suggestions: monaco.languages.CompletionItem[] = [];

for (const variable of variables) {
const provider = await this.variableService.getArgumentCompletionProvider(variable.name);
if (provider) {
Expand All @@ -164,33 +188,42 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
}
}
}
return { suggestions };
}

provideToolCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult<monaco.languages.CompletionList> {
return this.getSuggestions(
model,
position,
PromptText.FUNCTION_CHAR,
this.toolInvocationRegistry.getAllFunctions(),
monaco.languages.CompletionItemKind.Function,
tool => tool.id,
tool => tool.name,
tool => tool.description ?? ''
);
return { suggestions };
}

protected async triggerVariableArgumentPicker(): Promise<void> {
const inputEditor = monaco.editor.getEditors().find(editor => editor.hasTextFocus());
if (!inputEditor) {
return;
}

const model = inputEditor.getModel();
const position = inputEditor.getPosition();
if (!model || !position) {
return;
}

// Get the current line's content and the word at cursor
const lineContent = model.getLineContent(position.lineNumber);
const wordInfo = model.getWordUntilPosition(position);

// Only trigger if current word starts with #, followed by actual text
if (!wordInfo.word.startsWith(PromptText.VARIABLE_CHAR) || wordInfo.word.length <= 1) {
return;
}

// Ensure the # isn't part of another word (like "Hello#")
// by checking if there's a word character before the # character
if (wordInfo.startColumn > 1) {
// Convert to 0-based index and account for the # at start of word
const indexBeforeHash = wordInfo.startColumn - 2;
if (indexBeforeHash >= 0 && /\w/.test(lineContent[indexBeforeHash])) {
// We're in a case like "Hello#variable" - don't trigger
return;
}
}

// account for the variable separator character if present
let endOfWordPosition = position.column;
let insertTextPrefix = PromptText.VARIABLE_SEPARATOR_CHAR;
Expand All @@ -203,18 +236,22 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
if (!variableName) {
return;
}

const provider = await this.variableService.getArgumentPicker(variableName, VARIABLE_RESOLUTION_CONTEXT);
if (!provider) {
return;
}

const arg = await provider(VARIABLE_RESOLUTION_CONTEXT);
if (!arg) {
return;
}

inputEditor.executeEdits('variable-argument-picker', [{
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
text: insertTextPrefix + arg
}]);

await this.addContextVariable(variableName, arg);
}

Expand All @@ -228,6 +265,7 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
if (!variable || !AIContextVariable.is(variable)) {
return;
}

const widget = this.shell.getWidgetById(ChatViewWidget.ID);
if (widget instanceof ChatViewWidget) {
widget.addContext({ variable, arg });
Expand Down

0 comments on commit 8770084

Please sign in to comment.