editor: auto-close brackets and smart indent on Enter - #3018
Conversation
da8c307 to
cb15538
Compare
huacnlee
left a comment
There was a problem hiding this comment.
Please make the editing rules configurable at the Base boundary:
Editorbelongs togpui-base, which must remain independent of tree-sitter. Expose bracket pairs and their auto-close/newline behavior as Editor parameters instead of hard-coding characters in the editing engine.gpui-componentshould own language configuration and tree-sitter integration, supplying those rules and adapting syntax-context/indentation information to a parser-independent Base interface. Auto-close policy is language metadata; bracket/indent queries provide syntax information. Base should not consume tree-sitter-specific configuration or queries.
Both conveniences fit the editor, but the current universal character rules are not suitable for its language-independent API. The following correctness and performance issues also need addressing (| denotes the cursor):
- P1 — Unicode panic (
crates/base/src/input/editor/mod.rs:243): typing a quote after中|panics with “byte index is not on a char boundary”.range.startis already the insertion boundary; subtracting one byte can split a Unicode character and also reads the wrong preceding ASCII character. Use a Unicode-safe previous-character lookup. For example, typing an apostrophe aftera|also incorrectly produces two apostrophes. - P1 — Unrelated text deletion (
crates/base/src/input/base/state.rs:1646,crates/base/src/input/editor/mod.rs:224): UTF-8 byte ranges are passed toreplace_text_in_range_silent, which accepts UTF-16 ranges. Backspace at中(|)abcproduces中()c, deletingab; typing)there produces中())bc, deletinga. Convert the ranges or use the byte-oriented editing primitive. Cover both paths with CJK and non-BMP prefixes. - P2 — Closing quotes do not skip (
crates/base/src/input/editor/mod.rs:215-220): typing a double quote at"hello|"produces"hello"". The word guard for opening quotes must not suppress recognition of a closing quote. Test ordinary strings, contractions and escapes. - P2 — Linear work per keypress (
crates/base/src/input/base/state.rs:1635,1784and the quote lookups):slice(..cursor).chars().last()traverses the whole document prefix. An isolated optimized Ropey benchmark measured approximately 2.5 ms at 1 MB and 25 ms at 10 MB for one lookup, before other editor work. Seek to the cursor and traverse backwards. - P2 — Smart-indent opt-out is ignored for pairs (
crates/base/src/input/base/state.rs:1779-1810): afterset_smart_indent(false), Enter at{|}still adds an extra indent level. The branch only checksauto_close. Respect the independent options and test their combinations.
Validation on cb155386c6bdee10df9a16e6a4086f9bc5327a3b: the seven added tests and the input-filtered suite (227 tests) pass, but six additional temporary GPUI context reproductions fail on the cases above. No real-window or local macOS/Windows validation was performed. The separate Docs CI failure concerns a missing generated UI-testing page and is not attributed to these three changed files.
Security: BLOCK for affected text-input availability/integrity, based on the reproduced Unicode panic and deletion of unrelated text; no remote exploit or native memory corruption is claimed.
Please address the findings above, then request my review again.
Summary of ChangesArchitecture & Policy Refactoring
Key Fixes & Correctness Improvements
Testing & Verification
|
63f58d6 to
168dde4
Compare
huacnlee
left a comment
There was a problem hiding this comment.
Re-reviewed 168dde418f97a93958819ab4b85f6d214c7b1832. The previous Unicode panic, byte/UTF-16 deletion bugs, ordinary closing-quote skip, prefix scans, and smart-indent opt-out have been addressed. The following blockers remain (| denotes the cursor):
-
P2 — Unconditional notification during rendering (
crates/base/src/input/base/state.rs:8223-8225, called fromcrates/component/src/input/input.rs:384): every Editor render synchronizes rules and callscx.notify(), even when the rules are identical or customization prevents any update. Three identical sync calls produced three notifications in a context test. This can sustain redundant rendering and observer work while idle; native-window CPU impact was not measured. Make synchronization idempotent and avoid notifying during unchanged render-time synchronization. Cover unchanged and customized rules. -
P2 — Skipping a closer corrupts undo history (
crates/base/src/input/editor/mod.rs:222-227): at(|), type)and then Undo; the text becomes()). The initial insertion and subsequent silent deletion are separately recorded: silent replacement suppresses typing hooks, not history. Handle skipping as a cursor move, or a coherent operation that cannot expose the transient duplicate through Undo/Redo. Add history and cursor-position regressions for skip and pair insertion. -
P2 — Individual option overrides freeze all language rules (
crates/base/src/input/base/state.rs:8257-8262, also the auto-close setters):EditorState::new(...).language("text").smart_indent(false)marks the entire rule set customized before Component synchronization. The plain-text rules are consequently ignored, and typing(produces(). This was reproduced through the actualTextInputState::sync_edit_rulespath. Track flag overrides independently of language pairs/triggers; reserve suppression of whole-table synchronization for explicitedit_rulescustomization. Test builder/setter order and language changes. -
P2 — Escaped quotes incorrectly skip the string terminator (
crates/base/src/input/editor/mod.rs:219-228): starting with"hello\|", typing"should produce"hello\"|"; instead it produces"hello\"|, leaving the only final quote escaped and the string unterminated. The unconditional closer branch does not check escape or syntax context. Apply language-appropriate context before opening/skipping pairs; cover odd/even backslashes, strings and comments. -
P2 — The prior language/context boundary request is only partially implemented (
crates/component/src/input/edit_rules.rs:8-17,crates/base/src/input/editor/edit_rules.rs:16-24): exposingEditRulesis progress, but every non-plain language still gets the same Base default table. Component supplies no actual language-specific policy or syntax-context adaptation, and the Base interface cannot express per-pair newline behavior or context decisions. Rust, JSON, HTML and Python therefore still share colon indentation and quote policies. Complete the previously requested architecture: Component owns language metadata and tree-sitter integration, adapting syntax information into parser-independent Base interfaces; Base must remain independent of tree-sitter. Exercise the real language/provider synchronization path, not only the lookup function.
The two conveniences can reduce manual code editing, but require maintained language rules and context handling. Recommendation: retain them only with those boundaries completed. Product decision remains pending separately from this technical review.
Validation: the unmodified Base input suite passed (233 tests); additional temporary Base context reproductions passed 8 and failed 4 on the cases above. The Component input suite passed 29 and failed the additional plain-text/option integration reproduction. Formatting and diff checks passed. No native-window, local macOS/Windows or wasm validation was performed. No new latency benchmark was run.
Security: PASS, scoped to the reviewed Unicode/indexing boundaries and absence of new external I/O; CJK and non-BMP skip/deletion reproductions preserve neighboring text. The undo and escaped-quote defects remain functional correctness blockers. This is not a complete editor security audit.
Please address the findings above, then request my review again.
7f27dc0 to
f0a7f66
Compare
1. Notification Churn Optimization
2. Undo History Corruption Fix
3. Granular Flag Customization & Synchronization
4. Escaped Quote Handling
5. Language Boundary & Tree-Sitter Integration
|
Decide pairing before insertion and record paired characters as one edit. Preserve literal and escaped delimiters, final cursor positions, and syntax cache validity. Avoid syntax queries for ordinary typing and construct language providers lazily. Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
Define structural brackets, conditional string pairs, following-character limits, and compiled indentation patterns as language configuration. Keep auto-close and smart-indent preferences independent and allow application overrides to return to current language defaults. Preserve generated delimiter identity through edits and undo transactions. Document the supported API and require non-exhaustive public records in the coding guides. Co-authored-by: Codex <codex@openai.com>
Closes #3015
Description
The code editor is missing two standard editing behaviors:
{,(,[or:keeps the same indent instead of adding a level. Entering a newline between a bracket pair ({|}) does not split onto three lines.(,[,{,"or'inserts only the opener. Typing a closer that already follows duplicates it instead of skipping past. Backspace between an empty pair deletes one char instead of both.This adds both, enabled by default, with opt-outs following the existing
folding/line_numberbuilder + setter pattern:.auto_close(bool)/set_auto_close(...).smart_indent(bool)/set_smart_indent(...)Quote pairing/skipping is guarded against word characters so contractions like
don'tare unaffected. Pair deletion only fires on truly empty pairs.Screenshot
Screen.Recording.2026-09-08.183531.mp4
Break Changes
None. Both features default to enabled; existing callers get the new behavior automatically and can opt out per-editor.
How to Test
cargo run -p example-editor
Checklist
cargo runfor story tests related to the changes.