summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/line-comment.tsx
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-04-01 16:11:57 +0530
committerGitHub <[email protected]>2026-04-01 16:11:57 +0530
commita3a6cf1c075c40c87980dda181d586a1d06ea304 (patch)
tree71b3c3d7144f7c38eb73d7e173852858313fc4bf /packages/ui/src/components/line-comment.tsx
parent47a676111a3532aebed01110494742e536b7e5b4 (diff)
downloadopencode-a3a6cf1c075c40c87980dda181d586a1d06ea304.tar.gz
opencode-a3a6cf1c075c40c87980dda181d586a1d06ea304.zip
feat(comments): support file mentions (#20447)
Diffstat (limited to 'packages/ui/src/components/line-comment.tsx')
-rw-r--r--packages/ui/src/components/line-comment.tsx137
1 files changed, 136 insertions, 1 deletions
diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx
index bc47ad940..f0e29a485 100644
--- a/packages/ui/src/components/line-comment.tsx
+++ b/packages/ui/src/components/line-comment.tsx
@@ -1,5 +1,8 @@
-import { createEffect, createSignal, onMount, Show, splitProps, type JSX } from "solid-js"
+import { useFilteredList } from "@opencode-ai/ui/hooks"
+import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { createEffect, createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js"
import { Button } from "./button"
+import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
import { installLineCommentStyles } from "./line-comment-styles"
import { useI18n } from "../context/i18n"
@@ -183,6 +186,9 @@ export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "
autofocus?: boolean
cancelLabel?: string
submitLabel?: string
+ mention?: {
+ items: (query: string) => string[] | Promise<string[]>
+ }
}
export const LineCommentEditor = (props: LineCommentEditorProps) => {
@@ -198,12 +204,46 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
"autofocus",
"cancelLabel",
"submitLabel",
+ "mention",
])
const refs = {
textarea: undefined as HTMLTextAreaElement | undefined,
}
const [text, setText] = createSignal(split.value)
+ const [open, setOpen] = createSignal(false)
+
+ function selectMention(item: { path: string } | undefined) {
+ if (!item) return
+
+ const textarea = refs.textarea
+ const query = currentMention()
+ if (!textarea || !query) return
+
+ const value = `${text().slice(0, query.start)}@${item.path} ${text().slice(query.end)}`
+ const cursor = query.start + item.path.length + 2
+
+ setText(value)
+ split.onInput(value)
+ closeMention()
+
+ requestAnimationFrame(() => {
+ textarea.focus()
+ textarea.setSelectionRange(cursor, cursor)
+ })
+ }
+
+ const mention = useFilteredList<{ path: string }>({
+ items: async (query) => {
+ if (!split.mention) return []
+ if (!query.trim()) return []
+ const paths = await split.mention.items(query)
+ return paths.map((path) => ({ path }))
+ },
+ key: (item) => item.path,
+ filterKeys: ["path"],
+ onSelect: selectMention,
+ })
const focus = () => refs.textarea?.focus()
const hold: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
@@ -221,6 +261,46 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
setText(split.value)
})
+ const closeMention = () => {
+ setOpen(false)
+ mention.clear()
+ }
+
+ const currentMention = () => {
+ const textarea = refs.textarea
+ if (!textarea) return
+ if (!split.mention) return
+ if (textarea.selectionStart !== textarea.selectionEnd) return
+
+ const end = textarea.selectionStart
+ const match = textarea.value.slice(0, end).match(/@(\S*)$/)
+ if (!match) return
+
+ return {
+ query: match[1] ?? "",
+ start: end - match[0].length,
+ end,
+ }
+ }
+
+ const syncMention = () => {
+ const item = currentMention()
+ if (!item) {
+ closeMention()
+ return
+ }
+
+ setOpen(true)
+ mention.onInput(item.query)
+ }
+
+ const selectActiveMention = () => {
+ const items = mention.flat()
+ if (items.length === 0) return
+ const active = mention.active()
+ selectMention(items.find((item) => item.path === active) ?? items[0])
+ }
+
const submit = () => {
const value = text().trim()
if (!value) return
@@ -247,11 +327,38 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
const value = (e.currentTarget as HTMLTextAreaElement).value
setText(value)
split.onInput(value)
+ syncMention()
}}
+ on:click={() => syncMention()}
+ on:select={() => syncMention()}
on:keydown={(e) => {
const event = e as KeyboardEvent
if (event.isComposing || event.keyCode === 229) return
event.stopPropagation()
+ if (open()) {
+ if (e.key === "Escape") {
+ event.preventDefault()
+ closeMention()
+ return
+ }
+
+ if (e.key === "Tab") {
+ if (mention.flat().length === 0) return
+ event.preventDefault()
+ selectActiveMention()
+ return
+ }
+
+ const nav = e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter"
+ const ctrlNav =
+ event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey && (e.key === "n" || e.key === "p")
+ if ((nav || ctrlNav) && mention.flat().length > 0) {
+ mention.onKeyDown(event)
+ event.preventDefault()
+ return
+ }
+ }
+
if (e.key === "Escape") {
event.preventDefault()
e.currentTarget.blur()
@@ -264,6 +371,34 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
submit()
}}
/>
+ <Show when={open() && mention.flat().length > 0}>
+ <div data-slot="line-comment-mention-list">
+ <For each={mention.flat().slice(0, 10)}>
+ {(item) => {
+ const directory = item.path.endsWith("/") ? item.path : getDirectory(item.path)
+ const name = item.path.endsWith("/") ? "" : getFilename(item.path)
+ return (
+ <button
+ type="button"
+ data-slot="line-comment-mention-item"
+ data-active={mention.active() === item.path ? "" : undefined}
+ onMouseDown={(event) => event.preventDefault()}
+ onMouseEnter={() => mention.setActive(item.path)}
+ onClick={() => selectMention(item)}
+ >
+ <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
+ <div data-slot="line-comment-mention-path">
+ <span data-slot="line-comment-mention-dir">{directory}</span>
+ <Show when={name}>
+ <span data-slot="line-comment-mention-file">{name}</span>
+ </Show>
+ </div>
+ </button>
+ )
+ }}
+ </For>
+ </div>
+ </Show>
<div data-slot="line-comment-actions">
<div data-slot="line-comment-editor-label">
{i18n.t("ui.lineComment.editorLabel.prefix")}