2026-02-20 08:16:43 -05:00
|
|
|
export function applyTextInput(
|
|
|
|
|
key: string,
|
|
|
|
|
currentString: string,
|
|
|
|
|
cursorPos: number,
|
|
|
|
|
maxLength: number,
|
|
|
|
|
): { newString: string; newCursorPos: number } {
|
|
|
|
|
let newString = currentString;
|
|
|
|
|
let newCursorPos = cursorPos;
|
|
|
|
|
const lowerKey = key.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (lowerKey === 'arrowleft') {
|
|
|
|
|
newCursorPos = Math.max(0, cursorPos - 1);
|
|
|
|
|
} else if (lowerKey === 'arrowright') {
|
|
|
|
|
newCursorPos = Math.min(newString.length, cursorPos + 1);
|
|
|
|
|
} else if (lowerKey === 'backspace') {
|
|
|
|
|
if (cursorPos > 0) {
|
|
|
|
|
newString = newString.slice(0, cursorPos - 1) + newString.slice(cursorPos);
|
|
|
|
|
newCursorPos = cursorPos - 1;
|
|
|
|
|
}
|
2026-02-22 03:21:58 -05:00
|
|
|
} else if (lowerKey === 'delete') {
|
|
|
|
|
if (cursorPos < newString.length) {
|
|
|
|
|
newString = newString.slice(0, cursorPos) + newString.slice(cursorPos + 1);
|
|
|
|
|
}
|
2026-02-20 08:16:43 -05:00
|
|
|
} else if (lowerKey === 'home') {
|
|
|
|
|
newCursorPos = 0;
|
|
|
|
|
} else if (lowerKey === 'end') {
|
|
|
|
|
newCursorPos = newString.length;
|
|
|
|
|
} else if (key.length === 1 && newString.length < maxLength) {
|
|
|
|
|
newString = newString.slice(0, cursorPos) + key + newString.slice(cursorPos);
|
|
|
|
|
newCursorPos = cursorPos + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { newString, newCursorPos };
|
|
|
|
|
}
|
2026-02-21 03:57:49 -05:00
|
|
|
|
|
|
|
|
export function shouldReplaceCurrentText(
|
|
|
|
|
code: string,
|
|
|
|
|
key: string,
|
|
|
|
|
replaceTextOnNextType: boolean,
|
|
|
|
|
): { replaceTextOnNextType: boolean; shouldReplace: boolean } {
|
|
|
|
|
if (!replaceTextOnNextType) return { replaceTextOnNextType: false, shouldReplace: false };
|
|
|
|
|
if (code === 'ArrowLeft' || code === 'ArrowRight' || code === 'Home' || code === 'End') {
|
|
|
|
|
return { replaceTextOnNextType: false, shouldReplace: false };
|
|
|
|
|
}
|
|
|
|
|
if (code === 'Backspace' || code === 'Delete') {
|
|
|
|
|
return { replaceTextOnNextType: false, shouldReplace: false };
|
|
|
|
|
}
|
|
|
|
|
if (key.length === 1) {
|
|
|
|
|
return { replaceTextOnNextType: false, shouldReplace: true };
|
|
|
|
|
}
|
|
|
|
|
return { replaceTextOnNextType: true, shouldReplace: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizePastedText(raw: string): string {
|
|
|
|
|
return raw
|
|
|
|
|
.replace(/\r\n/g, '\n')
|
|
|
|
|
.replace(/\r/g, '\n')
|
|
|
|
|
.replace(/\n/g, ' ')
|
|
|
|
|
.replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function applyPastedText(
|
|
|
|
|
raw: string,
|
|
|
|
|
currentString: string,
|
|
|
|
|
cursorPos: number,
|
|
|
|
|
maxLength: number,
|
|
|
|
|
replaceTextOnNextType: boolean,
|
|
|
|
|
): { handled: boolean; newString: string; newCursorPos: number; replaceTextOnNextType: boolean } {
|
|
|
|
|
const text = normalizePastedText(raw);
|
|
|
|
|
if (!text) {
|
|
|
|
|
return { handled: true, newString: currentString, newCursorPos: cursorPos, replaceTextOnNextType };
|
|
|
|
|
}
|
|
|
|
|
if (replaceTextOnNextType) {
|
|
|
|
|
const replacement = text.slice(0, maxLength);
|
|
|
|
|
return {
|
|
|
|
|
handled: true,
|
|
|
|
|
newString: replacement,
|
|
|
|
|
newCursorPos: replacement.length,
|
|
|
|
|
replaceTextOnNextType: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const available = Math.max(0, maxLength - currentString.length);
|
|
|
|
|
if (available <= 0) {
|
|
|
|
|
return { handled: true, newString: currentString, newCursorPos: cursorPos, replaceTextOnNextType: false };
|
|
|
|
|
}
|
|
|
|
|
const insert = text.slice(0, available);
|
|
|
|
|
return {
|
|
|
|
|
handled: true,
|
|
|
|
|
newString: currentString.slice(0, cursorPos) + insert + currentString.slice(cursorPos),
|
|
|
|
|
newCursorPos: cursorPos + insert.length,
|
|
|
|
|
replaceTextOnNextType: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mapTextInputKey(code: string, key: string): string {
|
|
|
|
|
if (code === 'ArrowLeft') return 'arrowleft';
|
|
|
|
|
if (code === 'ArrowRight') return 'arrowright';
|
|
|
|
|
if (code === 'Backspace') return 'backspace';
|
2026-02-22 03:21:58 -05:00
|
|
|
if (code === 'Delete') return 'delete';
|
2026-02-21 03:57:49 -05:00
|
|
|
if (code === 'Home') return 'home';
|
|
|
|
|
if (code === 'End') return 'end';
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isWordCharacter(ch: string): boolean {
|
2026-02-21 04:01:20 -05:00
|
|
|
return /[A-Za-z0-9_'\u2019]/.test(ch);
|
2026-02-21 03:57:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function moveCursorWordLeft(text: string, cursorPos: number): number {
|
|
|
|
|
if (cursorPos <= 0) return 0;
|
|
|
|
|
let pos = cursorPos - 1;
|
|
|
|
|
while (pos > 0 && !isWordCharacter(text[pos])) pos -= 1;
|
|
|
|
|
while (pos > 0 && isWordCharacter(text[pos - 1])) pos -= 1;
|
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function moveCursorWordRight(text: string, cursorPos: number): number {
|
|
|
|
|
let pos = cursorPos;
|
|
|
|
|
while (pos < text.length && isWordCharacter(text[pos])) pos += 1;
|
|
|
|
|
while (pos < text.length && !isWordCharacter(text[pos])) pos += 1;
|
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function wordAtCursor(text: string, cursorPos: number): string | null {
|
|
|
|
|
if (cursorPos < 0 || cursorPos >= text.length || !isWordCharacter(text[cursorPos])) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
let start = cursorPos;
|
|
|
|
|
while (start > 0 && isWordCharacter(text[start - 1])) start -= 1;
|
|
|
|
|
let end = cursorPos + 1;
|
|
|
|
|
while (end < text.length && isWordCharacter(text[end])) end += 1;
|
|
|
|
|
return text.slice(start, end);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function describeCharacter(ch: string): string {
|
|
|
|
|
if (ch === ' ') return 'space';
|
|
|
|
|
if (ch === '\t') return 'tab';
|
|
|
|
|
if (ch === '.') return 'period';
|
|
|
|
|
if (ch === ',') return 'comma';
|
|
|
|
|
if (ch === ':') return 'colon';
|
|
|
|
|
if (ch === ';') return 'semicolon';
|
|
|
|
|
if (ch === '!') return 'exclamation mark';
|
|
|
|
|
if (ch === '?') return 'question mark';
|
|
|
|
|
if (ch === "'") return 'apostrophe';
|
|
|
|
|
if (ch === '"') return 'quote';
|
|
|
|
|
if (ch === '/') return 'slash';
|
|
|
|
|
if (ch === '\\') return 'backslash';
|
|
|
|
|
if (ch === '-') return 'dash';
|
|
|
|
|
if (ch === '_') return 'underscore';
|
|
|
|
|
if (ch === '=') return 'equals';
|
|
|
|
|
if (ch === '+') return 'plus';
|
|
|
|
|
if (ch === '*') return 'asterisk';
|
|
|
|
|
if (ch === '&') return 'ampersand';
|
|
|
|
|
if (ch === '@') return 'at sign';
|
|
|
|
|
if (ch === '#') return 'hash';
|
|
|
|
|
if (ch === '%') return 'percent';
|
|
|
|
|
if (ch === '$') return 'dollar sign';
|
|
|
|
|
if (ch === '^') return 'caret';
|
|
|
|
|
if (ch === '|') return 'pipe';
|
|
|
|
|
if (ch === '~') return 'tilde';
|
|
|
|
|
if (ch === '`') return 'backtick';
|
|
|
|
|
if (ch === '(') return 'left parenthesis';
|
|
|
|
|
if (ch === ')') return 'right parenthesis';
|
|
|
|
|
if (ch === '[') return 'left bracket';
|
|
|
|
|
if (ch === ']') return 'right bracket';
|
|
|
|
|
if (ch === '{') return 'left brace';
|
|
|
|
|
if (ch === '}') return 'right brace';
|
|
|
|
|
if (ch === '<') return 'less than';
|
|
|
|
|
if (ch === '>') return 'greater than';
|
|
|
|
|
return ch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function describeCursorCharacter(text: string, cursorPos: number): string | null {
|
|
|
|
|
if (cursorPos < 0 || cursorPos > text.length) return null;
|
|
|
|
|
if (cursorPos === text.length) return 'space';
|
|
|
|
|
return describeCharacter(text[cursorPos]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function describeCursorWordOrCharacter(text: string, cursorPos: number): string | null {
|
|
|
|
|
if (cursorPos === text.length) return 'space';
|
|
|
|
|
const word = wordAtCursor(text, cursorPos);
|
|
|
|
|
if (word) return word;
|
|
|
|
|
return describeCursorCharacter(text, cursorPos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function describeBackspaceDeletedCharacter(text: string, cursorPos: number): string | null {
|
|
|
|
|
if (cursorPos <= 0 || cursorPos > text.length) return null;
|
|
|
|
|
return describeCharacter(text[cursorPos - 1]);
|
|
|
|
|
}
|
2026-02-22 03:21:58 -05:00
|
|
|
|
|
|
|
|
export function describeDeleteDeletedCharacter(text: string, cursorPos: number): string | null {
|
|
|
|
|
if (cursorPos < 0 || cursorPos >= text.length) return null;
|
|
|
|
|
return describeCharacter(text[cursorPos]);
|
|
|
|
|
}
|