update method

  1. @override
(TextInputModel, Cmd?) update(
  1. Msg msg
)
override

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);
  });
}