feat(overlay): route TextKit-estimated carets to a popup anchored at the caret#694
Merged
Merged
Conversation
…the caret For `.estimated` hosts Cotabby's hidden-TextKit repair produces a confident caret estimate (`.layoutEstimated`). Previously that rendered inline ghost text. Inline glyphs on an estimate get scrutinized against the host's own text and read as misplaced, so route `.layoutEstimated` to the popup card instead, but anchor the card to the estimated caret (one line below it) rather than the whole field rect, so the popup tracks the cursor TextKit located. Adds a `.caretLayoutEstimated` MirrorReason to keep this distinct from the no-usable-caret `.caretGeometryEstimated` case, which still anchors to the field.
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
For
.estimatedhosts (no usable AX caret), Cotabby's hidden-TextKit layout repair produces a confident caret estimate, surfaced as.layoutEstimatedquality. Previously that rendered inline ghost text. Inline glyphs drawn on an estimate get scrutinized against the host's own text and read as misplaced, so this routes.layoutEstimatedto the popup card instead — but anchors the card to the estimated caret (one line below it) rather than the whole field rect, so the popup tracks the cursor TextKit located instead of floating at the bottom of the document.Behavior by caret quality under Auto is now:
.exact/.derived.estimated(no caret).layoutEstimated(TextKit repair)A new
.caretLayoutEstimatedMirrorReasonkeeps this distinct from the no-usable-caret.caretGeometryEstimatedcase (which still anchors to the field rect, since its caret can't be trusted). The mid-line override is unaffected:.layoutEstimatedis already a card, so the override (which only upgrades inline results) no longer relabels it.Validation
Updated the two policy tests that pinned
.layoutEstimated→ inline, and added aMirrorOverlayLayouttest asserting the new reason anchors below the estimated caret line (not the field bottom).Linked issues
Risk / rollout notes
.estimated/TextKit-repaired hosts only (e.g. some Electron/web editors): they now show the popup card instead of inline ghost text. Hosts on.exact/.derivedare untouched.MirrorReasoncase isString-backed so the debuglabelandOverlayState.detailpick it up with no other edits.Greptile Summary
This PR routes
.layoutEstimated-quality carets (produced by Cotabby's hidden-TextKit layout repair) to the popup card instead of inline ghost text, and anchors that popup one line below the estimated caret position rather than at the field rectangle used by the no-caret.estimatedpath.CompletionRenderModegains a newMirrorReason.caretLayoutEstimatedcase, keeping the TextKit-repair path distinct from the no-caret.caretGeometryEstimatedpath in diagnostics and layout.CompletionRenderModePolicyreplaces a ternary with an exhaustiveswitchoncaretQuality;.layoutEstimatednow returns.mirror(reason: .caretLayoutEstimated), and the mid-line promotion correctly no-ops for already-mirrored results.MirrorOverlayLayout.computeAnchorTopYadds acaretLayoutEstimatedbranch that anchors to the estimated caret rect withcaretFallbackVerticalOffset(22 pt) rather than the tight 8 pt gap used for fully trusted carets, with field-rect and last-resort fallbacks mirroring the existing patterns.Confidence Score: 4/5
Safe to merge; the change only affects hosts whose caret quality resolves to
.layoutEstimated(TextKit-repaired Electron/web editors) and is a pure render-mode routing change with no persistence or schema impact.The logic is sound and the new tests cover the primary happy-path behavior. Two small gaps keep this from a clean score: the
computeAnchorTopYdoc comment does not document the new case, and there is no test pinningalwaysInline+.layoutEstimatedquality — a combination that previously shared behavior with.autoand is now subtly distinct.MirrorOverlayLayout.swift— thecomputeAnchorTopYheader comment should be extended to describe the new case;CompletionRenderModePolicyTests.swift— worth adding a test for thealwaysInline+.layoutEstimatedcombination.Important Files Changed
caretLayoutEstimatedtoMirrorReasonwith a clear doc comment; no logic changes, clean addition.caretQuality;.layoutEstimatednow returns.mirror(reason: .caretLayoutEstimated)instead of.inline. Logic is correct and the mid-line guard correctly skips already-mirror results.caretLayoutEstimatedbranch tocomputeAnchorTopY; anchors below estimated caret withcaretFallbackVerticalOffset. The doc comment forcomputeAnchorTopYdoes not document the new case, leaving an incomplete description of the anchor-selection logic..layoutEstimatedpolicy behavior; missing coverage foralwaysInline+.layoutEstimatedquality combination.test_make_layoutEstimatedReasonAnchorsToCaretLine_notInputFieldwith correct AppKit-coordinate assertions confirming the popup tracks the estimated caret rather than the field bottom.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[caretQuality] --> B{switch} B -- .exact / .derived --> C[.inline] C --> D{isCaretAtEndOfLine?} D -- yes --> E[render inline] D -- no --> F[.mirror reason: .caretMidLine] B -- .estimated --> G[.mirror reason: .caretGeometryEstimated] G --> H[anchorTopY = inputFrame.minY - anchorGap] B -- .layoutEstimated --> I[.mirror reason: .caretLayoutEstimated] I --> J{caretRect.isEmpty?} J -- no --> K[anchorTopY = caretRect.minY - caretFallbackVerticalOffset] J -- yes --> L{inputFrame available?} L -- yes --> M[anchorTopY = inputFrame.minY - anchorGap] L -- no --> N[anchorTopY = caretRect.minY - caretFallbackVerticalOffset]Comments Outside Diff (2)
Cotabby/Support/MirrorOverlayLayout.swift, line 152-165 (link).caretLayoutEstimatedThe
computeAnchorTopYheader documents only.caretGeometryEstimatedand the trusted-caret reasons (.userPreference,.perAppOverride,.caretMidLine). A reader consulting just this comment to understand the anchor strategy gets no hint that.caretLayoutEstimatedexists or that it anchors to the estimated caret instead of the field rect. Consider adding a third bullet to the existing- .caretGeometryEstimated/- .userPreference …list describing the new case and why it usescaretFallbackVerticalOffsetrather thananchorGap.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
CotabbyTests/CompletionRenderModePolicyTests.swift, line 43-53 (link)alwaysInline+.layoutEstimatedqualityThe existing
test_alwaysInline_keepsInlineEvenForEstimatedGeometrycovers.estimatedcaret quality, but not.layoutEstimated. Before this PR,.layoutEstimatedfell through to.inlinein.automode, making it share behavior withalwaysInline. Now that.autoroutes.layoutEstimatedto.mirror, it's worth pinning explicitly thatalwaysInlinestill returns.inlinefor.layoutEstimatedquality, so a future change to thealwaysInlinebranch can't silently regress it.Reviews (1): Last reviewed commit: "feat(overlay): route TextKit-estimated c..." | Re-trigger Greptile