fix(tui): align input word segmentation with editor (#5068)
This commit is contained in:
@@ -4,14 +4,8 @@ import { decodePrintableKey, matchesKey } from "../keys.ts";
|
||||
import { KillRing } from "../kill-ring.ts";
|
||||
import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.ts";
|
||||
import { UndoStack } from "../undo-stack.ts";
|
||||
import {
|
||||
getGraphemeSegmenter,
|
||||
getWordSegmenter,
|
||||
isWhitespaceChar,
|
||||
PUNCTUATION_REGEX,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
} from "../utils.ts";
|
||||
import { getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.ts";
|
||||
import { findWordBackward, findWordForward } from "../word-navigation.ts";
|
||||
import { SelectList, type SelectListLayoutOptions, type SelectListTheme } from "./select-list.ts";
|
||||
|
||||
const graphemeSegmenter = getGraphemeSegmenter();
|
||||
@@ -1780,48 +1774,12 @@ export class Editor implements Component, Focusable {
|
||||
return;
|
||||
}
|
||||
|
||||
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
||||
const segments = [...this.segment(textBeforeCursor, "word")];
|
||||
let newCol = this.state.cursorCol;
|
||||
|
||||
// Skip trailing whitespace
|
||||
while (
|
||||
segments.length > 0 &&
|
||||
!isPasteMarker(segments[segments.length - 1]?.segment || "") &&
|
||||
isWhitespaceChar(segments[segments.length - 1]?.segment || "")
|
||||
) {
|
||||
newCol -= segments.pop()?.segment.length || 0;
|
||||
}
|
||||
|
||||
if (segments.length > 0) {
|
||||
const last = segments[segments.length - 1]!;
|
||||
if (isPasteMarker(last.segment)) {
|
||||
// Skip one paste marker.
|
||||
newCol -= segments.pop()?.segment.length || 0;
|
||||
} else if (last.isWordLike) {
|
||||
// Skip inside one word-like segment, preserving existing ASCII punctuation boundaries.
|
||||
const segment = last.segment;
|
||||
const matches = [...segment.matchAll(new RegExp(PUNCTUATION_REGEX, "g"))];
|
||||
if (matches.length <= 0) {
|
||||
newCol -= segment.length;
|
||||
} else {
|
||||
const lastMatch = matches[matches.length - 1]!;
|
||||
newCol -= segment.length - (lastMatch.index + lastMatch[0].length);
|
||||
}
|
||||
} else {
|
||||
// Skip non-word non-whitespace run (punctuation)
|
||||
while (
|
||||
segments.length > 0 &&
|
||||
!isPasteMarker(segments[segments.length - 1]?.segment || "") &&
|
||||
!segments[segments.length - 1]?.isWordLike &&
|
||||
!isWhitespaceChar(segments[segments.length - 1]?.segment || "")
|
||||
) {
|
||||
newCol -= segments.pop()?.segment.length || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setCursorCol(newCol);
|
||||
this.setCursorCol(
|
||||
findWordBackward(currentLine, this.state.cursorCol, {
|
||||
segment: (text) => this.segment(text, "word"),
|
||||
isAtomicSegment: isPasteMarker,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2008,40 +1966,12 @@ export class Editor implements Component, Focusable {
|
||||
return;
|
||||
}
|
||||
|
||||
const textAfterCursor = currentLine.slice(this.state.cursorCol);
|
||||
const segments = this.segment(textAfterCursor, "word");
|
||||
const iterator = segments[Symbol.iterator]();
|
||||
let next = iterator.next();
|
||||
let newCol = this.state.cursorCol;
|
||||
|
||||
// Skip leading whitespace
|
||||
while (!next.done && !isPasteMarker(next.value.segment) && isWhitespaceChar(next.value.segment)) {
|
||||
newCol += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
|
||||
if (!next.done) {
|
||||
if (isPasteMarker(next.value.segment)) {
|
||||
// Skip one paste marker.
|
||||
newCol += next.value.segment.length;
|
||||
} else if (next.value.isWordLike) {
|
||||
// Skip inside one word-like segment, preserving existing ASCII punctuation boundaries.
|
||||
newCol += PUNCTUATION_REGEX.exec(next.value.segment)?.index ?? next.value.segment.length;
|
||||
} else {
|
||||
// Skip non-word non-whitespace run (punctuation)
|
||||
while (
|
||||
!next.done &&
|
||||
!isPasteMarker(next.value.segment) &&
|
||||
!next.value.isWordLike &&
|
||||
!isWhitespaceChar(next.value.segment)
|
||||
) {
|
||||
newCol += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setCursorCol(newCol);
|
||||
this.setCursorCol(
|
||||
findWordForward(currentLine, this.state.cursorCol, {
|
||||
segment: (text) => this.segment(text, "word"),
|
||||
isAtomicSegment: isPasteMarker,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Slash menu only allowed on the first line of the editor
|
||||
|
||||
@@ -3,7 +3,8 @@ import { decodeKittyPrintable } from "../keys.ts";
|
||||
import { KillRing } from "../kill-ring.ts";
|
||||
import { type Component, CURSOR_MARKER, type Focusable } from "../tui.ts";
|
||||
import { UndoStack } from "../undo-stack.ts";
|
||||
import { getGraphemeSegmenter, isPunctuationChar, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.ts";
|
||||
import { getGraphemeSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.ts";
|
||||
import { findWordBackward, findWordForward } from "../word-navigation.ts";
|
||||
|
||||
const segmenter = getGraphemeSegmenter();
|
||||
|
||||
@@ -347,72 +348,15 @@ export class Input implements Component, Focusable {
|
||||
}
|
||||
|
||||
private moveWordBackwards(): void {
|
||||
if (this.cursor === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cursor === 0) return;
|
||||
this.lastAction = null;
|
||||
const textBeforeCursor = this.value.slice(0, this.cursor);
|
||||
const graphemes = [...segmenter.segment(textBeforeCursor)];
|
||||
|
||||
// Skip trailing whitespace
|
||||
while (graphemes.length > 0 && isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
|
||||
this.cursor -= graphemes.pop()?.segment.length || 0;
|
||||
}
|
||||
|
||||
if (graphemes.length > 0) {
|
||||
const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
|
||||
if (isPunctuationChar(lastGrapheme)) {
|
||||
// Skip punctuation run
|
||||
while (graphemes.length > 0 && isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")) {
|
||||
this.cursor -= graphemes.pop()?.segment.length || 0;
|
||||
}
|
||||
} else {
|
||||
// Skip word run
|
||||
while (
|
||||
graphemes.length > 0 &&
|
||||
!isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
||||
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")
|
||||
) {
|
||||
this.cursor -= graphemes.pop()?.segment.length || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cursor = findWordBackward(this.value, this.cursor);
|
||||
}
|
||||
|
||||
private moveWordForwards(): void {
|
||||
if (this.cursor >= this.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cursor >= this.value.length) return;
|
||||
this.lastAction = null;
|
||||
const textAfterCursor = this.value.slice(this.cursor);
|
||||
const segments = segmenter.segment(textAfterCursor);
|
||||
const iterator = segments[Symbol.iterator]();
|
||||
let next = iterator.next();
|
||||
|
||||
// Skip leading whitespace
|
||||
while (!next.done && isWhitespaceChar(next.value.segment)) {
|
||||
this.cursor += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
|
||||
if (!next.done) {
|
||||
const firstGrapheme = next.value.segment;
|
||||
if (isPunctuationChar(firstGrapheme)) {
|
||||
// Skip punctuation run
|
||||
while (!next.done && isPunctuationChar(next.value.segment)) {
|
||||
this.cursor += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
} else {
|
||||
// Skip word run
|
||||
while (!next.done && !isWhitespaceChar(next.value.segment) && !isPunctuationChar(next.value.segment)) {
|
||||
this.cursor += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cursor = findWordForward(this.value, this.cursor);
|
||||
}
|
||||
|
||||
private handlePaste(pastedText: string): void {
|
||||
|
||||
Reference in New Issue
Block a user