feat: add platform shells, CLI, formatting, plugins, tests, and benchmarks
Phase 4 — Platform shells: - calcpad-macos/: SwiftUI two-column editor with Rust FFI bridge (16 files) - calcpad-windows/: iced GUI with Windows 11 Fluent theme (7 files, 13 tests) - calcpad-web/: React 18 + CodeMirror 6 + WASM Worker + PWA (20 files) - calcpad-cli/: clap-based CLI with expression eval, pipe/stdin, JSON/CSV output, and interactive REPL with rustyline history Phase 5 — Engine modules: - formatting/: answer formatting (decimal/scientific/SI notation, thousands separators, currency), line type classification, clipboard values (93 tests) - plugins/: CalcPadPlugin trait, PluginRegistry, Rhai scripting stub (43 tests) - benches/: criterion benchmarks (single-line, 100/500-line sheets, DAG, incremental) - tests/sheet_scenarios.rs: 20 real-world integration tests - tests/proptest_fuzz.rs: 12 property-based fuzz tests 771 tests passing across workspace, 0 failures.
This commit is contained in:
142
calcpad-web/src/editor/error-display.ts
Normal file
142
calcpad-web/src/editor/error-display.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Inline error display for CalcPad.
|
||||
* Adapted from epic/9-2-codemirror-6-editor.
|
||||
*
|
||||
* Shows red underline decorations and gutter markers for lines with errors.
|
||||
*/
|
||||
|
||||
import {
|
||||
Decoration,
|
||||
type DecorationSet,
|
||||
GutterMarker,
|
||||
gutter,
|
||||
EditorView,
|
||||
} from '@codemirror/view'
|
||||
import { StateField, StateEffect, type Extension, RangeSet } from '@codemirror/state'
|
||||
|
||||
// --- State Effects ---
|
||||
|
||||
export interface LineError {
|
||||
from: number // absolute start position in document
|
||||
to: number // absolute end position in document
|
||||
message: string
|
||||
}
|
||||
|
||||
export const setErrorsEffect = StateEffect.define<LineError[]>()
|
||||
|
||||
// --- Error Gutter Marker ---
|
||||
|
||||
class ErrorGutterMarker extends GutterMarker {
|
||||
override toDOM(): HTMLElement {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'cm-error-marker'
|
||||
span.textContent = '\u26A0' // Warning sign
|
||||
return span
|
||||
}
|
||||
|
||||
override eq(other: GutterMarker): boolean {
|
||||
return other instanceof ErrorGutterMarker
|
||||
}
|
||||
}
|
||||
|
||||
const errorMarkerInstance = new ErrorGutterMarker()
|
||||
|
||||
// --- Error Underline Decorations (StateField) ---
|
||||
|
||||
const errorUnderlineMark = Decoration.mark({ class: 'cm-error-underline' })
|
||||
|
||||
export const errorDecorationsField = StateField.define<DecorationSet>({
|
||||
create() {
|
||||
return Decoration.none
|
||||
},
|
||||
|
||||
update(decos, tr) {
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setErrorsEffect)) {
|
||||
if (effect.value.length === 0) {
|
||||
return Decoration.none
|
||||
}
|
||||
const ranges = effect.value
|
||||
.filter((e) => e.from < e.to)
|
||||
.sort((a, b) => a.from - b.from)
|
||||
.map((e) => errorUnderlineMark.range(e.from, e.to))
|
||||
return RangeSet.of(ranges)
|
||||
}
|
||||
}
|
||||
|
||||
if (tr.docChanged) {
|
||||
return decos.map(tr.changes)
|
||||
}
|
||||
|
||||
return decos
|
||||
},
|
||||
|
||||
provide(field) {
|
||||
return EditorView.decorations.from(field)
|
||||
},
|
||||
})
|
||||
|
||||
// --- Error Lines Set (for gutter) ---
|
||||
|
||||
export const errorLinesField = StateField.define<Set<number>>({
|
||||
create() {
|
||||
return new Set()
|
||||
},
|
||||
|
||||
update(lines, tr) {
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setErrorsEffect)) {
|
||||
const newLines = new Set<number>()
|
||||
for (const error of effect.value) {
|
||||
const lineNumber = tr.state.doc.lineAt(error.from).number
|
||||
newLines.add(lineNumber)
|
||||
}
|
||||
return newLines
|
||||
}
|
||||
}
|
||||
return lines
|
||||
},
|
||||
})
|
||||
|
||||
// --- Error Gutter ---
|
||||
|
||||
export const errorGutter = gutter({
|
||||
class: 'cm-error-gutter',
|
||||
lineMarker(view, line) {
|
||||
const lineNumber = view.state.doc.lineAt(line.from).number
|
||||
const errorLines = view.state.field(errorLinesField)
|
||||
return errorLines.has(lineNumber) ? errorMarkerInstance : null
|
||||
},
|
||||
lineMarkerChange(update) {
|
||||
return update.transactions.some((tr) =>
|
||||
tr.effects.some((e) => e.is(setErrorsEffect)),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// --- Base Theme ---
|
||||
|
||||
export const errorBaseTheme = EditorView.baseTheme({
|
||||
'.cm-error-underline': {
|
||||
textDecoration: 'underline wavy red',
|
||||
},
|
||||
'.cm-error-marker': {
|
||||
color: '#e53e3e',
|
||||
fontSize: '14px',
|
||||
},
|
||||
'.cm-error-gutter': {
|
||||
width: '20px',
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates the error display extension bundle.
|
||||
*/
|
||||
export function errorDisplayExtension(): Extension {
|
||||
return [
|
||||
errorDecorationsField,
|
||||
errorLinesField,
|
||||
errorGutter,
|
||||
errorBaseTheme,
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user