"use client"; import { useEffect, useRef } from "react"; import Editor from "@monaco-editor/react"; import { useTheme } from "@/components/theme/app-theme"; import { Button } from "@kandev/ui/button"; import { IconMessagePlus } from "@tabler/icons-react"; import { getMonacoLanguage } from "@/lib/editor/language-map"; import { EDITOR_FONT_FAMILY, EDITOR_FONT_SIZE } from "@/lib/theme/editor-theme"; import { CommentForm } from "@/components/diff/comment-form"; import { CommentDisplay } from "@/components/diff/comment-display"; import { useEditorViewZoneComments } from "@/hooks/use-editor-view-zone-comments"; import { useLspStatusPlacement } from "@/hooks/use-lsp-status-placement"; import { MonacoEditorToolbar } from "./monaco-editor-toolbar"; import { useMonacoEditorComments } from "./use-monaco-editor-state"; import { useMonacoEditorLsp, useMonacoDiffDecorations } from "./use-monaco-editor-lsp"; import { useMonacoWalkthroughRange } from "./use-monaco-walkthrough-range"; import { useMonacoCursorNavigation } from "./use-monaco-cursor-navigation"; import { initMonacoThemes } from "./monaco-init"; import { useTranslation } from "react-i18next"; import type { FilePreviewKind } from "@/lib/utils/file-types"; initMonacoThemes(); type MonacoCodeEditorProps = { path: string; content: string; originalContent: string; isDirty: boolean; hasRemoteUpdate?: boolean; vcsDiff?: string; isSaving: boolean; sessionId?: string; worktreePath?: string; repo?: string; enableComments?: boolean; previewKind?: FilePreviewKind; onTogglePreview?: () => void; onPreviewHtml?: () => void; isPublishingHtmlPreview?: boolean; onChange: (newContent: string) => void; onSave: () => void; onReloadFromAgent?: () => void; onDelete?: () => void; onDownload?: () => void; }; function getMonacoTheme(resolvedTheme: string | undefined): string { return resolvedTheme === "dark" ? "kandev-dark" : "kandev-light"; } const EDITOR_OPTIONS = { fontSize: EDITOR_FONT_SIZE, fontFamily: EDITOR_FONT_FAMILY, lineHeight: 18, minimap: { enabled: false }, scrollBeyondLastLine: false, smoothScrolling: true, cursorSmoothCaretAnimation: "on" as const, glyphMargin: false, lineDecorationsWidth: 10, folding: true, lineNumbers: "on" as const, renderLineHighlight: "line" as const, automaticLayout: true, scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 }, padding: { top: 4 }, "semanticHighlighting.enabled": true, }; type EditorState = ReturnType; function buildCommentZones( state: EditorState, addZone: (line: number, height: number, node: React.ReactNode) => void, ) { for (const comment of state.comments) { const isEditing = state.editingCommentId === comment.id; const node = isEditing ? (
state.handleCommentUpdateRef.current(comment.id, c)} onCancel={() => state.setEditingComment(null)} isEditing />
) : (
state.handleCommentDeleteRef.current(comment.id)} onEdit={() => state.setEditingComment(comment.id)} onRun={() => state.handleCommentRunRef.current(comment)} showCode={false} compact />
); addZone(comment.endLine, isEditing ? 120 : 32, node); } if (state.formZoneRange) { addZone( state.formZoneRange.endLine, 120,
state.handleCommentSubmitRef.current(c)} onSubmitAndRun={(c) => state.handleCommentSubmitAndRunRef.current(c)} onCancel={() => { state.setFormZoneRange(null); state.clearGutterSelection(); }} />
, ); } } function useMonacoCodeEditorSetup(props: MonacoCodeEditorProps) { const { path, content, originalContent, isDirty, vcsDiff, sessionId, worktreePath, repo, enableComments = false, onChange, onSave, } = props; const contentRef = useRef(content); useEffect(() => { contentRef.current = content; }, [content]); const wrapperRef = useRef(null); const language = getMonacoLanguage(path); const state = useMonacoEditorComments({ path, repo, enableComments, sessionId, wrapperRef, onChange, onSave, contentRef, }); const lsp = useMonacoEditorLsp({ sessionId, worktreePath, repo, language, path, contentRef, editorRef: state.editorRef, editorReady: state.editorInstance !== null, }); useMonacoCursorNavigation({ editor: state.editorInstance, path, worktreePath, repo, sessionId, }); const { diffStats } = useMonacoDiffDecorations({ originalContent, isDirty, showDiffIndicators: state.showDiffIndicators, vcsDiff, editorReady: state.editorInstance, contentRef, editorRef: state.editorRef, diffDecorationsRef: state.diffDecorationsRef, }); useEditorViewZoneComments( state.editorInstance, [state.comments, state.formZoneRange, state.editingCommentId], (addZone) => buildCommentZones(state, addZone), ); const options = { ...EDITOR_OPTIONS, wordWrap: state.wrapEnabled ? ("on" as const) : ("off" as const), }; return { contentRef, wrapperRef, language, state, lsp, diffStats, options }; } function EditorLoading() { const { t } = useTranslation(); return (
{t("editors:loadingEditor")}
); } function MonacoFloatingCommentButton({ state }: { state: EditorState }) { const { t } = useTranslation(); if (!state.floatingButtonPos || state.formZoneRange) return null; return ( ); } type MonacoEditorViewProps = { props: MonacoCodeEditorProps; wrapperRef: React.RefObject; editorAreaRef: React.RefObject; language: string; state: EditorState; lsp: ReturnType; diffStats: { additions: number; deletions: number } | null; options: typeof EDITOR_OPTIONS & { wordWrap: "on" | "off" }; resolvedTheme: string | undefined; showLspStatus: boolean; walkthroughRange: ReturnType; }; function MonacoEditorView({ props, wrapperRef, editorAreaRef, language, state, lsp, diffStats, options, resolvedTheme, showLspStatus, walkthroughRange, }: MonacoEditorViewProps) { const { path, content, isDirty, hasRemoteUpdate = false, vcsDiff, isSaving, sessionId, worktreePath, repo, enableComments = false, } = props; return (
state.setWrapEnabled(!state.wrapEnabled)} onToggleDiffIndicators={() => state.setShowDiffIndicators(!state.showDiffIndicators)} onSave={props.onSave} onReloadFromAgent={props.onReloadFromAgent} onDelete={props.onDelete} onDownload={props.onDownload} previewKind={props.previewKind} onTogglePreview={props.onTogglePreview} onPreviewHtml={props.onPreviewHtml} isPublishingHtmlPreview={props.isPublishingHtmlPreview} />
} /> {walkthroughRange ? (
); } export function MonacoCodeEditor(props: MonacoCodeEditorProps) { const { path, repo } = props; const { wrapperRef, language, state, lsp, diffStats, options } = useMonacoCodeEditorSetup(props); const { resolvedTheme } = useTheme(); const lspStatusPlacement = useLspStatusPlacement(); const editorAreaRef = useRef(null); const walkthroughRange = useMonacoWalkthroughRange({ editor: state.editorInstance, editorAreaRef, path, repo, }); return ( ); }