Skip to content

feat(overlay): show popup card when caret is mid-line#624

Merged
FuJacob merged 2 commits into
mainfrom
feat-midline-caret-popup
Jun 7, 2026
Merged

feat(overlay): show popup card when caret is mid-line#624
FuJacob merged 2 commits into
mainfrom
feat-midline-caret-popup

Conversation

@FuJacob
Copy link
Copy Markdown
Owner

@FuJacob FuJacob commented Jun 6, 2026

Summary

When the caret sits mid-line (real characters follow it before the next line break), inline ghost text has no home: it would render on top of the text after the caret. This promotes any such presentation to the popup (mirror) card instead, anchored to the caret line. It is the first step toward fill-in-middle (FIM) completions, which structurally need the card because a mid-line completion cannot be drawn inline.

The signal is the existing CaretLinePosition.isAtEndOfLine, so an end-of-line caret that still has later paragraphs below it stays inline (only same-line trailing characters trigger the card).

Validation

swiftlint lint --quiet <6 changed source files>
# exit 0

xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' \
  build-for-testing -derivedDataPath build/DerivedData CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
# ** TEST BUILD SUCCEEDED **

xcodebuild test-without-building ... \
  -only-testing:CotabbyTests/CompletionRenderModePolicyTests \
  -only-testing:CotabbyTests/CaretLinePositionTests \
  -only-testing:CotabbyTests/MirrorOverlayLayoutTests
# Executed 39 tests, with 0 failures

App-hosted tests were run locally with signing disabled. The 7 new policy tests cover the auto / always-inline / always-mirror / per-app-override matrix at both end-of-line and mid-line carets; existing policy, caret-position, and mirror-layout suites still pass.

Linked issues

No issue number provided. Groundwork for FIM completions.

Risk / rollout notes

  • Behavior change to an existing flow: a suggestion generated with the caret mid-line (e.g. the mid-word continuations that already fire via MidWordContinuationPolicy) previously rendered as inline ghost text overlapping the trailing characters. It now renders in the popup card. This is a net improvement for that case, but it is a visible change.
  • The mid-line rule deliberately overrides an explicit "Inline" appearance preference, because inline cannot render mid-line without overlap. If we would rather honor the pin and leave those users on inline mid-line, the override is a one-line change in CompletionRenderModePolicy.mode (gate the promotion on .auto only). Flagging for a product call.
  • No schema, settings, or project.yml / pbxproj migrations. No new files (test changes live in existing files, so XcodeGen does not drift).
  • Mirror-card anchoring for the new .caretMidLine reason reuses the trustworthy-geometry path (anchors to the caret line), so there is no new positioning math.

Greptile Summary

This PR introduces mid-line caret detection into the completion render-mode pipeline: when isCaretAtEndOfLine is false, any inline result is promoted to the popup (mirror) card via the new .caretMidLine reason, preventing ghost text from painting over trailing characters and laying the groundwork for fill-in-middle completions.

  • Adds isCaretAtEndOfLine: Bool (default true) to SuggestionOverlayGeometry and threads it from FocusedInputContext through SuggestionCoordinator+Acceptance.
  • Refactors CompletionRenderModePolicy.mode into a thin wrapper + a private preferenceMode, with the mid-line promotion expressed as a single, well-scoped guard; MirrorOverlayLayout.computeAnchorTopY groups .caretMidLine with the other trustworthy-geometry reasons and anchors to the caret rect.
  • Adds 7 new policy tests covering the auto / alwaysInline / alwaysMirror / per-app-override matrix at both end-of-line and mid-line carets, including the important "estimated geometry keeps its own reason" invariant.

Confidence Score: 5/5

Safe to merge; the behavior change (mid-line inline → popup card) is intentional, the promotion is correctly scoped to inline-only base modes, and the prior inline path is preserved by a safe default.

