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:
C. Cassel
2026-03-17 09:46:40 -04:00
committed by C. Cassel
parent 68fa54615a
commit 806e2f1ec6
73 changed files with 11715 additions and 32 deletions

View 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,
]
}