Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("#")
Expand Down Expand Up @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions packages/opencode/test/cli/cmd/tui/prompt-autocomplete.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
Loading