update method
Updates the component state in response to a message.
Returns the updated component (often this) and an optional command.
Implementation
@override
(TextInputModel, Cmd?) update(Msg msg) {
return _runEditFrame(() {
final cmds = <Cmd>[];
if (msg is MouseMsg) {
if (multiline) {
return _handleMultilineMouse(msg);
}
if (msg.action == MouseAction.wheel) {
switch (msg.button) {
case MouseButton.wheelUp:
case MouseButton.wheelLeft:
_scrollSingleLineBy(-1);
break;
case MouseButton.wheelDown:
case MouseButton.wheelRight:
_scrollSingleLineBy(1);
break;
default:
break;
}
return (this, null);
}
if (msg.y != 0) {
if (msg.action == MouseAction.press &&
msg.button == MouseButton.left) {
_clearOffsetSelection();
_mouseSelecting = false;
_focused = false;
}
return (this, null);
}
final promptWidth = stringWidth(prompt);
final localX = msg.x - promptWidth;
final visibleValue = _value.sublist(_offset, _offsetRight);
final visibleText = visibleValue.join();
final idxInVisible = layout.localCellXToGraphemeIndex(
visibleText,
localX,
);
final x = _offset + idxInVisible;
if (msg.action == MouseAction.press && msg.button == MouseButton.left) {
_focused = true;
_mouseSelecting = true;
final now = DateTime.now();
final clickCount =
_lastClickTime != null &&
now.difference(_lastClickTime!) <
const Duration(milliseconds: 500) &&
_lastClickPos == x
? (_lastClickCount + 1).clamp(1, 3)
: 1;
_lastClickTime = now;
_lastClickPos = x;
_lastClickCount = clickCount;
if (clickCount == 2) {
final (start, end) = _findWordAt(x);
_selectOffsetState(
baseOffset: start,
extentOffset: end,
cursorOffset: end,
);
} else if (clickCount >= 3) {
final (start, end) = _findLineAt(x);
_selectOffsetState(
baseOffset: start,
extentOffset: end,
cursorOffset: end,
);
} else {
_selectOffsetState(
baseOffset: x,
extentOffset: x,
cursorOffset: x,
preserveCollapsedSelection: true,
);
}
} else if (msg.action == MouseAction.motion && _mouseSelecting) {
_selectOffsetState(
baseOffset: selectionStart ?? _pos,
extentOffset: x,
cursorOffset: x,
preserveCollapsedSelection: true,
);
} else if (msg.action == MouseAction.release && _mouseSelecting) {
_mouseSelecting = false;
if (selectionStart == selectionEnd) {
_clearOffsetSelection();
return (this, null);
}
final cmd = _copySelectionCmdIfAny();
_clearOffsetSelection();
return (this, cmd);
}
return (this, null);
}
if (!_focused) {
return (this, null);
}
// Check for suggestion acceptance first
if (msg is KeyMsg && keyMatches(msg.key, [keyMap.acceptSuggestion])) {
if (_canAcceptSuggestion()) {
_beginHistoryAction(
_TextInputHistoryAction.replace,
breakChain: true,
);
_recordUndoSnapshot();
final suggestion = _matchedSuggestions[_currentSuggestionIndex];
_value = [..._value, ...suggestion.sublist(_value.length)];
_invalidateWrappedLines();
cursorEnd();
}
}
if (msg is KeyMsg) {
if (keyMatches(msg.key, [keyMap.copy])) {
final selected = getSelectedText();
if (selected.isNotEmpty) {
return (this, Cmd.setClipboardBestEffort(selected));
}
}
if (keyMatches(msg.key, [keyMap.undo])) {
undo();
_updateSuggestions();
return (this, null);
}
if (keyMatches(msg.key, [keyMap.redo])) {
redo();
_updateSuggestions();
return (this, null);
}
if (keyMatches(msg.key, [keyMap.cut])) {
final selected = getSelectedText();
if (selected.isNotEmpty) {
_beginHistoryAction(
_TextInputHistoryAction.replace,
breakChain: true,
);
_deleteSelection();
return (this, Cmd.setClipboardBestEffort(selected));
}
}
if (keyMatches(msg.key, [keyMap.selectAll])) {
selectAll();
return (this, null);
}
// Multi-line: newline insertion (Enter / Shift+Enter)
if (multiline && keyMatches(msg.key, [keyMap.newline])) {
_beginHistoryAction(_TextInputHistoryAction.insert);
_deleteSelection();
_resetDesiredCol();
_insertNewline();
_updateSuggestions();
_handleOverflow();
return (this, null);
}
if (msg.key.type == KeyType.space) {
_beginHistoryAction(_TextInputHistoryAction.insert);
_resetDesiredCol();
_insertRunes([0x20]);
return (this, null);
}
if (keyMatches(msg.key, [keyMap.deleteWordBackward])) {
_beginHistoryAction(_TextInputHistoryAction.deleteBackward);
_resetDesiredCol();
if (!_deleteSelection()) {
_deleteWordBackward();
}
} else if (keyMatches(msg.key, [keyMap.deleteCharacterBackward])) {
_beginHistoryAction(_TextInputHistoryAction.deleteBackward);
_resetDesiredCol();
if (!_deleteSelection()) {
_deleteBeforeCursor();
}
} else if (keyMatches(msg.key, [keyMap.wordBackward])) {
_resetDesiredCol();
_wordBackward(clearSelection: true);
} else if (keyMatches(msg.key, [keyMap.selectWordBackward])) {
_resetDesiredCol();
_wordBackward(extendSelection: true);
} else if (keyMatches(msg.key, [keyMap.characterBackward])) {
_resetDesiredCol();
_moveByCharacter(forward: false, clearSelection: true);
} else if (keyMatches(msg.key, [keyMap.selectCharacterBackward])) {
_resetDesiredCol();
_moveByCharacter(forward: false, extendSelection: true);
} else if (keyMatches(msg.key, [keyMap.wordForward])) {
_resetDesiredCol();
_wordForward(clearSelection: true);
} else if (keyMatches(msg.key, [keyMap.selectWordForward])) {
_resetDesiredCol();
_wordForward(extendSelection: true);
} else if (keyMatches(msg.key, [keyMap.characterForward])) {
_resetDesiredCol();
_moveByCharacter(forward: true, clearSelection: true);
} else if (keyMatches(msg.key, [keyMap.selectCharacterForward])) {
_resetDesiredCol();
_moveByCharacter(forward: true, extendSelection: true);
} else if (multiline && keyMatches(msg.key, [keyMap.documentStart])) {
// Multi-line: Ctrl+Home — go to document start
_resetDesiredCol();
_moveToDocumentBoundary(forward: false, clearSelection: true);
} else if (multiline && keyMatches(msg.key, [keyMap.documentEnd])) {
// Multi-line: Ctrl+End — go to document end
_resetDesiredCol();
_moveToDocumentBoundary(forward: true, clearSelection: true);
} else if (keyMatches(msg.key, [keyMap.lineStart])) {
_resetDesiredCol();
if (multiline) {
_cursorLineStart();
} else {
cursorStart();
}
} else if (keyMatches(msg.key, [keyMap.selectLineStart])) {
_resetDesiredCol();
if (multiline) {
_cursorLineStart(extendSelection: true);
} else {
_moveToDocumentBoundary(
forward: false,
extendSelection: true,
clearSelection: false,
);
}
} else if (keyMatches(msg.key, [keyMap.deleteCharacterForward])) {
_beginHistoryAction(_TextInputHistoryAction.deleteForward);
_resetDesiredCol();
if (!_deleteSelection()) {
_deleteAfterCursor();
}
} else if (keyMatches(msg.key, [keyMap.lineEnd])) {
_resetDesiredCol();
if (multiline) {
_cursorLineEnd();
} else {
cursorEnd();
}
} else if (keyMatches(msg.key, [keyMap.selectLineEnd])) {
_resetDesiredCol();
if (multiline) {
_cursorLineEnd(extendSelection: true);
} else {
_moveToDocumentBoundary(
forward: true,
extendSelection: true,
clearSelection: false,
);
}
} else if (keyMatches(msg.key, [keyMap.deleteAfterCursor])) {
_beginHistoryAction(_TextInputHistoryAction.deleteForward);
_resetDesiredCol();
_clearOffsetSelection();
_deleteAfterCursor();
} else if (keyMatches(msg.key, [keyMap.deleteBeforeCursor])) {
_beginHistoryAction(_TextInputHistoryAction.deleteBackward);
_resetDesiredCol();
_clearOffsetSelection();
_deleteBeforeCursor();
} else if (keyMatches(msg.key, [keyMap.paste])) {
_beginHistoryAction(_TextInputHistoryAction.paste, breakChain: true);
_resetDesiredCol();
_deleteSelection();
// Return paste command - caller handles clipboard
return (this, _pasteCmd());
} else if (keyMatches(msg.key, [keyMap.deleteWordForward])) {
_beginHistoryAction(_TextInputHistoryAction.deleteForward);
_resetDesiredCol();
if (!_deleteSelection()) {
_deleteWordForward();
}
} else if (multiline && keyMatches(msg.key, [keyMap.lineUp])) {
// Multi-line: Up arrow — move cursor up one line
_lineUp();
} else if (multiline && keyMatches(msg.key, [keyMap.selectLineUp])) {
_lineUp(extendSelection: true);
} else if (multiline && keyMatches(msg.key, [keyMap.lineDown])) {
// Multi-line: Down arrow — move cursor down one line
_lineDown();
} else if (multiline && keyMatches(msg.key, [keyMap.selectLineDown])) {
_lineDown(extendSelection: true);
} else if (keyMatches(msg.key, [keyMap.nextSuggestion])) {
_nextSuggestion();
} else if (keyMatches(msg.key, [keyMap.prevSuggestion])) {
_previousSuggestion();
} else if (!msg.key.alt &&
msg.key.type == KeyType.runes &&
msg.key.runes.length == 1 &&
msg.key.runes.first == 0x03) {
final selected = getSelectedText();
if (selected.isNotEmpty) {
return (this, Cmd.setClipboardBestEffort(selected));
}
} else if (msg.key.runes.isNotEmpty && !msg.key.ctrl && !msg.key.alt) {
// Regular character input
final insertable = <int>[];
for (final r in msg.key.runes) {
if (r >= 0x20 && r != 0x7F) {
insertable.add(r);
}
}
if (insertable.isEmpty) {
_updateSuggestions();
return (this, null);
}
_beginHistoryAction(_TextInputHistoryAction.insert);
_resetDesiredCol();
_deleteSelection();
_insertRunes(insertable);
}
_updateSuggestions();
} else if (msg is _PasteChunkMsg) {
_beginHistoryAction(_TextInputHistoryAction.paste);
_applyNextPasteChunk();
if (_pasteController.hasPendingChunkedPaste) {
cmds.add(_schedulePasteChunk());
}
} else if (msg is PasteMsg || msg is PasteTextMsg) {
_beginHistoryAction(_TextInputHistoryAction.paste, breakChain: true);
final content = msg is PasteMsg
? msg.content
: (msg as PasteTextMsg).content;
final pastePlan = planTextPaste(
content,
collapseLargePaste: collapseLargePaste,
collapsedPasteMinChars: collapsedPasteMinChars,
collapsedPasteMinLines: collapsedPasteMinLines,
chunkThresholdRunes: _pasteChunkThresholdRunes,
);
if (TuiTrace.enabled) {
final kind = msg is PasteMsg ? 'PasteMsg' : 'PasteTextMsg';
TuiTrace.log(
'paste.msg kind=$kind chars=${content.length} focused=$_focused',
tag: TraceTag.input,
);
}
if (pastePlan.collapse) {
_insertCollapsedPasteReference(
content,
lineCount: pastePlan.lineCount,
);
} else if (pastePlan.chunked) {
cmds.add(_startChunkedPaste(content));
} else {
if (TuiTrace.enabled) {
TuiTrace.log(
'paste.inline chars=${content.length} runes=${pastePlan.runeCount}',
tag: TraceTag.input,
);
}
_insertRunes(uni.codePoints(content));
}
} else if (msg is PasteErrorMsg) {
error = msg.error.toString();
}
// Update cursor
final (newCursor, cursorCmd) = cursor.update(msg);
cursor = newCursor;
if (cursorCmd != null) cmds.add(cursorCmd);
// Avoid scheduling a blink command on every keypress. Cursor blinking is
// already driven by its own timer loop while focused.
_handleOverflow();
return (this, cmds.isNotEmpty ? Cmd.batch(cmds) : null);
});
}