From 114ae2b45dd7b7b4cf42bd8ac11df1e188f5cfa1 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 18:40:53 +0800 Subject: [PATCH] feat: compute cursor and selection by [Selection] or [Offset] --- .../example/lib/plugin/image_node_widget.dart | 26 +- .../lib/plugin/selected_text_node_widget.dart | 65 +++-- .../flowy_editor/lib/document/path.dart | 26 ++ .../flowy_editor/lib/document/position.dart | 7 + .../flowy_editor/lib/document/selection.dart | 23 +- .../lib/render/selection/selectable.dart | 13 +- .../arrow_keys_handler.dart | 2 +- .../delete_single_text_node_handler.dart | 4 +- .../shortcut_handler.dart | 14 +- .../lib/service/selection_service.dart | 230 ++++++++++++------ 10 files changed, 270 insertions(+), 140 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 8a9c96b22e..fc440a8fa5 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; @@ -38,27 +40,27 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { String get src => widget.node.attributes['image_src'] as String; @override - List getSelectionRectsInRange(Offset start, Offset end) { - final renderBox = context.findRenderObject() as RenderBox; - return [Offset.zero & renderBox.size]; + List getRectsInSelection(Selection selection) { + // TODO: implement getRectsInSelection + throw UnimplementedError(); } @override - Rect getCursorRect(Offset start) { - final renderBox = context.findRenderObject() as RenderBox; - final size = Size(2, renderBox.size.height); - final cursorOffset = Offset(renderBox.size.width, 0); - return cursorOffset & size; + Selection getSelectionInRange(Offset start, Offset end) { + // TODO: implement getSelectionInRange + throw UnimplementedError(); } @override - TextSelection? getCurrentTextSelection() { - return null; + Rect getCursorRectInPosition(Position position) { + // TODO: implement getCursorRectInPosition + throw UnimplementedError(); } @override - Offset getOffsetByTextSelection(TextSelection textSelection) { - return Offset.zero; + Position getPositionInOffset(Offset start) { + // TODO: implement getPositionInOffset + throw UnimplementedError(); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index b234ecd967..0f20f2fe3d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'package:example/plugin/debuggable_rich_text.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -56,49 +58,43 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> _textKey.currentContext?.findRenderObject() as RenderParagraph; @override - List getSelectionRectsInRange(Offset start, Offset end) { + Selection getSelectionInRange(Offset start, Offset end) { final localStart = _renderParagraph.globalToLocal(start); final localEnd = _renderParagraph.globalToLocal(end); - - var textSelection = - TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); - // Returns select all if the start or end exceeds the size of the box - // TODO: don't need to compute everytime. - var rects = _computeSelectionRects(textSelection); - _textSelection = textSelection; - - if (localEnd.dy > localStart.dy) { - // downward - if (localEnd.dy >= rects.last.bottom) { - return rects; - } - } else { - // upward - if (localEnd.dy <= rects.first.top) { - return rects; - } - } - - final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; - final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset; - textSelection = TextSelection( - baseOffset: selectionBaseOffset, - extentOffset: selectionExtentOffset, + final baseOffset = _getTextPositionAtOffset(localStart).offset; + final extentOffset = _getTextPositionAtOffset(localEnd).offset; + return Selection.single( + path: node.path, + startOffset: baseOffset, + endOffset: extentOffset, + ); + } + + @override + List getRectsInSelection(Selection selection) { + assert(pathEquals(selection.start.path, selection.end.path)); + assert(pathEquals(selection.start.path, node.path)); + final textSelection = TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, ); - _textSelection = textSelection; return _computeSelectionRects(textSelection); } @override - Rect getCursorRect(Offset start) { - final localStart = _renderParagraph.globalToLocal(start); - final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; - final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); + Rect getCursorRectInPosition(Position position) { + final textSelection = TextSelection.collapsed(offset: position.offset); _textSelection = textSelection; - print('text selection = $textSelection'); return _computeCursorRect(textSelection.baseOffset); } + @override + Position getPositionInOffset(Offset start) { + final localStart = _renderParagraph.globalToLocal(start); + final baseOffset = _getTextPositionAtOffset(localStart).offset; + return Position(path: node.path, offset: baseOffset); + } + @override TextSelection? getCurrentTextSelection() { return _textSelection; @@ -175,8 +171,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _renderParagraph.getPositionForOffset(offset); } - List _computeSelectionRects(TextSelection selection) { - final textBoxes = _renderParagraph.getBoxesForSelection(selection); + List _computeSelectionRects(TextSelection textSelection) { + final textBoxes = _renderParagraph.getBoxesForSelection(textSelection); return textBoxes.map((box) => box.toRect()).toList(); } @@ -185,7 +181,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); - print('offset = $offset, cursorHeight = $cursorHeight'); if (cursorHeight != null) { const cursorWidth = 2; return Rect.fromLTWH( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart index a8163f094d..bef96a7bd2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; typedef Path = List; @@ -5,3 +7,27 @@ typedef Path = List; bool pathEquals(Path path1, Path path2) { return listEquals(path1, path2); } + +/// Returns true if path1 >= path2, otherwise returns false. +/// TODO: Rename this function. +bool pathGreaterOrEquals(Path path1, Path path2) { + final length = min(path1.length, path2.length); + for (var i = 0; i < length; i++) { + if (path1[i] < path2[i]) { + return false; + } + } + return true; +} + +/// Returns true if path1 <= path2, otherwise returns false. +/// TODO: Rename this function. +bool pathLessOrEquals(Path path1, Path path2) { + final length = min(path1.length, path2.length); + for (var i = 0; i < length; i++) { + if (path1[i] > path2[i]) { + return false; + } + } + return true; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index 88941cd82e..e213c1eb33 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -24,4 +24,11 @@ class Position { final pathHash = hashList(path); return Object.hash(pathHash, offset); } + + Position copyWith({Path? path, int? offset}) { + return Position( + path: path ?? this.path, + offset: offset ?? this.offset, + ); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index dea3a2b752..fe60e1abec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -1,4 +1,5 @@ -import './position.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; class Selection { final Position start; @@ -9,9 +10,16 @@ class Selection { required this.end, }); - factory Selection.collapsed(Position pos) { - return Selection(start: pos, end: pos); - } + Selection.single({ + required Path path, + required int startOffset, + int? endOffset, + }) : start = Position(path: path, offset: startOffset), + end = Position(path: path, offset: endOffset ?? startOffset); + + Selection.collapsed(Position position) + : start = position, + end = position; Selection collapse({bool atStart = false}) { if (atStart) { @@ -24,4 +32,11 @@ class Selection { bool isCollapsed() { return start == end; } + + Selection copyWith({Position? start, Position? end}) { + return Selection( + start: start ?? this.start, + end: end ?? this.end, + ); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 6fc51049a1..f94d07e457 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; /// @@ -9,14 +11,17 @@ mixin Selectable on State { /// /// The return result must be a [List] of the [Rect] /// under the local coordinate system. - List getSelectionRectsInRange(Offset start, Offset end); + Selection getSelectionInRange(Offset start, Offset end); + + List getRectsInSelection(Selection selection); /// Returns a [Rect] for the offset in current widget. /// /// [start] is the offset of the global coordination system. /// /// The return result must be an offset of the local coordinate system. - Rect getCursorRect(Offset start); + Position getPositionInOffset(Offset start); + Rect getCursorRectInPosition(Position position); /// Returns a backward offset of the current offset based on the cause. Offset getBackwardOffset(/* Cause */); @@ -30,12 +35,12 @@ mixin Selectable on State { /// /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return null. - TextSelection? getCurrentTextSelection(); + TextSelection? getCurrentTextSelection() => null; /// For [TextNode] only. /// /// Retruns a [Offset]. /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return [Offset.zero]. - Offset getOffsetByTextSelection(TextSelection textSelection); + Offset getOffsetByTextSelection(TextSelection textSelection) => Offset.zero; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 3049f54453..44fc9a146f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -30,7 +30,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } final selectionService = editorState.service.selectionService; if (offset != null) { - selectionService.updateCursor(offset); + // selectionService.updateCursor(offset); return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart index db12d2bbb2..47a83f314a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart @@ -37,7 +37,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final newOfset = previousSelectable ?.getOffsetByTextSelection(newTextSelection); if (newOfset != null) { - selectionService.updateCursor(newOfset); + // selectionService.updateCursor(newOfset); } // merge TransactionBuilder(editorState) @@ -58,7 +58,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final selectionService = editorState.service.selectionService; final newOfset = selectable.getOffsetByTextSelection(newTextSelection); - selectionService.updateCursor(newOfset); + // selectionService.updateCursor(newOfset); return KeyEventResult.handled; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart index 4e52d1bbe9..3eef8c1d1b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart @@ -18,13 +18,13 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { final textNode = selectedNodes.first.unwrapOrNull(); final selectable = textNode?.key?.currentState?.unwrapOrNull(); final textSelection = selectable?.getCurrentTextSelection(); - if (textNode != null && selectable != null && textSelection != null) { - final offset = selectable.getOffsetByTextSelection(textSelection); - final rect = selectable.getCursorRect(offset); - editorState.service.floatingToolbarService - .showInOffset(rect.topLeft, textNode.layerLink); - return KeyEventResult.handled; - } + // if (textNode != null && selectable != null && textSelection != null) { + // final offset = selectable.getOffsetByTextSelection(textSelection); + // final rect = selectable.getCursorRect(offset); + // editorState.service.floatingToolbarService + // .showInOffset(rect.topLeft, textNode.layerLink); + // return KeyEventResult.handled; + // } return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index fa71536ecc..8f6ac6d6ee 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,3 +1,6 @@ +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; @@ -12,11 +15,8 @@ import '../render/selection/selectable.dart'; /// Process selection and cursor mixin FlowySelectionService on State { - /// [start] and [end] are the offsets under the global coordinate system. - void updateSelection(Offset start, Offset end); - - /// [start] is the offset under the global coordinate system. - void updateCursor(Offset start); + /// + void updateSelection(Selection selection); /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. @@ -26,18 +26,21 @@ mixin FlowySelectionService on State { /// /// If end is not null, it means multiple selection, /// otherwise single selection. - List getSelectedNodes(Offset start, [Offset? end]); + List getNodesInRange(Offset start, [Offset? end]); + + /// + List getNodesInSelection(Selection selection); /// Return the [Node] or [Null] in single selection. /// /// [start] is the offset under the global coordinate system. - Node? computeSelectedNodeInOffset(Node node, Offset offset); + Node? computeNodeInOffset(Node node, Offset offset); /// Return the [Node]s in multiple selection. Emtpy list would be returned /// if no nodes are in range. /// /// [start] is the offset under the global coordinate system. - List computeSelectedNodesInRange( + List computeNodesInRange( Node node, Offset start, Offset end, @@ -93,6 +96,10 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; + @override + List getNodesInSelection(Selection selection) => + _selectedNodesInSelection(editorState.document.root, selection); + @override Widget build(BuildContext context) { return RawGestureDetector( @@ -121,70 +128,23 @@ class _FlowySelectionState extends State } @override - void updateSelection(Offset start, Offset end) { + void updateSelection(Selection selection) { _clearAllOverlayEntries(); - final nodes = getSelectedNodes(start, end); - editorState.selectedNodes = nodes; - if (nodes.isEmpty) { - return; - } - - for (final node in nodes) { - if (node.key?.currentState is! Selectable) { - continue; - } - final selectable = node.key?.currentState as Selectable; - final selectionRects = selectable.getSelectionRectsInRange(start, end); - for (final rect in selectionRects) { - final overlay = OverlayEntry( - builder: ((context) => SelectionWidget( - color: widget.selectionColor, - layerLink: node.layerLink, - rect: rect, - )), - ); - _selectionOverlays.add(overlay); - } - } - Overlay.of(context)?.insertAll(_selectionOverlays); - } - - @override - void updateCursor(Offset start) { - _clearAllOverlayEntries(); - - final nodes = getSelectedNodes(start); - editorState.selectedNodes = nodes; - if (nodes.isEmpty) { - return; - } - - final selectedNode = nodes.first; - if (selectedNode.key?.currentState is! Selectable) { - return; - } - final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(start); - final cursor = OverlayEntry( - builder: ((context) => CursorWidget( - key: _cursorKey, - rect: rect, - color: widget.cursorColor, - layerLink: selectedNode.layerLink, - )), - ); - _cursorOverlays.add(cursor); - Overlay.of(context)?.insertAll(_cursorOverlays); - } - - @override - List getSelectedNodes(Offset start, [Offset? end]) { - if (end != null) { - return computeSelectedNodesInRange(editorState.document.root, start, end); + // cursor + if (selection.isCollapsed()) { + _updateCursor(selection.start); } else { - final reuslt = - computeSelectedNodeInOffset(editorState.document.root, start); + _updateSelection(selection); + } + } + + @override + List getNodesInRange(Offset start, [Offset? end]) { + if (end != null) { + return computeNodesInRange(editorState.document.root, start, end); + } else { + final reuslt = computeNodeInOffset(editorState.document.root, start); if (reuslt != null) { return [reuslt]; } @@ -193,9 +153,9 @@ class _FlowySelectionState extends State } @override - Node? computeSelectedNodeInOffset(Node node, Offset offset) { + Node? computeNodeInOffset(Node node, Offset offset) { for (final child in node.children) { - final result = computeSelectedNodeInOffset(child, offset); + final result = computeNodeInOffset(child, offset); if (result != null) { return result; } @@ -209,7 +169,7 @@ class _FlowySelectionState extends State } @override - List computeSelectedNodesInRange(Node node, Offset start, Offset end) { + List computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInSelection(node, start, end)) { @@ -217,7 +177,7 @@ class _FlowySelectionState extends State } } for (final child in node.children) { - result.addAll(computeSelectedNodesInRange(child, start, end)); + result.addAll(computeNodesInRange(child, start, end)); } // TODO: sort the result return result; @@ -254,7 +214,16 @@ class _FlowySelectionState extends State panStartOffset = null; panEndOffset = null; - updateCursor(tapOffset!); + final nodes = getNodesInRange(tapOffset!); + if (nodes.isNotEmpty) { + assert(nodes.length == 1); + final selectable = nodes.first.selectable; + if (selectable != null) { + final position = selectable.getPositionInOffset(tapOffset!); + final selection = Selection.collapsed(position); + updateSelection(selection); + } + } } void _onPanStart(DragStartDetails details) { @@ -271,7 +240,16 @@ class _FlowySelectionState extends State panEndOffset = details.globalPosition; tapOffset = null; - updateSelection(panStartOffset!, panEndOffset!); + final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + final first = nodes.first.selectable; + final last = nodes.last.selectable; + if (first != null && last != null) { + final selection = Selection( + start: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, + end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, + ); + updateSelection(selection); + } } void _onPanEnd(DragEndDetails details) { @@ -302,4 +280,106 @@ class _FlowySelectionState extends State ?.unwrapOrNull(); shortcutService?.hide(); } + + void _updateSelection(Selection selection) { + final nodes = + _selectedNodesInSelection(editorState.document.root, selection); + + var index = 0; + for (final node in nodes) { + final selectable = node.selectable; + if (selectable == null) { + continue; + } + + Selection newSelection; + if (node is TextNode) { + if (pathEquals(selection.start.path, selection.end.path)) { + newSelection = selection.copyWith(); + } else { + if (index == 0) { + newSelection = selection.copyWith( + /// FIXME: make it better. + end: selection.start.copyWith(offset: node.toRawString().length), + ); + } else if (index == nodes.length - 1) { + newSelection = selection.copyWith( + /// FIXME: make it better. + start: selection.end.copyWith(offset: 0), + ); + } else { + final position = Position(path: node.path); + newSelection = Selection( + start: position.copyWith(offset: 0), + end: position.copyWith(offset: node.toRawString().length), + ); + } + } + } else { + newSelection = Selection.collapsed( + Position(path: node.path), + ); + } + + final rects = selectable.getRectsInSelection(newSelection); + + for (final rect in rects) { + final overlay = OverlayEntry( + builder: ((context) => SelectionWidget( + color: widget.selectionColor, + layerLink: node.layerLink, + rect: rect, + )), + ); + _selectionOverlays.add(overlay); + } + index += 1; + } + Overlay.of(context)?.insertAll(_selectionOverlays); + } + + void _updateCursor(Position position) { + final node = _selectedNodeInPostion(editorState.document.root, position); + + assert(node != null); + if (node == null) { + return; + } + + final selectable = node.selectable; + final rect = selectable?.getCursorRectInPosition(position); + if (rect != null) { + final cursor = OverlayEntry( + builder: ((context) => CursorWidget( + key: _cursorKey, + rect: rect, + color: widget.cursorColor, + layerLink: node.layerLink, + )), + ); + _cursorOverlays.add(cursor); + Overlay.of(context)?.insertAll(_cursorOverlays); + } + } + + List _selectedNodesInSelection(Node node, Selection selection) { + List result = []; + if (node.parent != null) { + if (_isNodeInSelection(node, selection)) { + result.add(node); + } + } + for (final child in node.children) { + result.addAll(_selectedNodesInSelection(child, selection)); + } + return result; + } + + Node? _selectedNodeInPostion(Node node, Position position) => + node.childAtPath(position.path); + + bool _isNodeInSelection(Node node, Selection selection) { + return pathGreaterOrEquals(node.path, selection.start.path) && + pathLessOrEquals(node.path, selection.end.path); + } }