Skip to content

Commit 7e8ffdb

Browse files
committed
feat(web): mount() text I/O + per-range diagnostics (gh#1, gh#2) (v0.4.0)
Surface text I/O and precise diagnostics across the FFI to the web mount() controller, and add the shared Editor methods for all platforms. gh#1: cross-realm WASM<->JS bridge — push buffer text out via hone_editor_set_buffer_text (drives getText/onTextChange); pull setText/setCursor through the editor's poll loop (has_/take_pending_*). Adds getText/setText/onTextChange/setCursor/focus. gh#2: setRangeDiagnostics/clearRangeDiagnostics — severity-colored squiggles + hover tooltip on web (full, pointer hit-tested), wavy-underline rendering on macOS; link-resolving stubs on ios/windows/linux/android/harmonyos (same status as their line-diagnostics). +10 FFI functions in the perry.nativeLibrary manifest; 15 new tests in tests/web-controller.test.ts; web bundle rebuild verified.
1 parent c4e8fe7 commit 7e8ffdb

14 files changed

Lines changed: 1088 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,28 @@ ALL interaction directly by mutating `frame_lines` in place and calling `setNeed
159159
- Android: ⬜ Canvas/Skia JNI — needs `tokenizer.rs` port + same EditorView patterns
160160
- Web: ✅ TypeScript DOM renderer at `native/web/dom-ffi.ts`. No Rust crate — Perry's
161161
web target compiles the editor TS to WASM and imports JS-side `hone_editor_*`. Find
162-
highlights / diagnostics / breakpoints / fold ranges render as DOM overlay layers.
163-
Syntax highlighting works structurally but token COLORS are blocked on Perry
164-
PerryTS/perry#1071 (cross-module nested-object access returns `undefined`). Build:
162+
highlights / line diagnostics / per-range diagnostics / breakpoints / fold ranges render
163+
as DOM overlay layers. Syntax highlighting works structurally but token COLORS are blocked
164+
on Perry PerryTS/perry#1071 (cross-module nested-object access returns `undefined`). Build:
165165
`bun run examples/web/build.ts``examples/web/dist/{hone-editor.wasm, hone-editor.js, index.html}`.
166166

167+
### `mount()` controller bridge (gh#1, gh#2)
168+
The `mount(el, opts)` controller spans the WASM↔JS realm boundary: the buffer lives in the
169+
Perry-compiled WASM, the controller in plain JS. Two FFI patterns bridge them, both polled by
170+
the editor's existing 8ms `setInterval` (which **does** fire on web, unlike macOS):
171+
- **Push (editor → host):** the Editor calls `hone_editor_set_buffer_text` after every change;
172+
dom-ffi stores it (`ed.bufferText`) and fires `onTextChange` listeners. `getText()` reads it.
173+
- **Pull (host → editor):** `setText`/`setCursor` queue a request on the EditorState; the WASM
174+
poll loop drains it via `hone_editor_has_pending_*` / `take_pending_*` (see
175+
`Editor.flushEvents`). On **native** the host holds the Editor directly so the `has_*` probes
176+
return 0 — these FFIs are link-resolving no-ops in every Rust crate (same status as line
177+
diagnostics: only macOS + web render).
178+
- **Per-range diagnostics** (`setRangeDiagnostics`): squiggle the exact span (severity-colored
179+
wavy underline) + a pointer-hit-tested hover tooltip. Implemented on web (`dom-ffi.ts`,
180+
full incl. hover) and macOS (`editor_view.rs`, squiggle rendering; native hover is a follow-up).
181+
Pure helpers `computeDiagSegments` / `rangeDiagAtPosition` / `severityColor` are unit-tested in
182+
`tests/web-controller.test.ts`.
183+
167184
## Commands
168185
Run tests: `bun test`
169186
Run single test file: `bun test tests/buffer.test.ts`
@@ -176,7 +193,7 @@ Build Windows crate: `cd native/windows && cargo build`
176193
Run Windows interactive demo: `cd native/windows && cargo run --example demo_editor`
177194
Run iOS interactive demo: `cd native/ios && cargo run --example demo_editor_ios`
178195
Run Android interactive demo: `cd native/android && bash run-demo.sh`
179-
Build for web: `bun run examples/web/build.ts` — produces `examples/web/dist/{hone-editor.wasm, hone-editor.js, index.html}`. The build orchestrates: (1) `perry compile examples/web/entry.ts --target web --output hone-editor.wasm` for the raw WASM; (2) a second Perry compile to .html to extract the JS runtime bridge; (3) bundles `native/web/dom-ffi.ts` + the bridge + a `mount()` API into `hone-editor.js`. Consumers: `import { mount } from './hone-editor.js'; await mount(document.getElementById('editor'));`
196+
Build for web: `bun run examples/web/build.ts` — produces `examples/web/dist/{hone-editor.wasm, hone-editor.js, index.html}`. The build orchestrates: (1) `perry compile examples/web/entry.ts --target web --output hone-editor.wasm` for the raw WASM; (2) a second Perry compile to .html to extract the JS runtime bridge; (3) bundles `native/web/dom-ffi.ts` + the bridge + a `mount()` API into `hone-editor.js`. Consumers: `import { mount } from './hone-editor.js'; const ed = await mount(document.getElementById('editor'));`. The returned controller exposes overlay setters plus `getText()`, `setText(v)`, `onTextChange(cb)`, `setCursor(line,col)`, `focus()` (gh#1) and `setRangeDiagnostics(json)` / `clearRangeDiagnostics()` (gh#2).
180197

181198
## Development Phases (from PROJECT_PLAN.md)
182199

examples/web/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,41 @@ Produces `examples/web/dist/index.html`. Open it directly in a browser:
1616
open examples/web/dist/index.html
1717
```
1818

19+
## Controller API
20+
21+
`mount(el, opts)` returns a controller:
22+
23+
```js
24+
import { mount } from './hone-editor.js';
25+
const ed = await mount(document.getElementById('editor'), {
26+
content: 'const x = 1;', language: 'typescript', theme: 'dark',
27+
});
28+
29+
// Text I/O + change observation (gh#1)
30+
const src = ed.getText(); // current buffer text
31+
ed.setText('// loaded example\n'); // replace all text
32+
const off = ed.onTextChange((text) => { // observe edits; returns unsubscribe
33+
localStorage.setItem('draft', text);
34+
});
35+
ed.setCursor(12, 4); // 0-based; scrolls the line into view
36+
ed.focus();
37+
38+
// Per-range diagnostics: squiggles + hover tooltip (gh#2). 0-based ranges.
39+
ed.setRangeDiagnostics([
40+
{ startLine: 4, startCol: 10, endLine: 4, endCol: 20, severity: 1,
41+
message: "Type 'string' is not assignable to type 'number'", code: 'ts(2322)' },
42+
]);
43+
ed.clearRangeDiagnostics();
44+
45+
// Overlays (existing)
46+
ed.setFindHighlights('[{"line":2,"col":13,"len":6,"current":1}]');
47+
ed.setLineDiagnostics('5:2:#cca700:unused variable');
48+
ed.setBreakpoints('3\n7');
49+
ed.setFoldRanges('2:1');
50+
```
51+
52+
`severity`: 1=error, 2=warning, 3=info, 4=hint.
53+
1954
## What the build does
2055

2156
1. `perry compile perry/editor-component.ts --target web` → a self-contained

examples/web/build.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,22 @@ async function loadWasmAsBase64(url) {
138138
* object: explicit URL map, e.g. { typescript: '...', javascript: '...' }.
139139
* false / omit: use keyword tokenizer only.
140140
*
141-
* Returns a controller object exposing the underlying Editor instance for
142-
* setting find highlights, diagnostics, breakpoints, etc.
141+
* Returns a controller object:
142+
* handle {number}
143+
* // overlays
144+
* setFindHighlights(json), clearFindHighlights()
145+
* setLineDiagnostics(packed), clearDiagnostics()
146+
* setBreakpoints(packed), setFoldRanges(packed)
147+
* // text I/O + change observation (gh#1)
148+
* getText() -> string current buffer text
149+
* setText(value) replace all text (loads examples / permalinks)
150+
* onTextChange(cb) -> () => void subscribe to edits; returns unsubscribe
151+
* setCursor(line, column) 0-based; scrolls the line into view
152+
* focus() give the editor keyboard focus
153+
* // per-range diagnostics: squiggles + hover tooltip (gh#2)
154+
* setRangeDiagnostics(json) json (string or array) of
155+
* [{startLine,startCol,endLine,endCol,severity,message,code?}, …] (0-based)
156+
* clearRangeDiagnostics()
143157
*/
144158
export async function mount(el, opts = {}) {
145159
if (!el || typeof el.appendChild !== 'function') {
@@ -198,12 +212,23 @@ export async function mount(el, opts = {}) {
198212
const ffi = ctx.ffi;
199213
return {
200214
handle: h,
215+
// Overlay setters
201216
setFindHighlights: (json) => ffi.hone_editor_set_find_highlights(h, json),
202217
clearFindHighlights: () => ffi.hone_editor_clear_find_highlights(h),
203218
setLineDiagnostics: (packed) => ffi.hone_editor_set_line_diagnostics(h, packed),
204219
clearDiagnostics: () => ffi.hone_editor_clear_diagnostics(h),
205220
setBreakpoints: (packed) => ffi.hone_editor_set_breakpoints(h, packed),
206221
setFoldRanges: (packed) => ffi.hone_editor_set_fold_ranges(h, packed),
222+
// Text I/O + change observation (gh#1)
223+
getText: () => ctx.getText(h),
224+
setText: (value) => ctx.requestSetText(h, value),
225+
onTextChange: (cb) => ctx.onTextChange(h, cb),
226+
setCursor: (line, column) => ctx.requestSetCursor(h, line, column),
227+
focus: () => ffi.hone_editor_focus(h),
228+
// Per-range diagnostics: squiggles + hover tooltip (gh#2)
229+
setRangeDiagnostics: (json) =>
230+
ffi.hone_editor_set_range_diagnostics(h, typeof json === 'string' ? json : JSON.stringify(json)),
231+
clearRangeDiagnostics: () => ffi.hone_editor_clear_range_diagnostics(h),
207232
};
208233
}
209234
`;
@@ -252,8 +277,22 @@ export class Editor {
252277
});
253278
// Drive the overlay layers to demonstrate them at runtime.
254279
ed.setFindHighlights('[{"line":2,"col":13,"len":6,"current":1}]');
255-
ed.setLineDiagnostics('5:2:#f87171:cursorLine is implicitly typed as number — declare it\\n13:1:#f87171:Type \\'string\\' is not assignable to parameter of type \\'number\\'');
256280
ed.setBreakpoints('3\\n7');
281+
282+
// Per-range diagnostics (gh#2): squiggle the exact span + hover tooltip.
283+
ed.setRangeDiagnostics([
284+
{ startLine: 4, startCol: 10, endLine: 4, endCol: 20, severity: 2,
285+
message: 'cursorLine is implicitly typed as number — declare it', code: 'ts(7008)' },
286+
{ startLine: 12, startCol: 24, endLine: 12, endCol: 28, severity: 1,
287+
message: "Argument of type 'string' is not assignable to parameter of type 'number'", code: 'ts(2345)' },
288+
]);
289+
290+
// Text I/O (gh#1): read the live buffer + observe edits.
291+
ed.onTextChange((text) => {
292+
console.log('buffer changed — ' + text.length + ' chars');
293+
});
294+
// e.g. read on demand: const source = ed.getText();
295+
// e.g. jump to a diagnostic: ed.setCursor(12, 24); ed.focus();
257296
</script>
258297
</body>
259298
</html>

native/android/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,31 @@ pub extern "C" fn hone_editor_clear_diagnostics(_view: *mut EditorView) {}
997997
pub extern "C" fn hone_editor_set_breakpoints(_view: *mut EditorView, _data: f64) {}
998998
#[no_mangle]
999999
pub extern "C" fn hone_editor_set_fold_ranges(_view: *mut EditorView, _data: f64) {}
1000+
1001+
// === gh#1 host bridge + gh#2 range diagnostics (link-resolving stubs) ===
1002+
// Range-diagnostic squiggles and the host text/cursor bridge are implemented
1003+
// on macOS + web; these exist so Perry apps embedding the editor link cleanly.
1004+
// (Android already stubs line diagnostics above — same status.)
1005+
#[no_mangle]
1006+
pub extern "C" fn hone_editor_set_range_diagnostics(_view: *mut EditorView, _json: i64) {}
1007+
#[no_mangle]
1008+
pub extern "C" fn hone_editor_clear_range_diagnostics(_view: *mut EditorView) {}
1009+
#[no_mangle]
1010+
pub extern "C" fn hone_editor_focus(_view: *mut EditorView) {}
1011+
#[no_mangle]
1012+
pub extern "C" fn hone_editor_set_buffer_text(_view: *mut EditorView, _text: i64) {}
1013+
#[no_mangle]
1014+
pub extern "C" fn hone_editor_has_pending_set_text(_view: *mut EditorView) -> f64 { 0.0 }
1015+
#[no_mangle]
1016+
pub extern "C" fn hone_editor_take_pending_set_text(_view: *mut EditorView) -> i64 { 0 }
1017+
#[no_mangle]
1018+
pub extern "C" fn hone_editor_has_pending_cursor(_view: *mut EditorView) -> f64 { 0.0 }
1019+
#[no_mangle]
1020+
pub extern "C" fn hone_editor_pending_cursor_line(_view: *mut EditorView) -> f64 { 0.0 }
1021+
#[no_mangle]
1022+
pub extern "C" fn hone_editor_pending_cursor_col(_view: *mut EditorView) -> f64 { 0.0 }
1023+
#[no_mangle]
1024+
pub extern "C" fn hone_editor_clear_pending_cursor(_view: *mut EditorView) {}
10001025
/// Set persistent find highlights (NOT cleared by begin_frame).
10011026
#[no_mangle]
10021027
pub extern "C" fn hone_editor_set_find_highlights(view: *mut EditorView, json: *const u8) {

native/harmonyos/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,31 @@ pub extern "C" fn hone_editor_set_fold_ranges(_view: *mut EditorView, _packed_da
464464
#[no_mangle]
465465
pub extern "C" fn hone_editor_clear_diagnostics(_view: *mut EditorView) {}
466466

467+
// === gh#1 host bridge + gh#2 range diagnostics (link-resolving stubs) ===
468+
// As with every other symbol in this crate, these exist purely so Perry apps
469+
// embedding @honeide/editor load on HarmonyOS. Real rendering + the host
470+
// text/cursor bridge land with the ArkTS-side renderer.
471+
#[no_mangle]
472+
pub extern "C" fn hone_editor_set_range_diagnostics(_view: *mut EditorView, _json: *const u8) {}
473+
#[no_mangle]
474+
pub extern "C" fn hone_editor_clear_range_diagnostics(_view: *mut EditorView) {}
475+
#[no_mangle]
476+
pub extern "C" fn hone_editor_focus(_view: *mut EditorView) {}
477+
#[no_mangle]
478+
pub extern "C" fn hone_editor_set_buffer_text(_view: *mut EditorView, _text: *const u8) {}
479+
#[no_mangle]
480+
pub extern "C" fn hone_editor_has_pending_set_text(_view: *mut EditorView) -> f64 { 0.0 }
481+
#[no_mangle]
482+
pub extern "C" fn hone_editor_take_pending_set_text(_view: *mut EditorView) -> i64 { 0 }
483+
#[no_mangle]
484+
pub extern "C" fn hone_editor_has_pending_cursor(_view: *mut EditorView) -> f64 { 0.0 }
485+
#[no_mangle]
486+
pub extern "C" fn hone_editor_pending_cursor_line(_view: *mut EditorView) -> f64 { 0.0 }
487+
#[no_mangle]
488+
pub extern "C" fn hone_editor_pending_cursor_col(_view: *mut EditorView) -> f64 { 0.0 }
489+
#[no_mangle]
490+
pub extern "C" fn hone_editor_clear_pending_cursor(_view: *mut EditorView) {}
491+
467492
#[no_mangle]
468493
pub extern "C" fn hone_editor_poll_touch(_view: *mut EditorView) -> f64 {
469494
0.0

native/ios/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,31 @@ pub extern "C" fn hone_editor_set_breakpoints(_view: *mut EditorView, _data: f64
4242
#[no_mangle]
4343
pub extern "C" fn hone_editor_set_fold_ranges(_view: *mut EditorView, _data: f64) {}
4444

45+
// === gh#1 host bridge + gh#2 range diagnostics (link-resolving stubs) ===
46+
// Range-diagnostic squiggles and the host text/cursor bridge are implemented
47+
// on macOS + web; these exist so Perry apps embedding the editor link cleanly.
48+
// (iOS already stubs line diagnostics above — same status.)
49+
#[no_mangle]
50+
pub extern "C" fn hone_editor_set_range_diagnostics(_view: *mut EditorView, _json: i64) {}
51+
#[no_mangle]
52+
pub extern "C" fn hone_editor_clear_range_diagnostics(_view: *mut EditorView) {}
53+
#[no_mangle]
54+
pub extern "C" fn hone_editor_focus(_view: *mut EditorView) {}
55+
#[no_mangle]
56+
pub extern "C" fn hone_editor_set_buffer_text(_view: *mut EditorView, _text: i64) {}
57+
#[no_mangle]
58+
pub extern "C" fn hone_editor_has_pending_set_text(_view: *mut EditorView) -> f64 { 0.0 }
59+
#[no_mangle]
60+
pub extern "C" fn hone_editor_take_pending_set_text(_view: *mut EditorView) -> i64 { 0 }
61+
#[no_mangle]
62+
pub extern "C" fn hone_editor_has_pending_cursor(_view: *mut EditorView) -> f64 { 0.0 }
63+
#[no_mangle]
64+
pub extern "C" fn hone_editor_pending_cursor_line(_view: *mut EditorView) -> f64 { 0.0 }
65+
#[no_mangle]
66+
pub extern "C" fn hone_editor_pending_cursor_col(_view: *mut EditorView) -> f64 { 0.0 }
67+
#[no_mangle]
68+
pub extern "C" fn hone_editor_clear_pending_cursor(_view: *mut EditorView) {}
69+
4570
/// Poll active touch — returns scroll delta Y since last poll.
4671
/// touchesMoved never fires in Perry's UIView embedding, so TypeScript
4772
/// polls the saved UITouch's current position via setInterval.

native/linux/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,31 @@ pub extern "C" fn hone_editor_set_fold_ranges(
637637
// Stub — fold indicators not yet implemented on Linux
638638
}
639639

640+
// === gh#1 host bridge + gh#2 range diagnostics (link-resolving stubs) ===
641+
// Range-diagnostic squiggles and the host text/cursor bridge are implemented
642+
// on macOS + web; these exist so Perry apps embedding the editor link cleanly.
643+
// (Linux already stubs line diagnostics above — same status.)
644+
#[no_mangle]
645+
pub extern "C" fn hone_editor_set_range_diagnostics(_view: *mut EditorView, _json: i64) {}
646+
#[no_mangle]
647+
pub extern "C" fn hone_editor_clear_range_diagnostics(_view: *mut EditorView) {}
648+
#[no_mangle]
649+
pub extern "C" fn hone_editor_focus(_view: *mut EditorView) {}
650+
#[no_mangle]
651+
pub extern "C" fn hone_editor_set_buffer_text(_view: *mut EditorView, _text: i64) {}
652+
#[no_mangle]
653+
pub extern "C" fn hone_editor_has_pending_set_text(_view: *mut EditorView) -> f64 { 0.0 }
654+
#[no_mangle]
655+
pub extern "C" fn hone_editor_take_pending_set_text(_view: *mut EditorView) -> i64 { 0 }
656+
#[no_mangle]
657+
pub extern "C" fn hone_editor_has_pending_cursor(_view: *mut EditorView) -> f64 { 0.0 }
658+
#[no_mangle]
659+
pub extern "C" fn hone_editor_pending_cursor_line(_view: *mut EditorView) -> f64 { 0.0 }
660+
#[no_mangle]
661+
pub extern "C" fn hone_editor_pending_cursor_col(_view: *mut EditorView) -> f64 { 0.0 }
662+
#[no_mangle]
663+
pub extern "C" fn hone_editor_clear_pending_cursor(_view: *mut EditorView) {}
664+
640665
/// Copy text to clipboard (called by TypeScript).
641666
/// Perry params: ["i64", "i64"]
642667
#[no_mangle]

0 commit comments

Comments
 (0)