feat: auto-close brackets and quotes#336
Merged
bartekplus merged 7 commits intomasterfrom Mar 22, 2026
Merged
Conversation
Add a new grammar rule that automatically inserts matching closing
brackets/quotes when typing opening ones, positions the cursor between
them, and supports smart overtype (skip-over) for closing characters.
Supported pairs: (), [], {}, '', "", ``, <>, «»
Smart guards: apostrophe suppression, word-char guards for < and >
Extends GrammarEdit with cursorOffset for mid-replacement cursor
positioning, with full support in edit merging and context application.
Off by default (advanced tier, priority 134).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
For symmetric quotes (", ', `), the engine's steady-state loop would
oscillate: auto-close → overtype → auto-close → overtype → auto-close,
producing """" (4 chars) instead of "" (2 chars).
Root cause: after auto-close places cursor between quotes ("|"), the
engine re-evaluates and overtype fires because the opening quote looks
like a closing quote. This undoes the auto-close, and the cycle repeats.
Fix: only allow overtype for symmetric quotes when preceded by a word
character (genuine closing-quote context like: hello"|). When preceded
by non-word or start-of-string, block overtype so auto-close sticks.
Adds engine integration tests to verify no oscillation for all quote types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues caused the cursor to land at the end of the replacement instead of in the middle (e.g., "(|)") on contenteditable editors like Facebook: 1. Host-handled path: when the host editor (Draft.js, Lexical, etc.) handles the beforeInput event and applies the text change itself, FluentTyper never called setCaret — cursor was wherever the host put it. Now setCaret is called after host-handled edits. 2. DOM fallback path: setCaret was called BEFORE dispatchReplacementInput. Host editors handle the input event synchronously and override the cursor to end of replacement. Swapped the order so setCaret runs after the input event, giving our position the final word. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
React-based editors (Facebook/Lexical, Reddit/Slate) reconcile the DOM asynchronously via microtasks after handling beforeInput events, overriding any cursor position set synchronously. Add a deferred setCaret via requestAnimationFrame that runs after framework reconciliation completes, ensuring the cursor lands in the middle of auto-closed pairs on these editors. Only triggers when cursorOffset is set (bracket-close edits), so normal grammar edits are unaffected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix two issues with cursor placement after auto-bracket-close on React-based editors like Facebook (Lexical) and Reddit (Slate): 1. The deferred cursor fix was scheduled AFTER the didMutateDom early return. React editors handle beforeinput by calling preventDefault() and reconciling async via microtask, so didMutateDom is false at check time. Move the fix before the early return with text validation. 2. Selection.modify() from the content script's isolated world doesn't trigger native selectionchange events that host editors listen for. Use the existing main-world bridge to execute cursor movement in the page context, where it triggers proper event propagation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The setCaret/dispatchReplacementInput swap broke Quill predictions on Firefox. Calling setCaret after dispatching the input event interferes with Quill's internal selection tracking on Firefox, causing prediction suggestions to stop appearing. Revert setCaret to its original position (before dispatchReplacementInput) and remove the redundant setCaret in the host-handled beforeinput path. Cursor positioning for auto-bracket-close is now handled entirely by the deferred main-world bridge, which runs after editor reconciliation. Co-Authored-By: Claude Opus 4.6 <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.
Summary
Closes #335
AutoBracketCloseRulegrammar rule that auto-inserts matching closing brackets/quotes when typing opening ones and positions the cursor between themGrammarEditwithcursorOffsetfor mid-replacement cursor positioning, with full support in edit merging (mergeSequentialGrammarEdits) and context simulation (applyGrammarEditToContext)Supported pairs:
(),[],{},'',"",``,<>, `«»`Smart guards:
',",`): suppressed after word characters (apostrophes/contractions)<: suppressed after word characters (comparison operators)>overtype: suppressed after word charactersConfiguration: Off by default, advanced safety tier, priority 134.
Files changed
types.ts— addedcursorOffsettoGrammarEdit,autoBracketClosetoGrammarRuleIdAutoBracketCloseRule.ts— new rule implementationGrammarEditSequencing.ts—cursorOffsetsupport in apply/mergeruleCatalog.ts— catalog entryruleFactory.ts— rule registrationSuggestionTextEditService.ts— cursor positioning forcursorOffsetfluenttyperI18n.ts— i18n strings (9 languages)AutoBracketCloseRule.test.ts— 27 testsTest plan
bun run lintpassesbun run format:checkpassesbun run testpasses (1521 tests, 27 new)bun run test:e2epassesbun run check:e2e:coveragepasses🤖 Generated with Claude Code