cursorOnlyUpdate method

void cursorOnlyUpdate()

Notifies listeners for a cursor/selection-only change that does NOT mutate the document structure or text. Crucially it does NOT invalidate the content-derived caches (node index, caret-stop rail), so repeated arrow navigation reuses them instead of rebuilding O(n) on every press.

Listeners that are expensive on cursor-only changes can read cursorOnlyChange (true for the duration of this synchronous call) and skip unnecessary work.

Implementation

void cursorOnlyUpdate() {
  final sw = Stopwatch()..start();
  _cursorOnlyChange = true;

  // Pre-compute expensive lookups once before notifying 20+ widgets.
  // Each widget previously called findLogicalContainerId + isNodeSelected
  // + getSelectionRangeForNode independently — O(visible × n).
  // Now it's O(n) once, then O(1) per widget.
  _cachedCursorContainerId = cursor.focusId.isNotEmpty
      ? findLogicalContainerId(cursor.focusId)
      : null;

  _cachedSelectedNodeIds.clear();
  _cachedSelectionRanges.clear();
  if (selectionManager.hasSelection) {
    for (final node in _content.nodes) {
      final nodeId = node.id;
      if (selectionManager.isNodeSelected(nodeId)) {
        _cachedSelectedNodeIds.add(nodeId);
        final range = getSelectionRangeForNode(nodeId);
        if (range != null) {
          _cachedSelectionRanges[nodeId] = range;
        }
      }
    }
  }

  cursor.notifyListeners();
  sw.stop();
  print('[CURSOR_ONLY] cursor.notifyListeners=${sw.elapsedMicroseconds}μs');
  sw.reset();
  sw.start();
  notifyListeners();
  sw.stop();
  print('[CURSOR_ONLY] doc.notifyListeners=${sw.elapsedMicroseconds}μs');
  _cursorOnlyChange = false;
}