/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/session-ui/src/components/line-comment.tsx
439 строк
13 KB
Victor Navarro
refactor(ui): isolate session components (#33670)
24 июн 2026, 17:56
Не верифицирован
24 июн 2026, 17:56
04c1730
Код
Авторство
О чём код?
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 "@opencode-ai/ui/button" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { installLineCommentStyles } from "./line-comment-styles" import { useI18n } from "@opencode-ai/ui/context/i18n" installLineCommentStyles() export type LineCommentVariant = "default" | "editor" | "add" function InlineGlyph(props: { icon: "comment" | "plus" }) { return ( <svg data-slot="line-comment-icon" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <Show when={props.icon === "comment"} fallback={ <path d="M10 5.41699V10.0003M10 10.0003V14.5837M10 10.0003H5.4165M10 10.0003H14.5832" stroke="currentColor" stroke-linecap="square" /> } > <path d="M16.25 3.75H3.75V16.25L6.875 14.4643H16.25V3.75Z" stroke="currentColor" stroke-linecap="square" /> </Show> </svg> ) } export type LineCommentAnchorProps = { id?: string top?: number inline?: boolean hideButton?: boolean open: boolean variant?: LineCommentVariant icon?: "comment" | "plus" buttonLabel?: string onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent> 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 ( <div data-component="line-comment" data-prevent-autofocus="" data-variant={variant()} data-comment-id={props.id} data-open={props.open ? "" : undefined} data-inline={props.inline ? "" : undefined} classList={{ [props.class ?? ""]: !!props.class, }} style={ props.inline ? undefined : { top: `${props.top ?? 0}px`, opacity: hidden() ? 0 : 1, "pointer-events": hidden() ? "none" : "auto", } } > <Show when={inlineBody()} fallback={ <> <button type="button" aria-label={props.buttonLabel} data-slot="line-comment-button" on:mousedown={(e) => e.stopPropagation()} on:mouseup={(e) => e.stopPropagation()} on:click={props.onClick as any} on:mouseenter={props.onMouseEnter as any} > <Show when={props.inline} fallback={<Icon name={icon() === "plus" ? "plus-small" : "comment"} size="small" />} > <InlineGlyph icon={icon()} /> </Show> </button> <Show when={props.open}> <div data-slot="line-comment-popover" classList={{ [props.popoverClass ?? ""]: !!props.popoverClass, }} on:mousedown={(e) => e.stopPropagation()} on:focusout={props.onPopoverFocusOut as any} > {props.children} </div> </Show> </> } > <div data-slot="line-comment-popover" data-inline-body="" classList={{ [props.popoverClass ?? ""]: !!props.popoverClass, }} on:mousedown={(e) => e.stopPropagation()} on:click={props.onClick as any} on:mouseenter={props.onMouseEnter as any} on:focusout={props.onPopoverFocusOut as any} > {props.children} </div> </Show> </div> ) } export type LineCommentProps = Omit<LineCommentAnchorProps, "children" | "variant"> & { 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 ( <LineCommentAnchor {...rest} variant="default" hideButton={props.inline}> <div data-slot="line-comment-content"> <div data-slot="line-comment-head"> <div data-slot="line-comment-text">{split.comment}</div> <Show when={split.actions}> <div data-slot="line-comment-tools">{split.actions}</div> </Show> </div> <div data-slot="line-comment-label"> {i18n.t("ui.lineComment.label.prefix")} {split.selection} {i18n.t("ui.lineComment.label.suffix")} </div> </div> </LineCommentAnchor> ) } export type LineCommentAddProps = Omit<LineCommentAnchorProps, "children" | "variant" | "open" | "icon"> & { label?: string } export const LineCommentAdd = (props: LineCommentAddProps) => { const [split, rest] = splitProps(props, ["label"]) const i18n = useI18n() return ( <LineCommentAnchor {...rest} open={false} variant="add" icon="plus" buttonLabel={split.label ?? i18n.t("ui.lineComment.submit")} /> ) } export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "open" | "variant" | "onClick"> & { 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<string[]> } } 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"], skipFilter: () => true, onSelect: selectMention, }) const focus = () => refs.textarea?.focus() const hold: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => { e.preventDefault() e.stopPropagation() } const click = (fn: VoidFunction): JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> => (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 ( <LineCommentAnchor {...rest} open={true} variant="editor" hideButton={props.inline} onClick={() => focus()}> <div data-slot="line-comment-editor"> <textarea ref={(el) => { 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() }} /> <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")} {split.selection} {i18n.t("ui.lineComment.editorLabel.suffix")} </div> <Show when={!props.inline} fallback={ <> <button type="button" data-slot="line-comment-action" data-variant="ghost" on:mousedown={hold as any} on:click={click(split.onCancel) as any} > {split.cancelLabel ?? i18n.t("ui.common.cancel")} </button> <button type="button" data-slot="line-comment-action" data-variant="primary" disabled={split.value.trim().length === 0} on:mousedown={hold as any} on:click={click(submit) as any} > {split.submitLabel ?? i18n.t("ui.lineComment.submit")} </button> </> } > <Button size="small" variant="ghost" onClick={split.onCancel}> {split.cancelLabel ?? i18n.t("ui.common.cancel")} </Button> <Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}> {split.submitLabel ?? i18n.t("ui.lineComment.submit")} </Button> </Show> </div> </div> </LineCommentAnchor> ) }