Archived
fix: improve file mention suggestions without ripgrep
This commit is contained in:
@@ -133,8 +133,20 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
export interface FileSuggestionQueryOptions {
|
||||
kind?: FileSuggestion["kind"] | undefined;
|
||||
mode?: "file" | "path" | undefined;
|
||||
scope?: "tracked" | "all" | undefined;
|
||||
}
|
||||
|
||||
export const filesApi = {
|
||||
files: (cwd: string, query: string, kind?: FileSuggestion["kind"], mode?: "file" | "path") => request(`/api/files?cwd=${encodeURIComponent(cwd)}&q=${encodeURIComponent(query)}${kind !== undefined ? `&kind=${encodeURIComponent(kind)}` : ""}${mode !== undefined ? `&mode=${encodeURIComponent(mode)}` : ""}`, arrayOf(parseFileSuggestion)),
|
||||
files: (cwd: string, query: string, options: FileSuggestionQueryOptions = {}) => {
|
||||
const params = new URLSearchParams({ cwd, q: query });
|
||||
if (options.kind !== undefined) params.set("kind", options.kind);
|
||||
if (options.mode !== undefined) params.set("mode", options.mode);
|
||||
if (options.scope !== undefined) params.set("scope", options.scope);
|
||||
return request(`/api/files?${params.toString()}`, arrayOf(parseFileSuggestion));
|
||||
},
|
||||
};
|
||||
|
||||
export const gitApi = {
|
||||
|
||||
@@ -110,7 +110,7 @@ export class PromptEditor extends LitElement {
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of((view) => inputAssistanceContentAttributes(view.state.sliceDoc(0, view.state.selection.main.head))),
|
||||
placeholder("Message pi... Use / for commands, @ for files"),
|
||||
placeholder("Message pi... Use / for commands, @ for tracked files, @ space for all files"),
|
||||
this.editableCompartment.of(EditorView.editable.of(!this.disabled)),
|
||||
this.readOnlyCompartment.of(EditorState.readOnly.of(this.disabled)),
|
||||
EditorView.updateListener.of((update) => {
|
||||
@@ -182,12 +182,12 @@ export class PromptEditor extends LitElement {
|
||||
...(command.description === undefined ? {} : { description: command.description }),
|
||||
}));
|
||||
} else if (trigger.kind === "file" && this.cwd !== undefined && this.cwd !== "") {
|
||||
const files = await api.files(this.cwd, trigger.query, trigger.fileKind, trigger.fileMode).catch(emptyFileSuggestions);
|
||||
const files = await api.files(this.cwd, trigger.query, { scope: trigger.fileScope }).catch(emptyFileSuggestions);
|
||||
if (version !== this.requestVersion) return;
|
||||
this.completions = files
|
||||
.slice(0, 12)
|
||||
.map((file) => {
|
||||
const insertText = fileInsertText(file.path, trigger.fileMode === "path", trigger.quoted === true);
|
||||
const insertText = fileInsertText(file.path, trigger.quoted === true, file.path.endsWith("/") ? trigger.allPrefix : undefined);
|
||||
return {
|
||||
kind: "file",
|
||||
replaceFrom: trigger.from,
|
||||
@@ -200,7 +200,7 @@ export class PromptEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private currentTrigger(): { kind: "command" | "file"; query: string; from: number; to: number; fileKind?: FileSuggestion["kind"]; fileMode?: "file" | "path"; quoted?: boolean } | undefined {
|
||||
private currentTrigger(): { kind: "command" | "file"; query: string; from: number; to: number; fileScope?: "tracked" | "all" | undefined; allPrefix?: "@ " | "!@" | undefined; quoted?: boolean } | undefined {
|
||||
const cursor = this.editor?.state.selection.main.head ?? this.draft.length;
|
||||
const beforeCursor = this.draft.slice(0, cursor);
|
||||
const quotedTrigger = this.currentQuotedTrigger(beforeCursor, cursor);
|
||||
@@ -209,18 +209,20 @@ export class PromptEditor extends LitElement {
|
||||
const tokenStart = Math.max(beforeCursor.lastIndexOf(" "), beforeCursor.lastIndexOf("\n")) + 1;
|
||||
const token = beforeCursor.slice(tokenStart);
|
||||
const beforeToken = beforeCursor.slice(0, tokenStart);
|
||||
if (beforeToken.endsWith("@ ")) return { kind: "file", query: token, from: tokenStart, to: cursor, fileMode: "path" };
|
||||
if (beforeToken.endsWith("@ ")) return { kind: "file", query: token, from: tokenStart - 2, to: cursor, fileScope: "all", allPrefix: "@ " };
|
||||
if (token.startsWith("/") && tokenStart === 0) return { kind: "command", query: token.slice(1), from: tokenStart, to: cursor };
|
||||
if (token.startsWith("@")) return { kind: "file", query: token.slice(1), from: tokenStart, to: cursor };
|
||||
if (token.startsWith("!@")) return { kind: "file", query: token.slice(2), from: tokenStart, to: cursor, fileScope: "all", allPrefix: "!@" };
|
||||
if (token.startsWith("@")) return { kind: "file", query: token.slice(1), from: tokenStart, to: cursor, fileScope: "tracked" };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private currentQuotedTrigger(beforeCursor: string, cursor: number): { kind: "file"; query: string; from: number; to: number; fileMode?: "file" | "path"; quoted: true } | undefined {
|
||||
private currentQuotedTrigger(beforeCursor: string, cursor: number): { kind: "file"; query: string; from: number; to: number; fileScope?: "tracked" | "all" | undefined; allPrefix?: "@ " | "!@" | undefined; 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 };
|
||||
if (prefix.endsWith("!@")) return { kind: "file", query: beforeCursor.slice(quoteStart + 1), from: prefix.length - 2, to: cursor, fileScope: "all", allPrefix: "!@", quoted: true };
|
||||
if (prefix.endsWith("@")) return { kind: "file", query: beforeCursor.slice(quoteStart + 1), from: prefix.length - 1, to: cursor, fileScope: "tracked", quoted: true };
|
||||
if (prefix.endsWith("@ ")) return { kind: "file", query: beforeCursor.slice(quoteStart + 1), from: prefix.length - 2, to: cursor, fileScope: "all", allPrefix: "@ ", quoted: true };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -286,8 +288,8 @@ export class PromptEditor extends LitElement {
|
||||
static override styles = promptEditorStyles;
|
||||
}
|
||||
|
||||
function fileInsertText(path: string, pathMode: boolean, quoted: boolean): string {
|
||||
const prefix = pathMode ? "" : "@";
|
||||
function fileInsertText(path: string, quoted: boolean, allPrefix?: "@ " | "!@"): string {
|
||||
const prefix = allPrefix ?? "@";
|
||||
if (!quoted && !path.includes(" ")) return `${prefix}${path}`;
|
||||
return `${prefix}"${path}"`;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ describe("inputModeForDraft", () => {
|
||||
it("detects file completion contexts", () => {
|
||||
expect(inputModeForDraft("open @src/main.ts")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open @ ")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open !@vendor/file.ts")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("!@vendor/file.ts")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open @ \"src/main.ts")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open !@\"vendor/file.ts")).toEqual({ kind: "file" });
|
||||
expect(inputModeForDraft("open \"src/main.ts")).toEqual({ kind: "normal" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ export type InputMode =
|
||||
|
||||
export function inputModeForDraft(draft: string): InputMode {
|
||||
const trimmed = draft.trimStart();
|
||||
if (trimmed.startsWith("!@")) return { kind: "file" };
|
||||
if (trimmed.startsWith("!")) return { kind: "shell", excludeFromContext: trimmed.startsWith("!!") };
|
||||
if (currentToken(draft).startsWith("/")) return { kind: "command" };
|
||||
if (isFileCompletionContext(draft)) return { kind: "file" };
|
||||
@@ -23,11 +24,11 @@ function currentToken(draft: string): string {
|
||||
|
||||
function isFileCompletionContext(draft: string): boolean {
|
||||
const token = currentToken(draft);
|
||||
if (token.startsWith("@")) return true;
|
||||
if (token.startsWith("@") || token.startsWith("!@")) return true;
|
||||
const tokenStart = draft.length - token.length;
|
||||
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("@ ");
|
||||
return prefix.endsWith("@") || prefix.endsWith("@ ") || prefix.endsWith("!@");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user