import { useFilteredList } from "@opencode-ai/ui/hooks" import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { 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" installLineCommentStyles() export type LineCommentVariant = "default" | "editor" | "add" function InlineGlyph(props: { icon: "comment" | "plus" }) { return ( } > ) } export type LineCommentAnchorProps = { id?: string top?: number inline?: boolean hideButton?: boolean open: boolean variant?: LineCommentVariant icon?: "comment" | "plus" buttonLabel?: string onClick?: JSX.EventHandlerUnion onMouseEnter?: JSX.EventHandlerUnion onPopoverFocusOut?: JSX.EventHandlerUnion class?: string popoverClass?: string children?: JSX.Element } export const LineCommentAnchor = (props: LineCommentAnchorProps) => { const hidden = () => !props.inline && props.top === undefined const variant = () => props.variant ?? "default" const icon = () => props.icon ?? "comment" const inlineBody = () => props.inline && props.hideButton return ( e.stopPropagation()} on:mouseup={(e) => e.stopPropagation()} on:click={props.onClick as any} on:mouseenter={props.onMouseEnter as any} > } > e.stopPropagation()} on:focusout={props.onPopoverFocusOut as any} > {props.children} > } > e.stopPropagation()} on:click={props.onClick as any} on:mouseenter={props.onMouseEnter as any} on:focusout={props.onPopoverFocusOut as any} > {props.children} ) } export type LineCommentProps = Omit & { comment: JSX.Element selection: JSX.Element actions?: JSX.Element } export const LineComment = (props: LineCommentProps) => { const i18n = useI18n() const [split, rest] = splitProps(props, ["comment", "selection", "actions"]) return ( {split.comment} {split.actions} {i18n.t("ui.lineComment.label.prefix")} {split.selection} {i18n.t("ui.lineComment.label.suffix")} ) } export type LineCommentAddProps = Omit & { label?: string } export const LineCommentAdd = (props: LineCommentAddProps) => { const [split, rest] = splitProps(props, ["label"]) const i18n = useI18n() return ( ) } export type LineCommentEditorProps = Omit & { value: string selection: JSX.Element onInput: (value: string) => void onCancel: VoidFunction onSubmit: (value: string) => void placeholder?: string rows?: number autofocus?: boolean cancelLabel?: string submitLabel?: string mention?: { items: (query: string) => string[] | Promise } } export const LineCommentEditor = (props: LineCommentEditorProps) => { const i18n = useI18n() const [split, rest] = splitProps(props, [ "value", "selection", "onInput", "onCancel", "onSubmit", "placeholder", "rows", "autofocus", "cancelLabel", "submitLabel", "mention", ]) const refs = { textarea: undefined as HTMLTextAreaElement | undefined, } 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 = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}` const cursor = query.start + item.path.length + 2 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 = (e) => { e.preventDefault() e.stopPropagation() } const click = (fn: VoidFunction): JSX.EventHandlerUnion => (e) => { e.stopPropagation() fn() } 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 = split.value.trim() if (!value) return split.onSubmit(value) } onMount(() => { if (split.autofocus === false) return requestAnimationFrame(focus) }) return ( focus()}> { refs.textarea = el }} data-slot="line-comment-textarea" rows={split.rows ?? 3} placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")} value={split.value} on:input={(e) => { const value = (e.currentTarget as HTMLTextAreaElement).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() split.onCancel() return } if (e.key !== "Enter") return if (e.shiftKey) return event.preventDefault() submit() }} /> 0}> {(item) => { const directory = item.path.endsWith("/") ? item.path : getDirectory(item.path) const name = item.path.endsWith("/") ? "" : getFilename(item.path) return ( event.preventDefault()} onMouseEnter={() => mention.setActive(item.path)} onClick={() => selectMention(item)} > {directory} {name} ) }} {i18n.t("ui.lineComment.editorLabel.prefix")} {split.selection} {i18n.t("ui.lineComment.editorLabel.suffix")} {split.cancelLabel ?? i18n.t("ui.common.cancel")} {split.submitLabel ?? i18n.t("ui.lineComment.submit")} > } > {split.cancelLabel ?? i18n.t("ui.common.cancel")} {split.submitLabel ?? i18n.t("ui.lineComment.submit")} ) }