Add CodeMirror editor signals (selection, focus, viewport, geometry) and reveal_line#5984
Draft
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Draft
Add CodeMirror editor signals (selection, focus, viewport, geometry) and reveal_line#5984Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Conversation
Three small editor-event primitives requested by host applications that embed CodeMirror as an in-app code editor: - `on_cursor_line` (typed `CodeMirrorCursorLineEventArguments`, 1-indexed line number, 30 ms debounce on the JS side) reports cursor-line changes. The debounce is short enough to feel immediate when arrow- keying through a file but long enough to coalesce bursts during multi-line selection drags. - `on_save` (typed `CodeMirrorSaveEventArguments`) fires on Ctrl/Cmd+S inside the editor and suppresses the browser's default save dialog. The Mod-s keymap binding is only installed when the host opts in via the `save-shortcut-enabled` prop, so editors without an `on_save` handler keep the browser's default behavior. - `reveal_line(line_number)` scrolls a 1-indexed line into view via CodeMirror's `EditorView.scrollIntoView` effect. Tests dispatch CM6 selection transactions directly rather than relying on Selenium keystroke focus timing. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the narrow `on_cursor_line` and `on_save` primitives added in
the prior commit with two generic mechanisms (one of which lands here,
the other deferred to a follow-up PR). Both removals are unreleased.
ViewUpdate signal dispatcher (this PR):
- Single JS `ViewPlugin` (`updateDispatcher`) reads four CM6 ViewUpdate
flags and emits one event per signal: `selection-change` (line +
column), `focus-change` (focused), `viewport-change` (visible line
range), `geometry-change` (width / height / content height). Each
signal has a dedicated `<signal>-tracking-enabled` prop so the
dispatcher bails early when the host has not subscribed; per-signal
`<signal>-debounce-ms` props (defaults 30 / 0 / 100 / 100) are read
fresh on each emit so runtime overrides apply on the next event
without a Vue watch. Per-signal dedupe of the last emitted payload
drops redundant traffic. Timers live on the plugin instance and
`destroy()` clears them, removing the leak-prone `_cursorTimer` on
the Vue component.
- Public Python API: four typed `on_*_change` handlers + corresponding
event-args dataclasses. Both the constructor kwargs and the methods
accept `Handler | CodeMirrorHandlerSpec`; per-registration overrides
go through a single `ui.codemirror.handler(callback, debounce_ms=...)`
factory (a static method exposing the spec dataclass) so constructor
and method surfaces stay shape-identical.
Generic keybindings API (separate follow-up PR): replaces `on_save` with
`keybindings={...}` and `editor.on_keybinding(key, handler)`. No
JS-plugin overlap with the dispatcher landed here (different CM6
primitives — `ViewPlugin` vs `keymap`); the only shared piece is
`CodeMirrorHandlerSpec`, which the keybindings PR extends with
`prevent_default`. Brief no-save-binding gap is invisible to users
since `on_save` is also unreleased.
`reveal_line` is unchanged. The new `viewport-change` signal closes
its previously missing feedback channel — a host can now confirm a
revealed line landed in the visible range.
Tests cover all four signals (with mid-line cursor positions to
exercise the column field, viewport range after `reveal_line`, focus
toggles via JS, geometry under container resize), plus
`test_no_handler_no_traffic` (verifies subscription gating prevents
emit when nothing is listening) and `test_debounce_override` (verifies
the spec factory's debounce_ms is honored end-to-end).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Hosts that embed
ui.codemirroras an in-app editor often need feedback about the editor's state — cursor line / column, focus, the visible line range, and editor geometry. None of this is exposed through the Python API today, and there's no programmatic way to scroll a specific line into view.Implementation
This PR adds:
reveal_line(line_number)— scrolls a 1-indexed line into view viaCM.EditorView.scrollIntoView.on_selection_change(line, column),on_focus_change(focused),on_viewport_change(from_line, to_line),on_geometry_change(width, height, content_height).ui.codemirror.handler(callback, debounce_ms=...)factory for per-registration debounce overrides. Both constructor kwargs and methods accept either a bare callable or a wrapped spec, so the two surfaces stay shape-identical.A single JS
ViewPluginreads the four CM6ViewUpdateflags and emits per-signal events. Each signal has its own enable prop (auto-flipped when a handler is registered) and debounce-ms prop (defaults 30 / 0 / 100 / 100); when nothing is subscribed the dispatcher bails before computing a payload, so unsubscribed signals cost nothing on the wire. Per-signal dedupe drops redundant emits; debounce timers live on the plugin instance anddestroy()clears them.Tests cover all four signals, the
reveal_line→ viewport feedback loop, subscription gating (no traffic when nothing's listening), and the debounce-override path end-to-end. Selection / focus events are driven viaeditor.dispatch({...})in JS rather than Selenium keystrokes to avoid focus-timing flakiness.Progress