diff options
| author | Clawd <ai@clawd.bot> | 2026-02-28 13:51:38 -0800 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-02-28 13:51:38 -0800 |
| commit | 812702aa9afbfe8de7672520852a6066341a4dc0 (patch) | |
| tree | 17f778c2989b49b43ecb4a5975fe6f2979fe509b /renderer/src/components/DocumentPane.tsx | |
| parent | c386a1acfab0db99af57a9a18a49c72b89184f15 (diff) | |
Add CodeMirror 6 for markdown editing
- Install CodeMirror 6 with markdown language support
- Add MarkdownEditor component with:
- Syntax highlighting for markdown
- Line numbers
- Active line highlighting
- History (undo/redo)
- One Dark theme
- Code block language highlighting
- Keep react-markdown for preview mode
- Add CSS for CodeMirror integration
Diffstat (limited to 'renderer/src/components/DocumentPane.tsx')
| -rw-r--r-- | renderer/src/components/DocumentPane.tsx | 100 |
1 files changed, 94 insertions, 6 deletions
diff --git a/renderer/src/components/DocumentPane.tsx b/renderer/src/components/DocumentPane.tsx index 95d7d03..cf777b1 100644 --- a/renderer/src/components/DocumentPane.tsx +++ b/renderer/src/components/DocumentPane.tsx | |||
| @@ -1,6 +1,12 @@ | |||
| 1 | import React, { useState } from "react"; | 1 | import React, { useState, useRef, useEffect } from "react"; |
| 2 | import ReactMarkdown from "react-markdown"; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import remarkGfm from "remark-gfm"; | 3 | import remarkGfm from "remark-gfm"; |
| 4 | import { EditorState } from "@codemirror/state"; | ||
| 5 | import { EditorView, keymap, lineNumbers, highlightActiveLine } from "@codemirror/view"; | ||
| 6 | import { markdown } from "@codemirror/lang-markdown"; | ||
| 7 | import { languages } from "@codemirror/language-data"; | ||
| 8 | import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; | ||
| 9 | import { oneDark } from "@codemirror/theme-one-dark"; | ||
| 4 | import type { Phase } from "../types"; | 10 | import type { Phase } from "../types"; |
| 5 | 11 | ||
| 6 | interface DocumentPaneProps { | 12 | interface DocumentPaneProps { |
| @@ -11,6 +17,90 @@ interface DocumentPaneProps { | |||
| 11 | showOnboarding?: boolean; | 17 | showOnboarding?: boolean; |
| 12 | } | 18 | } |
| 13 | 19 | ||
| 20 | function MarkdownEditor({ | ||
| 21 | content, | ||
| 22 | onChange, | ||
| 23 | disabled, | ||
| 24 | }: { | ||
| 25 | content: string; | ||
| 26 | onChange: (content: string) => void; | ||
| 27 | disabled: boolean; | ||
| 28 | }) { | ||
| 29 | const editorRef = useRef<HTMLDivElement>(null); | ||
| 30 | const viewRef = useRef<EditorView | null>(null); | ||
| 31 | |||
| 32 | useEffect(() => { | ||
| 33 | if (!editorRef.current) return; | ||
| 34 | |||
| 35 | const updateListener = EditorView.updateListener.of((update) => { | ||
| 36 | if (update.docChanged) { | ||
| 37 | onChange(update.state.doc.toString()); | ||
| 38 | } | ||
| 39 | }); | ||
| 40 | |||
| 41 | const state = EditorState.create({ | ||
| 42 | doc: content, | ||
| 43 | extensions: [ | ||
| 44 | lineNumbers(), | ||
| 45 | highlightActiveLine(), | ||
| 46 | history(), | ||
| 47 | keymap.of([...defaultKeymap, ...historyKeymap]), | ||
| 48 | markdown({ codeLanguages: languages }), | ||
| 49 | oneDark, | ||
| 50 | updateListener, | ||
| 51 | EditorView.editable.of(!disabled), | ||
| 52 | EditorView.theme({ | ||
| 53 | "&": { | ||
| 54 | height: "100%", | ||
| 55 | fontSize: "14px", | ||
| 56 | }, | ||
| 57 | ".cm-scroller": { | ||
| 58 | overflow: "auto", | ||
| 59 | fontFamily: '"SF Mono", Monaco, "Cascadia Code", monospace', | ||
| 60 | }, | ||
| 61 | ".cm-content": { | ||
| 62 | padding: "16px 0", | ||
| 63 | }, | ||
| 64 | ".cm-line": { | ||
| 65 | padding: "0 16px", | ||
| 66 | }, | ||
| 67 | }), | ||
| 68 | ], | ||
| 69 | }); | ||
| 70 | |||
| 71 | const view = new EditorView({ | ||
| 72 | state, | ||
| 73 | parent: editorRef.current, | ||
| 74 | }); | ||
| 75 | |||
| 76 | viewRef.current = view; | ||
| 77 | |||
| 78 | return () => { | ||
| 79 | view.destroy(); | ||
| 80 | viewRef.current = null; | ||
| 81 | }; | ||
| 82 | }, [disabled]); | ||
| 83 | |||
| 84 | // Update content when it changes externally | ||
| 85 | useEffect(() => { | ||
| 86 | const view = viewRef.current; | ||
| 87 | if (!view) return; | ||
| 88 | |||
| 89 | const currentContent = view.state.doc.toString(); | ||
| 90 | if (content !== currentContent) { | ||
| 91 | view.dispatch({ | ||
| 92 | changes: { | ||
| 93 | from: 0, | ||
| 94 | to: currentContent.length, | ||
| 95 | insert: content, | ||
| 96 | }, | ||
| 97 | }); | ||
| 98 | } | ||
| 99 | }, [content]); | ||
| 100 | |||
| 101 | return <div ref={editorRef} className="codemirror-editor" />; | ||
| 102 | } | ||
| 103 | |||
| 14 | export function DocumentPane({ | 104 | export function DocumentPane({ |
| 15 | content, | 105 | content, |
| 16 | onChange, | 106 | onChange, |
| @@ -115,12 +205,10 @@ export function DocumentPane({ | |||
| 115 | </div> | 205 | </div> |
| 116 | 206 | ||
| 117 | {isEditing ? ( | 207 | {isEditing ? ( |
| 118 | <textarea | 208 | <MarkdownEditor |
| 119 | className="document-content editing" | 209 | content={content} |
| 120 | value={content} | 210 | onChange={onChange} |
| 121 | onChange={(e) => onChange(e.target.value)} | ||
| 122 | disabled={disabled} | 211 | disabled={disabled} |
| 123 | placeholder={`${filename} will appear here...`} | ||
| 124 | /> | 212 | /> |
| 125 | ) : ( | 213 | ) : ( |
| 126 | <div | 214 | <div |
