Lay an extension span out as a fixed-width control box (InlineSyntax.inlineBoxWidth) - #157
Draft
dylandeheer wants to merge 3 commits into
Draft
Lay an extension span out as a fixed-width control box (InlineSyntax.inlineBoxWidth)#157dylandeheer wants to merge 3 commits into
dylandeheer wants to merge 3 commits into
Conversation
An extension can supply syntax and content attributes, which is enough for a construct that reads as text but not for one that reads as a control: a citation playhead, a chip, a token. `contentAttributes(theme:)` covers the content range uniformly, so an embedder cannot reserve a width that is independent of the character count, and drawing the control over the text leaves it overlapping whatever follows. `inlineBoxWidth` collapses the construct's markers and content and keeps that many points of line space in their place, riding as kerning on the span's first character so the box is one advance and the line height stays the paragraph's. The marker-shrink pass skips a box (it would overwrite the reserved advance), the caret no longer reveals its delimiters (there is no text to reveal, and revealing would shove the box aside mid-typing), and the auto-link and incomplete-bracket text passes skip it, so a span like `[t=12.0-19.5]` is not painted as a broken link. Default nil, so every existing extension lays out exactly as before. Co-authored-by: Cursor <cursoragent@cursor.com>
The other cases assert what the styler asks TextKit for. This one lays the same document out at two box widths and checks the line gains exactly the difference, so a reserved point is a laid-out point. Co-authored-by: Cursor <cursoragent@cursor.com>
A box has no glyphs of its own, so without this the construct reads as a hole in the line: an arrow key stops a dozen times in a spot that never moves, a click drops the caret inside it, one backspace shaves off a delimiter and turns the box back into raw text, and the pointer keeps the I-beam over what is drawn as a control. The styler marks the span with `.inlineExtensionBox`. From there the text view normalizes every selection through it, so the caret only rests on its edges and a selection takes all of it or none; backward and forward delete remove the whole span in one keystroke; and the pointer reads its laid-out rect as a control and shows the hand, as the drawn task checkbox already does. A span without `inlineBoxWidth` is untouched on all three counts. Co-authored-by: Cursor <cursoragent@cursor.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.
What
Adds
InlineSyntax.inlineBoxWidth: CGFloat?. When set, the extension's span islaid out as a fixed-width box instead of as text: markers and content collapse,
that many points of line space are kept in their place, and the span behaves as
one control for the caret, the selection, delete, and the pointer.
Why
An extension supplies syntax and content attributes today, which is enough for
a construct that reads as prose but not for one that reads as a control — a
citation playhead, a chip, a token.
contentAttributes(theme:)applies to thewhole content range uniformly, so an embedder cannot reserve a width that is
independent of the character count. Without reserved space the only option is
to draw the control over the text, where it overlaps whatever follows and does
not wrap with the paragraph.
Reserved space alone is not enough, though: a span with no glyphs of its own is
a hole in the line. An arrow key stops a dozen times in a spot that never
moves, a click drops the caret inside the construct, one backspace shaves off a
delimiter and turns the box back into raw text mid-sentence, and the pointer
keeps the I-beam over something drawn as a button.
How
is a single advance: the line's height stays the paragraph's rather than the
box's.
shrinkInactiveMarkersskips a box span — its shrink would overwrite thereserved advance, which sits on a marker character.
and revealing them would shove the box sideways while the user types in the
line it sits on.
bracketed construct like
[t=12.0-19.5]is not painted as a broken link..inlineExtensionBox.NativeTextViewnormalizes every selection through
InlineBoxCaret, so the caret rests onlyon the box's edges and a selection takes all of it or none; backward and
forward delete remove the whole span in one keystroke; and the pointer reads
the span's laid-out rect as a control and shows the hand, the way the drawn
task checkbox already does.
Compatibility
nilby default: every existing extension parses, styles, and navigatesexactly as before — covered by a plain-span counterpart in each test. The
registry fingerprint includes the new field, so two registries differing only
in it cannot share cached parses.
Test plan
InlineExtensionBoxTests: the width lands on the first character, therest of the span collapses, a caret inside the span changes nothing in it,
a span without the knob keeps upstream layout and caret reveal, the
fingerprint separates the two registries, and TextKit grants exactly the
reserved width (same document laid out at two widths).
InlineBoxCaretTests: arrows step over the box from either side and neverrest inside it, a dropped caret takes the nearer edge, shift-arrow and a
partial range take the whole box, backspace and forward delete remove it
whole, the laid-out rect is the reserved width, and every point of that
rect reads as a control while points beside it do not.
Made with Cursor