Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/utils/Cursor.nfc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, test } from 'bun:test'
import { Cursor } from './Cursor.js'

describe('Cursor.insert NFC boundary offset', () => {
test('places the cursor correctly when the insert composes with the prefix', () => {
// Cursor sits between "e" and "X". Inserting a combining acute accent
// composes with the "e" into a single "é", so the normalized text is "éX"
// (length 2) and the cursor belongs at offset 1 (before "X"). The old code
// measured the insert in isolation and landed at offset 2 (after "X").
const c = Cursor.fromText('eX', 80, 1)
expect(c.text).toBe('eX')

const after = c.insert('́')
expect(after.text).toBe('éX')
expect(after.offset).toBe(1)
})

test('non-composing inserts still advance by the inserted length', () => {
const c = Cursor.fromText('abcX', 80, 3) // between "abc" and "X"
const after = c.insert('de')
expect(after.text).toBe('abcdeX')
expect(after.offset).toBe(5)
})

test('astral-plane insert advances by its UTF-16 unit count', () => {
const c = Cursor.fromText('X', 80, 0)
const after = c.insert('😀') // 2 UTF-16 code units
expect(after.text).toBe('😀X')
expect(after.offset).toBe(2)
})
})
16 changes: 11 additions & 5 deletions src/utils/Cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -875,11 +875,17 @@ export class Cursor {
insertString +
this.text.slice(endOffset)

return Cursor.fromText(
newText,
this.columns,
startOffset + insertString.normalize('NFC').length,
)
// Cursor.fromText NFC-normalizes the whole newText, so compute the new
// offset from the normalized prefix-plus-insert rather than normalizing
// insertString in isolation. Otherwise a combining mark that composes with
// the last character of the prefix (e.g. "e" + U+0301 -> "é") shortens the
// normalized text by one unit that this offset doesn't account for, landing
// the cursor one position too far (past following text).
const newOffset = (
this.text.slice(0, startOffset) + insertString
).normalize('NFC').length

return Cursor.fromText(newText, this.columns, newOffset)
}

insert(insertString: string): Cursor {
Expand Down