resolveSelection function
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,
);
}