diff --git a/frontend/src/components/editor/EditorPane.tsx b/frontend/src/components/editor/EditorPane.tsx index 9ac4e3a..dc3e65a 100644 --- a/frontend/src/components/editor/EditorPane.tsx +++ b/frontend/src/components/editor/EditorPane.tsx @@ -1,11 +1,55 @@ import { useEffect, useRef } from "react"; -import { EditorView, keymap } from "@codemirror/view"; -import { EditorState } from "@codemirror/state"; +import { EditorView, keymap, lineNumbers, highlightActiveLine, Decoration, ViewPlugin } from "@codemirror/view"; +import { EditorState, RangeSetBuilder } from "@codemirror/state"; import type { Extension } from "@codemirror/state"; +import type { DecorationSet } from "@codemirror/view"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { searchKeymap } from "@codemirror/search"; import { oneDark } from "./oneDarkTheme"; +function toRoman(n: number): string { + const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]; + const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]; + let result = ""; + for (let i = 0; i < vals.length; i++) { + while (n >= vals[i]) { + result += syms[i]; + n -= vals[i]; + } + } + return result; +} + +const dotDeco = Decoration.mark({ class: "cm-dot-space" }); + +function buildSpaceDeco(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + for (const { from, to } of view.visibleRanges) { + const text = view.state.sliceDoc(from, to); + for (let i = 0; i < text.length; i++) { + if (text[i] === " ") { + builder.add(from + i, from + i + 1, dotDeco); + } + } + } + return builder.finish(); +} + +const subtleWhitespace = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = buildSpaceDeco(view); + } + update(update: { docChanged: boolean; viewportChanged: boolean; view: EditorView }) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildSpaceDeco(update.view); + } + } + }, + { decorations: (v) => v.decorations }, +); + interface Props { value: string; onChange: (value: string) => void; @@ -25,6 +69,9 @@ export default function EditorPane({ value, onChange, extensions = [] }: Props) doc: value, extensions: [ history(), + lineNumbers({ formatNumber: toRoman }), + highlightActiveLine(), + subtleWhitespace, keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap]), oneDark, EditorView.updateListener.of((update) => { @@ -49,6 +96,20 @@ export default function EditorPane({ value, onChange, extensions = [] }: Props) borderLeftColor: "var(--primary)", borderLeftWidth: "0.5em", }, + ".cm-dot-space": { + color: "transparent", + position: "relative", + }, + ".cm-dot-space:before": { + content: '"\\22C5"', + position: "absolute", + left: "0", + right: "0", + textAlign: "center", + color: "var(--muted-foreground)", + opacity: "0.5", + pointerEvents: "none", + }, }), ...extensions, ], diff --git a/frontend/src/components/editor/oneDarkTheme.ts b/frontend/src/components/editor/oneDarkTheme.ts index ee7dbc6..ca7ac10 100644 --- a/frontend/src/components/editor/oneDarkTheme.ts +++ b/frontend/src/components/editor/oneDarkTheme.ts @@ -3,26 +3,27 @@ import { EditorView } from "@codemirror/view"; export const oneDark = EditorView.theme( { "&": { - backgroundColor: "#0d1117", - color: "#c9d1d9", + backgroundColor: "var(--background)", + color: "var(--foreground)", }, ".cm-cursor, .cm-cursor-primary": { borderLeftColor: "var(--primary)", borderLeftWidth: "0.5em", }, ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { - backgroundColor: "#264f78", + backgroundColor: "color-mix(in oklch, var(--primary) 25%, transparent)", }, ".cm-activeLine": { - backgroundColor: "#161b2266", + backgroundColor: "color-mix(in oklch, var(--primary) 8%, transparent)", }, ".cm-gutters": { - backgroundColor: "#0d1117", - color: "#484f58", - borderRight: "1px solid #21262d", + backgroundColor: "var(--background)", + color: "var(--muted-foreground)", + borderRight: "1px solid var(--border)", }, ".cm-activeLineGutter": { - backgroundColor: "#161b2266", + backgroundColor: "color-mix(in oklch, var(--primary) 8%, transparent)", + color: "var(--primary)", }, }, { dark: true }