resolveSelection function

ResolvedSelection? resolveSelection(
  1. Root root,
  2. String anchorFragmentId,
  3. int anchorOffset,
  4. String focusFragmentId,
  5. int focusOffset, {
  6. List<CaretStop>? cachedStops,
  7. List<LogicalLine>? cachedLines,
})

Resolves the selection defined by (anchorFragmentId, anchorOffset) → (focusFragmentId, focusOffset) in the root tree.

Returns null if:

  • anchor == focus (collapsed selection)
  • one of the fragments is not found in the tree
  • positions are not on the stop rail

Usage example:

final sel = resolveSelection(
  document.content,
  cursor.anchorId, cursor.anchorOffset,
  cursor.focusId,  cursor.focusOffset,
);
if (sel != null) {
  for (final node in sel.nodes) {
    print(node);
  }
}

Implementation

ResolvedSelection? resolveSelection(
  Root root,
  String anchorFragmentId,
  int anchorOffset,
  String focusFragmentId,
  int focusOffset, {
  List<CaretStop>? cachedStops,
  List<LogicalLine>? cachedLines,
}) {
  // Collapsed selection: nothing to resolve
  if (anchorFragmentId == focusFragmentId && anchorOffset == focusOffset) {
    return null;
  }

  // Build the stop rail to determine the order in the document.
  // Use cached values when provided (e.g. from document.caretStops /
  // document.logicalLines) to avoid O(n) rebuild on every key press.
  final stops = cachedStops ?? buildAllStops(root);
  final lines = cachedLines ?? buildAllLogicalLines(root);

  final anchorIdx = findStopIndex(stops, anchorFragmentId, anchorOffset);
  final focusIdx  = findStopIndex(stops, focusFragmentId,  focusOffset);

  if (anchorIdx < 0 || focusIdx < 0) return null;

  // Normalize: base = first in document, extent = after
  final baseIsAnchor = anchorIdx <= focusIdx;
  final baseIdx   = baseIsAnchor ? anchorIdx : focusIdx;
  final extentIdx = baseIsAnchor ? focusIdx  : anchorIdx;

  // Resolve the Fragments (HorizontalRule extends Fragment so it works directly).
  final anchorFragResolved = findById(root, anchorFragmentId);
  final focusFragResolved  = findById(root, focusFragmentId);
  if (anchorFragResolved is! Fragment || focusFragResolved is! Fragment) return null;

  // Resolve the containers of the two endpoints
  final anchorContainer = findLogicalContainer(root, anchorFragmentId);
  final focusContainer  = findLogicalContainer(root, focusFragmentId);
  if (anchorContainer == null || focusContainer == null) return null;

  final anchorEndpoint = SelectionEndpoint(
    fragment:  anchorFragResolved,
    offset:    anchorOffset,
    container: anchorContainer,
  );
  final focusEndpoint = SelectionEndpoint(
    fragment:  focusFragResolved,
    offset:    focusOffset,
    container: focusContainer,
  );

  final baseEndpoint   = baseIsAnchor ? anchorEndpoint : focusEndpoint;
  final extentEndpoint = baseIsAnchor ? focusEndpoint  : anchorEndpoint;

  // Find the LogicalLines involved
  // A line is involved if it contains at least one stop in the range [baseIdx, extentIdx]
  final selectedNodes = <SelectedNode>[];

  for (final line in lines) {
    // Check if the line has stop in the range
    bool hasStopInRange = false;
    for (final stop in line.stops) {
      final i = findStopIndex(stops, stop.fragmentId, stop.offset);
      if (i >= baseIdx && i <= extentIdx) {
        hasStopInRange = true;
        break;
      }
    }
    if (!hasStopInRange) continue;

    // Determine startFragment/startOffset for this line
    final Fragment startFrag;
    final int startOff;

    final lineContainerId = (line.node as FNode).id;
    final isBaseLine   = lineContainerId == (baseEndpoint.container as FNode).id;
    final isExtentLine = lineContainerId == (extentEndpoint.container as FNode).id;

    if (isBaseLine) {
      startFrag = baseEndpoint.fragment;
      startOff  = baseEndpoint.offset;
    } else {
      // Line completely selected from the start: take the first fragment
      final firstStop = line.stops.first;
      final firstNode = findById(root, firstStop.fragmentId);
      if (firstNode is! Fragment) continue;
      final frag = firstNode;
      startFrag = frag;
      startOff  = 0;
    }

    // Determine endFragment/endOffset for this line
    final Fragment endFrag;
    final int endOff;

    if (isExtentLine) {
      endFrag = extentEndpoint.fragment;
      endOff  = extentEndpoint.offset;
    } else {
      // Line completely selected until the end: take the last fragment
      final lastStop = line.stops.last;
      final lastNode = findById(root, lastStop.fragmentId);
      if (lastNode is! Fragment) continue;
      final frag = lastNode;
      endFrag = frag;
      endOff  = frag.text.length;
    }

    final isFullySelected = !isBaseLine && !isExtentLine;

    selectedNodes.add(SelectedNode(
      container:       line.node,
      startFragment:   startFrag,
      startOffset:     startOff,
      endFragment:     endFrag,
      endOffset:       endOff,
      isFullySelected: isFullySelected,
    ));
  }

  if (selectedNodes.isEmpty) return null;

  return ResolvedSelection(
    anchor: anchorEndpoint,
    focus:  focusEndpoint,
    base:   baseEndpoint,
    extent: extentEndpoint,
    nodes:  selectedNodes,
  );
}