The policy refactor is small and well-contained: preferenceMode is the unchanged original logic, and the new wrapper adds a single, clearly-guarded promotion. The isCaretAtEndOfLine default of true ensures all pre-existing call sites keep their prior behavior. The 7 new tests cover the full preference × caret-position matrix, including the estimated-geometry invariant and the alwaysInline override case. No schema, persistence, or migration changes are involved.

No files require special attention.

Important Files Changed

Filename Overview
Cotabby/Support/CompletionRenderModePolicy.swift Public mode is now a thin wrapper that calls the renamed private preferenceMode and applies the mid-line promotion; logic is clean and all test scenarios pass.
Cotabby/Models/SuggestionModels.swift Adds isCaretAtEndOfLine: Bool = true to SuggestionOverlayGeometry; default preserves prior inline behavior for existing call sites, and replacingCaretRect correctly forwards the value.
Cotabby/Support/MirrorOverlayLayout.swift computeAnchorTopY groups .caretMidLine with .userPreference and .perAppOverride for caret-rect anchoring; doc comment updated accordingly.
Cotabby/Models/CompletionRenderMode.swift New .caretMidLine reason added to MirrorReason with accurate doc comment describing caret-rect anchoring behavior.
Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift Passes isCaretAtEndOfLine: context.isCaretAtEndOfLine to SuggestionOverlayGeometry; single-line, correct change.
CotabbyTests/CompletionRenderModePolicyTests.swift Seven new tests cover the full preference × caret-position matrix, including the subtle "estimated geometry keeps its reason" and "alwaysInline is overridden mid-line" invariants.
CotabbyTests/CotabbyTestFixtures.swift Fixture overlayGeometry gains isCaretAtEndOfLine parameter (default true) matching the production default.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SuggestionCoordinator\nreceives context] --> B[Build SuggestionOverlayGeometry\nwith isCaretAtEndOfLine]
    B --> C[CompletionRenderModePolicy\n.mode for:bundleIdentifier:]
    C --> D[preferenceMode\npreference + caretQuality]
    D --> E{baseMode == .inline?}
    E -- No --> F[Return baseMode\ne.g. .mirror caretGeometryEstimated\nor .mirror userPreference]
    E -- Yes --> G{isCaretAtEndOfLine?}
    G -- true --> H[Return .inline]
    G -- false --> I[Return .mirror reason: .caretMidLine]
    H --> J[MirrorOverlayLayout\nno card shown]
    I --> K[MirrorOverlayLayout\ncomputeAnchorTopY]
    F --> K
    K --> L{reason}
    L -- caretGeometryEstimated --> M[Anchor to inputFieldRect\nwith vertical slack offset]
    L -- userPreference / perAppOverride / caretMidLine --> N[Anchor to caretRect\nfield rect as fallback]
Loading

Reviews (2): Last reviewed commit: "docs: correct caretMidLine anchor commen..." | Re-trigger Greptile

When the caret sits mid-line (real characters follow it before the next line break), inline ghost text has no home: it would paint over the trailing characters. Promote any such inline presentation to the mirror card, which anchors to the caret line. This also establishes the surface fill-in-middle completions will render in.

The promotion overrides an explicit Inline preference too, since inline cannot render mid-line; presentations already routed to the card keep their original, more specific reason. Drives off the existing CaretLinePosition.isAtEndOfLine signal so an end-of-line caret with later paragraphs stays inline.

Adds isCaretAtEndOfLine to SuggestionOverlayGeometry (defaulted true), the .caretMidLine mirror reason, MirrorOverlayLayout anchoring for it, and 7 policy tests covering the auto/inline/mirror/per-app matrix.
Comment thread Cotabby/Models/CompletionRenderMode.swift
The .caretMidLine card anchors to the caret rect (computeAnchorTopY groups it
with the trustworthy-geometry cases), not the field rect. Fix the misleading
comment Greptile flagged on CompletionRenderMode.caretMidLine, plus the same
wrong phrasing in CompletionRenderModePolicy.mode(for:).
@FuJacob FuJacob merged commit d4c7bae into main Jun 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant