prompt: fix cursor math for wide characters (#27017)
String.length counts code points, not display columns, so CJK characters and emoji that occupy two terminal cells caused misaligned cursors, broken mention triggers, and incorrect history restoration offsets. Use Bun.stringWidth for now, we need an alternative for this. Fix #26716 Close #26922
This commit is contained in:
@@ -14,7 +14,10 @@ import { createEffect, createMemo, createResource, createSignal, onCleanup, onMo
|
||||
import * as Locale from "@/util/locale"
|
||||
import {
|
||||
createPromptHistory,
|
||||
displayCharAt,
|
||||
displaySlice,
|
||||
isExitCommand,
|
||||
mentionTriggerIndex,
|
||||
isNewCommand,
|
||||
movePromptHistory,
|
||||
promptCycle,
|
||||
@@ -537,7 +540,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
})
|
||||
}
|
||||
|
||||
const restore = (value: RunPrompt, cursor = value.text.length) => {
|
||||
const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => {
|
||||
draft = clonePrompt(value)
|
||||
if (!area || area.isDestroyed) {
|
||||
return
|
||||
@@ -546,7 +549,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
hide()
|
||||
area.setText(value.text)
|
||||
restoreParts(value.parts)
|
||||
area.cursorOffset = Math.min(cursor, area.plainText.length)
|
||||
area.cursorOffset = Math.min(cursor, Bun.stringWidth(area.plainText))
|
||||
scheduleRows()
|
||||
area.focus()
|
||||
}
|
||||
@@ -577,7 +580,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
area.setText(text)
|
||||
clearParts()
|
||||
draft = { text: area.plainText, parts: [] }
|
||||
area.cursorOffset = Math.min(text.length, area.plainText.length)
|
||||
area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText))
|
||||
scheduleRows()
|
||||
area.focus()
|
||||
}
|
||||
@@ -610,12 +613,13 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
}
|
||||
|
||||
if (visible() && mode() === "mention") {
|
||||
if (cursor <= at() || /\s/.test(text.slice(at(), cursor))) {
|
||||
const query = displaySlice(text, at(), cursor)
|
||||
if (cursor <= at() || /\s/.test(query)) {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
|
||||
setQuery(text.slice(at() + 1, cursor))
|
||||
setQuery(displaySlice(text, at() + 1, cursor))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -623,19 +627,12 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
return
|
||||
}
|
||||
|
||||
const head = text.slice(0, cursor)
|
||||
const idx = head.lastIndexOf("@")
|
||||
if (idx === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const before = idx === 0 ? undefined : head[idx - 1]
|
||||
const tail = head.slice(idx)
|
||||
if ((before === undefined || /\s/.test(before)) && !/\s/.test(tail)) {
|
||||
const idx = mentionTriggerIndex(text, cursor)
|
||||
if (idx !== undefined) {
|
||||
setAt(idx)
|
||||
menu.reset()
|
||||
setMode("mention")
|
||||
setQuery(head.slice(idx + 1))
|
||||
setQuery(displaySlice(text, idx + 1, cursor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,7 +779,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
}
|
||||
|
||||
const cursor = area.cursorOffset
|
||||
const tail = area.plainText.at(cursor)
|
||||
const tail = displayCharAt(area.plainText, cursor)
|
||||
const append = "@" + next.value + (tail === " " ? "" : " ")
|
||||
area.cursorOffset = at()
|
||||
const start = area.logicalCursor
|
||||
@@ -941,7 +938,8 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
}
|
||||
|
||||
const dir = up ? -1 : 1
|
||||
if ((dir === -1 && area.cursorOffset === 0) || (dir === 1 && area.cursorOffset === area.plainText.length)) {
|
||||
const endOffset = Bun.stringWidth(area.plainText)
|
||||
if ((dir === -1 && area.cursorOffset === 0) || (dir === 1 && area.cursorOffset === endOffset)) {
|
||||
move(dir, event)
|
||||
return
|
||||
}
|
||||
@@ -955,7 +953,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
? area.height - 1
|
||||
: Math.max(0, (area.virtualLineCount ?? 1) - 1)
|
||||
if (dir === 1 && area.visualCursor.visualRow === end) {
|
||||
area.cursorOffset = area.plainText.length
|
||||
area.cursorOffset = endOffset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// The leader-key cycle (promptCycle) uses a two-step pattern: first press
|
||||
// arms the leader, second press within the timeout fires the action.
|
||||
import type { KeyBinding } from "@opentui/core"
|
||||
export { displayCharAt, displaySlice, mentionTriggerIndex } from "../prompt-display"
|
||||
import { formatBinding, parseBindings } from "./keymap.shared"
|
||||
import type { FooterKeybinds, RunPrompt } from "./types"
|
||||
|
||||
@@ -275,7 +276,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
||||
return { state, apply: false }
|
||||
}
|
||||
|
||||
if (dir === 1 && cursor !== text.length) {
|
||||
if (dir === 1 && cursor !== Bun.stringWidth(text)) {
|
||||
return { state, apply: false }
|
||||
}
|
||||
|
||||
@@ -309,7 +310,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
||||
index: null,
|
||||
},
|
||||
text: state.draft,
|
||||
cursor: state.draft.length,
|
||||
cursor: Bun.stringWidth(state.draft),
|
||||
apply: true,
|
||||
}
|
||||
}
|
||||
@@ -320,7 +321,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
||||
index: idx,
|
||||
},
|
||||
text: state.items[idx].text,
|
||||
cursor: dir === -1 ? 0 : state.items[idx].text.length,
|
||||
cursor: dir === -1 ? 0 : Bun.stringWidth(state.items[idx].text),
|
||||
apply: true,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user