Editable view-source pane with live KDL highlighting - #38
Merged
Conversation
Half-shipped. The structural plumbing works (parse + apply, position preservation, zoom-to-fit), the live highlighting during edit doesn't yet. Rust side. New WorkflowController::apply_kdl_source(current_json, kdl) qinvokable. Parses the KDL through the existing decode path, preserves the workflow id from the supplied current_json, swaps the in-memory workflow on success via set_workflow_json. Returns "" on success or the parse error message on failure so the pane can render an "unparsed" chip without tearing down the canvas. A new preserve_step_ids helper walks old + new step lists in order and copies old.id onto new.id where the action variant matches, recursing into repeat / conditional inner steps. Uses std::mem::discriminant so adding a new Action variant needs no edit here. Without this, every step looked new on re-parse, the positions sidecar (keyed by step.id) lost every mapping, and the canvas re-flowed the whole graph on each edit. QML side. ViewSourcePane drops readOnly when editable is true, gates the upstream rebind on a _editing flag (true on first printable key, false on focus-loss) so user typing doesn't get its cursor stomped by every canonical refresh, debounces apply at 600ms and re-highlight at 150ms, shows an "● unparsed" chip in the header on parse failure (tooltip carries the message), intercepts Tab to insert four spaces (matches encoder), sets tabStopDistance to four space widths as a fallback for any \t that pastes in. WorkflowPage wires editable: !fragmentMode, workflowController: wfCtrl, parseError: root.sourceParseError; the onApplyRequested handler calls apply_kdl_source, fires _scheduleSave on success, clears parseError when sourceKdl moves on (canvas mutation or successful apply), and schedules canvasView._zoomToFit so the camera frames any newly-placed cards. Open issues (matthew dogfooded, called these out before relogging): - Live syntax highlighting goes wonky as you type or delete chars, stays wonky until pane close + reopen forces a fresh render. Per-keystroke re-tokenize fighting RichText cursor + document model. Two reasonable next moves: drop highlighting entirely while _editing is true (PlainText for the duration, RichText on focus-loss), or land a proper Rust-side QSyntaxHighlighter via cxx-qt against the body's QQuickTextDocument. - Tab key inserts nothing. Keys.onPressed handler runs at default AfterItem priority, Qt's Tab focus-traversal fires first and steals the event. Fix: Keys.priority: Keys.BeforeItem on the TextEdit. Both deferred to next session. ACTIVE.md carries the handoff. Refs: WFLOW-66
The tab handler in ViewSourcePane.qml was at default priority, so Qt's focus traversal grabbed the key before body.insert could fire. Keys.priority: Keys.BeforeItem flips the order and tab now drops in 4 spaces like the encoder expects. The live re-highlight was the bigger one. Per keystroke we re-tokenized, rebuilt HTML, swapped body.text, and restored the cursor. RichText's document model fights the cursor on every rebuild and the colors drifted away from the underlying text. v1 fix is to switch textFormat to PlainText while the user is typing (so highlighting freezes) and snap back to RichText on focus-loss when the canonical-source Binding re-fires with fresh tokens. The pane stages the rendered plain text into body.text on the _editing→true transition so the HTML markup doesn't render literally for a frame. The long-term fix is a Rust-side QSyntaxHighlighter attached to body's QQuickTextDocument via cxx-qt; that's a separate piece of work. Refs: WFLOW-66
The two known bugs (tab insertion, live-highlight drift) are fixed in 2f9d6ab but untested on the running app. Doc now points the next session at the dogfood steps and the still-open long-term QSyntaxHighlighter work. Refs: WFLOW-66
…ystroke The textFormat flip in 2f9d6ab made the bug worse. Setting body.text=plain in RichText mode causes Qt to round-trip the input through its default HTML serialization (DOCTYPE, head/style, p tags). The follow-up switch to PlainText then renders that serialization literally, so the moment you typed a character the entire source pane was replaced with Qt's HTML preamble. New approach: stay in RichText the whole edit. on_EditingChanged swaps the colored HTML for a no-spans <pre> wrapping (same plain text, default color, no enclosing colored span at the cursor), and the Binding on text still releases during _editing. On focus-loss the canonical highlighted HTML snaps back. Refs: WFLOW-66
… put ef9ad9b's no-spans swap was doing what I told it to (drop highlight on edit-start) but the visual was wrong: the moment you touched the buffer, the whole pane went default-color and stayed that way until focus-loss. The interim now removes the on_EditingChanged snapshot entirely. Existing colored spans stay put while typing; new chars inherit whatever cursor format Qt picks (sometimes colored, sometimes default, depending on where the cursor was). The Binding on text still releases during _editing so the upstream rebind doesn't fight, and on focus-loss the canonical highlighted HTML snaps back. This is a holding pattern. The proper fix is a C++ QSyntaxHighlighter attached to body.textDocument; setFormat is non-destructive to the cursor, so we can re-highlight on every keystroke without the document rebuild that started this whole thread. Next. Refs: WFLOW-66
Took the proper fix instead of staying on the holding pattern. The view-source pane now drives highlighting through a hand-written QSyntaxHighlighter subclass (cpp/kdl_syntax_highlighter.h) attached to the body TextEdit's QTextDocument. setFormat() applies char-format ranges to the existing document without rebuilding it, so the cursor stays put across keystrokes and we can re-tokenize on every textChanged without the document-rebuild that started this whole thread.
How the pieces fit together. The body TextEdit is plain text now (no more RichText/HTML pipeline, no more _buildHtml). On every keystroke during edit, ViewSourcePane calls wfCtrl.tokenize_kdl on the local buffer and feeds the resulting span JSON to the highlighter's spansJson property. The highlighter parses the JSON once, then highlightBlock filters the document-relative spans down to each block and applies a QTextCharFormat per match (foreground from a Theme-driven colors map, italic for comments). Outside the edit burst the same property tracks the parent's pre-computed kdlSpansJson so a canvas-side re-encode lands cleanly.
The C++ side lands as two headers in cpp/, registered with the build via .cpp_file(). cxx-qt-build auto-adds the parent dir to the cc include path for moc'd headers, so kdl_qml_register.h can include the highlighter directly. A tiny bridge in src/bridge/kdl_highlight.rs exposes a single register_kdl_qml_types() function that main.rs calls before QQmlApplicationEngine::load, which is how `import Wflow 1.0; KdlSyntaxHighlighter {}` resolves at QML parse time alongside the cxx-qt-registered types.
The interim "leave colors alone" fallback that landed in 33ce134 is gone; this replaces it.
Refs: WFLOW-66
…ther Two bugs surfaced in dogfooding 7a2eb6f. Both fixed here. Once QSyntaxHighlighter is putting QTextCharFormats on the document, the QML TextEdit's `color` property stops acting as a render-time fallback for unformatted ranges. Chars not covered by a token span ended up with an unset foreground and rendered effectively invisible (you'd see colored tokens with gaps where the rest of the text should be). The highlighter now exposes a `defaultColor` property, primes every block with it via setFormat(0, len, baseFmt) before applying per-token spans, and ViewSourcePane drives it from Theme.text. The bigger one was the body content vanishing the moment you typed. Qt's Binding type defaults to restoreMode: RestoreBindingOrValue, which means when `when` flips false it doesn't just stop driving — it actively restores the target property to its pre-binding value. For TextEdit.text that pre-value is the empty string, so the whole document wiped on the first keystroke and the typed char landed alone in an empty buffer (which the apply path then dutifully tried to parse as a workflow, surfacing "unknown top-level node `ff`" in the chip). The Binding now sets restoreMode: Binding.RestoreNone so it keeps the last binding-driven value when it releases, and the keystroke inserts into the canonical text the user was reading. Refs: WFLOW-66
Editable view-source pane is the headline; the rest is housekeeping that piled up since 1.2.0 (flat top app bar, KDL clipboard copy/paste, drop a .kdl on the canvas to import its steps, explore drawer rendering the catalog trail during the loading window). Refs: WFLOW-66
The view-source pane is new for any user landing on a tagged release: the read-only + syntax-highlighted parts shipped to main directly in earlier sessions but never got cut into a published version. The 1.3.0 notes assumed prior knowledge of the pane; they don't anymore. Combined the editable-pane and live-highlighting bullets into one feature description in CHANGELOG.md, and rewrote the opening of docs/release-notes/v1.3.0.md to introduce the pane before describing the editing behavior. Refs: WFLOW-66
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.
1.3.0 adds a view-source pane to the workflow editor. Click
</> Sourceand a panel slides in from the right showing the current workflow as KDL, syntax-highlighted, re-encoded live as you change the canvas. You can edit the KDL directly: type into it and the canvas updates from your changes.How editing works: type into the KDL, the canvas updates on a 600ms debounce. Tab inserts 4 spaces. Broken KDL surfaces an "unparsed" chip in the header (with the parse error in a tooltip), and the canvas holds at last-good state until you fix the source or click out. Card positions survive a round-trip so adding or removing a line doesn't reshuffle the layout.
Live syntax highlighting took four tries. Rebuilding the highlighted HTML per keystroke fights Qt's RichText document model and drifts the cursor, and the obvious workarounds I tried (flipping textFormat, swapping in a no-spans HTML, switching to PlainText) all broke in different ways. I ended up writing a C++
QSyntaxHighlighterincpp/kdl_syntax_highlighter.hattached to the body'sQTextDocument; it applies token formats to the existing document without rebuilding it, so the cursor stays put.Worth flagging: the read-only and syntax-highlighted parts of the pane landed on main directly in earlier sessions, as did several other features in 1.3.0. That was a workflow miss; from now on nothing reaches main without a PR. The rest of what's new since 1.2.0:
.kdlfile on the canvas to import its steps.Bumps Cargo.toml to 1.3.0 with a CHANGELOG entry and a release note at
docs/release-notes/v1.3.0.md.