Conversation
|
I manually ran these cases through the renderer and checked both the reported position and the emitted cursor escape:
The branch also currently fails its own two “cursor wraps after text” tests. The test helper treats an omitted offset as present, so it duplicates the input with the cursor between the copies. The earlier movement tests don't catch that because they check the cursor position without asserting the duplicated rendered output. The common problem is that the cursor starts as a source string index, but sanitization and nested transforms can change the string without updating that index. It is then converted to a terminal-column count, which the wrapping code compares with JavaScript string indexes, while outer transforms can still change the visible output afterward. The source-to-output mapping has already been lost before the final position is resolved, so I don't think more offset adjustments will make this robust. |
|
The docs say that Transform:
Wouldn't something that prefixes the text break that expectation? |
*Maybe* wrapping the cursor won't be as painful as I initially thought?
It's helpful for tests, at least...
The cursor should wrap with the character it's on, not stay at the newline position
ac4e713 to
73724d8
Compare
squashTextNodes now consistently returns *offsets* into the string returned, rather than a terminal cursor position; renderNodeToOutput handles stringWidth math instead
We're somewhat successfully handling wrapping adding length, but handling the sanitization *removing* it may be... interesting.
Yes, you're right. Prepending The underlying issue still applies to dimension-preserving transforms, though. For example: <Text>
<Transform transform={text => `\u001B[31m${text}\u001B[39m`}>
<Text>a<Cursor />b</Text>
</Transform>
</Text>This still renders So width-changing transforms do not need to be supported, but the transform concern remains for styling transforms that follow the documented contract. |
|
Sounds good, thank you for the clarification 👍 Still iterating here and have tweaked some approaches to handle ansi sanitization and cjk. Thinking about output clipping and transforms right now. If the expected cursor column is clipped off, what would be the expected behavior? Render at the closest non-clipped position, or just hide it? 🤔 |
wrapCursorOffsetToPosition now looks at the ansi-stripped wrappedText when computing its result. The tricky part here is that something like `<red>abc</red>` might get wrapped to eg: ``` \u001B[31mab\u001B[39m \u001B[31mc\u001B[39m ``` Note the extra close and open sequences in addition to the newline added by wrapping! By stripping out ansi tokens from the `cursorOffset` and also when converting from offset to `CursorPosition` we can more easily compare apples to apples.
*Ideally*, the cursor should probably stay wherever the truncated part is---which means if we truncate in the middle, and the cursor is somewhere in that truncated area, it should land on the ellipsis instead of in the "after" part of the truncation
This *may* be a bit jank... but seems maybe okay? We're reusing the existing stringWidth computations so it's pretty cheap
Discovered in manual testing that our example IME component wasn't placing the cursor correctly anymore. Was definitely trying to be too clever with the "consume x columns" approach before—this version is both simpler to follow (and better matches the docstring on cursorOffset!) and is more correct.
We were using the wrapped x offset to determine truncation, but comparing against a range of the original text; we should use cursorOffset instead
|
For now, I've opted to hide the cursor if it gets clipped and clamp the cursor to the ellipsis when the text is truncated, but I think clamping on clip would be straightforward enough to implement that if it's a behavior enough folks need, it could be selected via a prop on the |
This is an alternative to #872 (and replacement to #1002) that also address #870 and #251.
<Cursor />renders inline within<Text />, following it naturally with Ink's word wraps. Wherever<Cursor />is rendered within the output is where Ink will place the system cursor.How
Unlike #1002, as discussed there this approach renders an
ink-textelement with a newinternal_cursorOffsetparam that signalssquashTextNodesto return acursorOffsetthatrenderNodeToOutputthen uses to calculate aCursorPosition(including handling of text wrapping).I was a little concerned about getting the cursor wrapped cleanly, following the text, but at least the cases I've tested seem to work pretty well and were less challenging to handle than expected.
Details
<Cursor />is mutually exclusive with useCursor, enforced at runtime. In component mode, the absence of a rendered Cursor in the output means to hide the cursor, but useCursor will never render one in the output so clearing the component cursor would also clear any set by useCursor. There might be a clever way to use the CursorContext to indicate to Ink that a useCursor hook is mounted, but that didn't seem worth it.onCursorUpdatedrender prop. For most consumers it may be more convenient to provide the cursor position viaCursorContextand a new hook, but I think users that need that could create such a thing themselves without too much trouble from this render prop.Questions
<Cursor />wrapping<Text />children and accepting anoffsetproperty. Is that interesting?