Improve web file completion continuation

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 23:33:40 +02:00
parent 9a80503b72
commit 5053d9db64
3 changed files with 48 additions and 5 deletions
+42 -4
View File
@@ -107,13 +107,25 @@ export class PromptEditor extends LitElement {
if (version !== this.requestVersion) return; if (version !== this.requestVersion) return;
this.completions = files this.completions = files
.slice(0, 12) .slice(0, 12)
.map((file) => ({ kind: "file", replaceFrom: trigger.from, replaceTo: trigger.to, insertText: `${trigger.fileMode === "path" ? "" : "@"}${file.path}`, detail: file.kind })); .map((file) => {
const insertText = fileInsertText(file.path, trigger.fileMode === "path", trigger.quoted === true);
return {
kind: "file",
replaceFrom: trigger.from,
replaceTo: trigger.to,
insertText,
detail: file.kind,
...(file.path.endsWith("/") && insertText.endsWith("\"") ? { cursorOffset: insertText.length - 1 } : {}),
};
});
} }
} }
private currentTrigger(): { kind: "command" | "file"; query: string; from: number; to: number; fileKind?: FileSuggestion["kind"]; fileMode?: "file" | "path" } | undefined { private currentTrigger(): { kind: "command" | "file"; query: string; from: number; to: number; fileKind?: FileSuggestion["kind"]; fileMode?: "file" | "path"; quoted?: boolean } | undefined {
const cursor = this.textarea?.selectionStart ?? this.draft.length; const cursor = this.textarea?.selectionStart ?? this.draft.length;
const beforeCursor = this.draft.slice(0, cursor); const beforeCursor = this.draft.slice(0, cursor);
const quotedTrigger = this.currentQuotedTrigger(beforeCursor, cursor);
if (quotedTrigger !== undefined) return quotedTrigger;
const tokenStart = Math.max(beforeCursor.lastIndexOf(" "), beforeCursor.lastIndexOf("\n")) + 1; const tokenStart = Math.max(beforeCursor.lastIndexOf(" "), beforeCursor.lastIndexOf("\n")) + 1;
const token = beforeCursor.slice(tokenStart); const token = beforeCursor.slice(tokenStart);
@@ -124,6 +136,15 @@ export class PromptEditor extends LitElement {
return undefined; return undefined;
} }
private currentQuotedTrigger(beforeCursor: string, cursor: number): { kind: "file"; query: string; from: number; to: number; fileMode?: "file" | "path"; quoted: true } | undefined {
const quoteStart = beforeCursor.lastIndexOf("\"");
if (quoteStart === -1) return undefined;
const prefix = beforeCursor.slice(0, quoteStart);
if (prefix.endsWith("@")) return { kind: "file", query: beforeCursor.slice(quoteStart + 1), from: prefix.length - 1, to: cursor, quoted: true };
if (prefix.endsWith("@ ")) return { kind: "file", query: beforeCursor.slice(quoteStart + 1), from: quoteStart, to: cursor, fileMode: "path", quoted: true };
return undefined;
}
private handleKeyDown(event: KeyboardEvent) { private handleKeyDown(event: KeyboardEvent) {
if (this.completions.length) { if (this.completions.length) {
if (event.key === "ArrowDown") { if (event.key === "ArrowDown") {
@@ -148,6 +169,14 @@ export class PromptEditor extends LitElement {
return; return;
} }
} }
if (event.key === "Tab") {
const trigger = this.currentTrigger();
if (trigger?.kind === "file") {
event.preventDefault();
void this.refreshCompletions();
return;
}
}
if (event.key === "Enter" && !event.shiftKey) { if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
this.send(this.canSteer || this.isCompacting ? "followUp" : undefined); this.send(this.canSteer || this.isCompacting ? "followUp" : undefined);
@@ -155,10 +184,13 @@ export class PromptEditor extends LitElement {
} }
private pick(item: CompletionItem) { private pick(item: CompletionItem) {
const suffix = item.kind === "file" && item.insertText.endsWith("/") ? "" : " "; const suffix = item.kind === "file" && (item.insertText.endsWith("/") || item.cursorOffset !== undefined) ? "" : " ";
this.draft = `${this.draft.slice(0, item.replaceFrom)}${item.insertText}${suffix}${this.draft.slice(item.replaceTo)}`; const cursor = item.replaceFrom + (item.cursorOffset ?? item.insertText.length) + suffix.length;
const after = item.insertText.endsWith("\"") && this.draft.slice(item.replaceTo).startsWith("\"") ? this.draft.slice(item.replaceTo + 1) : this.draft.slice(item.replaceTo);
this.draft = `${this.draft.slice(0, item.replaceFrom)}${item.insertText}${suffix}${after}`;
if (this.sessionId !== undefined && this.sessionId !== "") saveDraft(this.sessionId, this.draft); if (this.sessionId !== undefined && this.sessionId !== "") saveDraft(this.sessionId, this.draft);
this.completions = []; this.completions = [];
void this.updateComplete.then(() => this.textarea?.setSelectionRange(cursor, cursor));
} }
private send(streamingBehavior?: "steer" | "followUp") { private send(streamingBehavior?: "steer" | "followUp") {
@@ -173,6 +205,12 @@ export class PromptEditor extends LitElement {
static override styles = promptEditorStyles; static override styles = promptEditorStyles;
} }
function fileInsertText(path: string, pathMode: boolean, quoted: boolean): string {
const prefix = pathMode ? "" : "@";
if (!quoted && !path.includes(" ")) return `${prefix}${path}`;
return `${prefix}\"${path}\"`;
}
function emptySlashCommands(): SlashCommand[] { function emptySlashCommands(): SlashCommand[] {
return []; return [];
} }
+1
View File
@@ -21,6 +21,7 @@ export interface CompletionItem {
insertText: string; insertText: string;
detail: string; detail: string;
description?: string; description?: string;
cursorOffset?: number;
} }
export const appStyles = css` export const appStyles = css`
+5 -1
View File
@@ -25,5 +25,9 @@ function isFileCompletionContext(draft: string): boolean {
const token = currentToken(draft); const token = currentToken(draft);
if (token.startsWith("@")) return true; if (token.startsWith("@")) return true;
const tokenStart = draft.length - token.length; const tokenStart = draft.length - token.length;
return draft.slice(0, tokenStart).endsWith("@ "); if (draft.slice(0, tokenStart).endsWith("@ ")) return true;
const quoteStart = draft.lastIndexOf("\"");
if (quoteStart === -1) return false;
const prefix = draft.slice(0, quoteStart);
return prefix.endsWith("@") || prefix.endsWith("@ ");
} }