From 1a0df95b0d57c9c51be152633d3a79bfcf3181f6 Mon Sep 17 00:00:00 2001 From: tomymaritano Date: Mon, 8 Jun 2026 22:39:05 -0300 Subject: [PATCH] refactor(editor): extract theme + highlight from MarkdownEditor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the MarkdownEditor split. Pure CSS-variable-based theme and markdown HighlightStyle move to their own file so further extractions (extensions array, keymap bindings) can ride on top without merging against a churn-prone component shell. MarkdownEditor.tsx: 737 -> 612 lines (-17%). editorTheme.ts: new (139 lines). Extracted: - SCROLL_PAST_END_PADDING constant - createEditorTheme(fontSize, fontFamily, lineHeight) — EditorView.theme(...) - markdownHighlighting — HighlightStyle.define([...]) for markdown tags Imports cleaned up: - Drop the explicit HighlightStyle import from @codemirror/language (still consumed transitively, but not referenced in this file anymore) - Drop the `tags` import from @lezer/highlight (moved to editorTheme.ts) What this PR DELIBERATELY does NOT do: - Extract the extensions array (lines 348-444 today). The array closes over user settings (lineNumbersCompartment etc.) and mixes context-coupled values like `wikilinkAutocomplete` from a hook; safely pulling that out requires either passing the closure context in via a builder, or moving the whole `useMemo` into a hook of its own. Better done under Playwright coverage (PR-E #277) so renderer regressions surface. - Extract the keymap. Same reason — the bindings reference view-imperatives + the command-registry which are constructed inside the React tree. Validates: - pnpm -r typecheck — green (renderer + e2e tsconfigs) - pnpm test — 17/17 packages Co-Authored-By: Claude Opus 4.7 (1M context) --- .../renderer/components/MarkdownEditor.tsx | 138 +---------------- .../src/renderer/components/editorTheme.ts | 139 ++++++++++++++++++ 2 files changed, 143 insertions(+), 134 deletions(-) create mode 100644 apps/desktop/src/renderer/components/editorTheme.ts diff --git a/apps/desktop/src/renderer/components/MarkdownEditor.tsx b/apps/desktop/src/renderer/components/MarkdownEditor.tsx index 96c35dfc..f24c2a6a 100644 --- a/apps/desktop/src/renderer/components/MarkdownEditor.tsx +++ b/apps/desktop/src/renderer/components/MarkdownEditor.tsx @@ -16,13 +16,7 @@ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirro import { indentUnit } from '@codemirror/language'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { languages } from '@codemirror/language-data'; -import { - syntaxHighlighting, - HighlightStyle, - indentOnInput, - bracketMatching, -} from '@codemirror/language'; -import { tags } from '@lezer/highlight'; +import { syntaxHighlighting, indentOnInput, bracketMatching } from '@codemirror/language'; import { toggleBold, toggleItalic, @@ -51,6 +45,7 @@ import { htmlToGfmMarkdown } from '../utils/htmlToMarkdown'; import { useEditorBufferStore } from '../stores/editorBufferStore'; import { useSettingsStore, selectEditor } from '../stores/settings'; import { setEditorView } from '../hooks/useCommandRegistry'; +import { createEditorTheme, markdownHighlighting, SCROLL_PAST_END_PADDING } from './editorTheme.js'; // Compartments for dynamic settings const lineNumbersCompartment = new Compartment(); @@ -61,132 +56,8 @@ const tabSizeCompartment = new Compartment(); const scrollPastEndCompartment = new Compartment(); const spellCheckCompartment = new Compartment(); -/** Scroll past end padding - allows scrolling content to top of viewport */ -const SCROLL_PAST_END_PADDING = '50vh'; - -/** Create theme with configurable settings (uses CSS variables for colors) */ -function createEditorTheme(fontSize: number, fontFamily: string, lineHeight: number) { - return EditorView.theme({ - '&': { - backgroundColor: 'transparent', - color: 'var(--cm-text)', - fontSize: `${fontSize}px`, - height: '100%', - }, - '.cm-content': { - fontFamily: fontFamily || "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace", - padding: '12px', - lineHeight: String(lineHeight), - caretColor: 'var(--cm-cursor)', - }, - '.cm-cursor': { - borderLeftColor: 'var(--cm-cursor)', - borderLeftWidth: '2px', - }, - '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { - backgroundColor: 'var(--cm-selection)', - }, - '.cm-activeLine': { - backgroundColor: 'var(--cm-active-line)', - }, - '.cm-activeLineGutter': { - backgroundColor: 'var(--cm-active-line)', - }, - '.cm-gutters': { - backgroundColor: 'transparent', - borderRight: '1px solid var(--cm-gutter-border)', - color: 'var(--cm-gutter-text)', - }, - '.cm-lineNumbers .cm-gutterElement': { - padding: '0 12px 0 16px', - minWidth: '40px', - }, - '.cm-scroller': { - overflow: 'auto', - }, - '.cm-line': { - padding: '0 4px', - }, - '&.cm-focused .cm-matchingBracket': { - backgroundColor: 'var(--cm-bracket-match)', - outline: 'none', - }, - // Autocomplete tooltip - '.cm-tooltip-autocomplete': { - backgroundColor: 'var(--cm-tooltip-bg)', - backdropFilter: 'blur(12px)', - border: '1px solid var(--cm-tooltip-border)', - borderRadius: '8px', - boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', - overflow: 'hidden', - }, - '.cm-tooltip-autocomplete > ul': { - fontFamily: "'Inter', -apple-system, sans-serif", - fontSize: '13px', - maxHeight: '300px', - }, - '.cm-tooltip-autocomplete > ul > li': { - padding: '8px 12px', - color: 'var(--cm-tooltip-text)', - cursor: 'pointer', - }, - '.cm-tooltip-autocomplete > ul > li[aria-selected]': { - backgroundColor: 'var(--accent-muted)', - color: 'var(--accent)', - }, - '.cm-completionLabel': { - fontWeight: '500', - }, - }); -} - -/** Syntax highlighting for Markdown (uses CSS variables for theme-aware colors) */ -const markdownHighlighting = HighlightStyle.define([ - // Headings - { tag: tags.heading1, color: 'var(--cm-heading)', fontWeight: '700', fontSize: '1.5em' }, - { tag: tags.heading2, color: 'var(--cm-heading)', fontWeight: '600', fontSize: '1.3em' }, - { tag: tags.heading3, color: 'var(--cm-heading)', fontWeight: '600', fontSize: '1.15em' }, - { tag: tags.heading4, color: 'var(--cm-heading)', fontWeight: '600' }, - { tag: tags.heading5, color: 'var(--cm-heading)', fontWeight: '600' }, - { tag: tags.heading6, color: 'var(--cm-heading)', fontWeight: '600' }, - - // Emphasis - { tag: tags.emphasis, fontStyle: 'italic', color: 'var(--cm-emphasis)' }, - { tag: tags.strong, fontWeight: '700', color: 'var(--cm-strong)' }, - { tag: tags.strikethrough, textDecoration: 'line-through', color: 'var(--cm-strikethrough)' }, - - // Code - { - tag: tags.monospace, - fontFamily: "'JetBrains Mono', monospace", - backgroundColor: 'var(--cm-code-bg)', - padding: '2px 4px', - borderRadius: '3px', - }, - - // Links - { tag: tags.link, color: 'var(--cm-link)', textDecoration: 'underline' }, - { tag: tags.url, color: 'var(--cm-link)' }, - - // Lists - { tag: tags.list, color: 'var(--cm-list)' }, - - // Quotes - { - tag: tags.quote, - color: 'var(--cm-quote)', - fontStyle: 'italic', - borderLeft: '3px solid var(--cm-quote-border)', - paddingLeft: '12px', - }, - - // Meta (like --- for frontmatter) - { tag: tags.meta, color: 'var(--cm-meta)' }, - { tag: tags.comment, color: 'var(--cm-meta)' }, - - // Punctuation - { tag: tags.processingInstruction, color: 'var(--cm-meta)' }, -]); +// createEditorTheme, markdownHighlighting, and SCROLL_PAST_END_PADDING +// live in editorTheme.ts. interface MarkdownEditorProps { initialContent: string; @@ -363,7 +234,6 @@ export const MarkdownEditor = forwardRef { - // eslint-disable-next-line no-console console.error('[CodeMirror] plugin error:', err); const sentry = ( globalThis as unknown as { diff --git a/apps/desktop/src/renderer/components/editorTheme.ts b/apps/desktop/src/renderer/components/editorTheme.ts new file mode 100644 index 00000000..5b249c1d --- /dev/null +++ b/apps/desktop/src/renderer/components/editorTheme.ts @@ -0,0 +1,139 @@ +/** + * CodeMirror theme + syntax highlighting for Readied's MarkdownEditor. + * + * Pure values extracted from MarkdownEditor.tsx so theme tweaks don't + * force a rebuild of the entire editor file. Colors come from CSS + * variables (defined in renderer/styles/) so light/dark switching works + * without rebuilding the EditorView. + */ + +import { EditorView } from '@codemirror/view'; +import { HighlightStyle } from '@codemirror/language'; +import { tags } from '@lezer/highlight'; + +/** Padding under the document so the user can scroll the last line near the top. */ +export const SCROLL_PAST_END_PADDING = '50vh'; + +/** Build a CodeMirror theme bound to the user's font/size preferences. */ +export function createEditorTheme(fontSize: number, fontFamily: string, lineHeight: number) { + return EditorView.theme({ + '&': { + backgroundColor: 'transparent', + color: 'var(--cm-text)', + fontSize: `${fontSize}px`, + height: '100%', + }, + '.cm-content': { + fontFamily: fontFamily || "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace", + padding: '12px', + lineHeight: String(lineHeight), + caretColor: 'var(--cm-cursor)', + }, + '.cm-cursor': { + borderLeftColor: 'var(--cm-cursor)', + borderLeftWidth: '2px', + }, + '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { + backgroundColor: 'var(--cm-selection)', + }, + '.cm-activeLine': { + backgroundColor: 'var(--cm-active-line)', + }, + '.cm-activeLineGutter': { + backgroundColor: 'var(--cm-active-line)', + }, + '.cm-gutters': { + backgroundColor: 'transparent', + borderRight: '1px solid var(--cm-gutter-border)', + color: 'var(--cm-gutter-text)', + }, + '.cm-lineNumbers .cm-gutterElement': { + padding: '0 12px 0 16px', + minWidth: '40px', + }, + '.cm-scroller': { + overflow: 'auto', + }, + '.cm-line': { + padding: '0 4px', + }, + '&.cm-focused .cm-matchingBracket': { + backgroundColor: 'var(--cm-bracket-match)', + outline: 'none', + }, + // Autocomplete tooltip + '.cm-tooltip-autocomplete': { + backgroundColor: 'var(--cm-tooltip-bg)', + backdropFilter: 'blur(12px)', + border: '1px solid var(--cm-tooltip-border)', + borderRadius: '8px', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', + overflow: 'hidden', + }, + '.cm-tooltip-autocomplete > ul': { + fontFamily: "'Inter', -apple-system, sans-serif", + fontSize: '13px', + maxHeight: '300px', + }, + '.cm-tooltip-autocomplete > ul > li': { + padding: '8px 12px', + color: 'var(--cm-tooltip-text)', + cursor: 'pointer', + }, + '.cm-tooltip-autocomplete > ul > li[aria-selected]': { + backgroundColor: 'var(--accent-muted)', + color: 'var(--accent)', + }, + '.cm-completionLabel': { + fontWeight: '500', + }, + }); +} + +/** Syntax highlighting for Markdown — uses CSS variables so dark/light works. */ +export const markdownHighlighting = HighlightStyle.define([ + // Headings + { tag: tags.heading1, color: 'var(--cm-heading)', fontWeight: '700', fontSize: '1.5em' }, + { tag: tags.heading2, color: 'var(--cm-heading)', fontWeight: '600', fontSize: '1.3em' }, + { tag: tags.heading3, color: 'var(--cm-heading)', fontWeight: '600', fontSize: '1.15em' }, + { tag: tags.heading4, color: 'var(--cm-heading)', fontWeight: '600' }, + { tag: tags.heading5, color: 'var(--cm-heading)', fontWeight: '600' }, + { tag: tags.heading6, color: 'var(--cm-heading)', fontWeight: '600' }, + + // Emphasis + { tag: tags.emphasis, fontStyle: 'italic', color: 'var(--cm-emphasis)' }, + { tag: tags.strong, fontWeight: '700', color: 'var(--cm-strong)' }, + { tag: tags.strikethrough, textDecoration: 'line-through', color: 'var(--cm-strikethrough)' }, + + // Code + { + tag: tags.monospace, + fontFamily: "'JetBrains Mono', monospace", + backgroundColor: 'var(--cm-code-bg)', + padding: '2px 4px', + borderRadius: '3px', + }, + + // Links + { tag: tags.link, color: 'var(--cm-link)', textDecoration: 'underline' }, + { tag: tags.url, color: 'var(--cm-link)' }, + + // Lists + { tag: tags.list, color: 'var(--cm-list)' }, + + // Quotes + { + tag: tags.quote, + color: 'var(--cm-quote)', + fontStyle: 'italic', + borderLeft: '3px solid var(--cm-quote-border)', + paddingLeft: '12px', + }, + + // Meta (like --- for frontmatter) + { tag: tags.meta, color: 'var(--cm-meta)' }, + { tag: tags.comment, color: 'var(--cm-meta)' }, + + // Punctuation + { tag: tags.processingInstruction, color: 'var(--cm-meta)' }, +]);