diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete-trigger.ts b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete-trigger.ts new file mode 100644 index 000000000000..53013d29f8fa --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete-trigger.ts @@ -0,0 +1,11 @@ +export function mentionTriggerIndex(text: string) { + const idx = text.lastIndexOf("@") + if (idx === -1) return + + const before = text.slice(0, idx) + const between = text.slice(idx) + if (between.match(/\s/)) return + if (before && !before.match(/\s$/)) return + + return Bun.stringWidth(before) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3242de94d671..1a3ea4e6abdd 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -20,6 +20,7 @@ import { useFrecency } from "./frecency" import { useBindings } from "../../keymap" import { Reference } from "@/reference/reference" import type { Config } from "@/config/config" +import { mentionTriggerIndex } from "./autocomplete-trigger" function removeLineRange(input: string) { const hashIndex = input.lastIndexOf("#") @@ -786,14 +787,9 @@ export function Autocomplete(props: { return } - // Check for "@" trigger - find the nearest "@" before cursor with no whitespace between - const text = value.slice(0, offset) - const idx = text.lastIndexOf("@") - if (idx === -1) return - - const between = text.slice(idx) - const before = idx === 0 ? undefined : value[idx - 1] - if ((before === undefined || /\s/.test(before)) && !between.match(/\s/)) { + // cursorOffset is in terminal cells, so use Textarea ranges instead of JS string indexes. + const idx = mentionTriggerIndex(props.input().getTextRange(0, offset)) + if (idx !== undefined) { show("@") setStore("index", idx) } diff --git a/packages/opencode/test/cli/cmd/tui/prompt-autocomplete.test.ts b/packages/opencode/test/cli/cmd/tui/prompt-autocomplete.test.ts new file mode 100644 index 000000000000..adb2fbe02872 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/prompt-autocomplete.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { mentionTriggerIndex } from "../../../../src/cli/cmd/tui/component/prompt/autocomplete-trigger" + +describe("mentionTriggerIndex", () => { + test("returns offset for mention triggers at the start or after ascii text", () => { + expect(mentionTriggerIndex("@")).toBe(0) + expect(mentionTriggerIndex("test @")).toBe(5) + }) + + test("returns display-width offset for CJK text before @", () => { + expect(mentionTriggerIndex("中文 @")).toBe(5) + expect(mentionTriggerIndex("こんにちは @")).toBe(11) + expect(mentionTriggerIndex("한국어 @")).toBe(7) + expect(mentionTriggerIndex("🙂 @")).toBe(3) + }) + + test("does not trigger inside ascii words or after whitespace in the query", () => { + expect(mentionTriggerIndex("中文@")).toBeUndefined() + expect(mentionTriggerIndex("こんにちは@")).toBeUndefined() + expect(mentionTriggerIndex("한국어@")).toBeUndefined() + expect(mentionTriggerIndex("🙂@")).toBeUndefined() + expect(mentionTriggerIndex("hello@")).toBeUndefined() + expect(mentionTriggerIndex("foo@bar.com")).toBeUndefined() + expect(mentionTriggerIndex("中文 @src file")).toBeUndefined() + }) +})