Skip to content

Commit a84cfbd

Browse files
committed
WIP refactoring for document updating by NodePath. HR reaction updated, so it works within CompositeNode
1 parent bddc428 commit a84cfbd

8 files changed

Lines changed: 150 additions & 69 deletions

File tree

super_editor/example/lib/main_components_in_components.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,7 @@ class _BannerNode extends CompositeNode {
321321
throw UnimplementedError('Copy more than one child node is not yet implemented');
322322
}
323323

324-
@override
325-
CompositeNode copyAndReplaceLeaf({required CompositeNodePosition position, required DocumentNode newLeaf}) {
326-
return internalCopyAndReplaceLeaf(
327-
position: position,
328-
newLeaf: newLeaf,
329-
compositeNodeBuilder: (old, newChildren) {
330-
return _BannerNode(id: old.id, metadata: metadata, children: newChildren);
331-
},
332-
);
324+
CompositeNode copyWithChildren(List<DocumentNode> newChildren) {
325+
return _BannerNode(id: id, metadata: metadata, children: newChildren);
333326
}
334327
}

super_editor/lib/src/core/document.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ abstract class Document implements Iterable<DocumentNode> {
5353
/// has the given [nodeId], or `-1` if the node does not exist.
5454
int getNodeIndexById(String nodeId);
5555

56+
/// Returns the index of the `DocumentNode` in parent node. If this
57+
/// is a root node, then returns the index in this `Document`.
58+
int getNodeIndexInParent(NodePath path);
59+
5660
/// Returns the [DocumentNode] that appears immediately before the
5761
/// given [node] in this [Document], or null if the given [node]
5862
/// is the first node, or the given [node] does not exist in this
@@ -147,9 +151,9 @@ class NodePath {
147151

148152
bool get isRoot => _path.length == 1;
149153

150-
String get rootId => _path.first;
154+
String get rootNodeId => _path.first;
151155

152-
String get leafId => _path.last;
156+
String get leafNodeId => _path.last;
153157

154158
NodePath? toLeafParentPath() {
155159
if (length > 1) {
@@ -160,7 +164,7 @@ class NodePath {
160164

161165
@override
162166
String toString() {
163-
return isRoot ? rootId : _path.join('.');
167+
return isRoot ? rootNodeId : _path.join('.');
164168
}
165169

166170
@override
@@ -184,6 +188,10 @@ class NodePath {
184188

185189
@override
186190
int get hashCode => _path.hashCode;
191+
192+
NodePath append(String childId) {
193+
return NodePath([..._path, childId]);
194+
}
187195
}
188196

189197
/// Listener that's notified when a document changes.
@@ -225,9 +233,9 @@ abstract class NodeDocumentChange extends DocumentChange {
225233
const NodeDocumentChange();
226234

227235
@Deprecated('Use rootNodeId instead or nodePath')
228-
String get nodeId => nodePath.rootId;
236+
String get nodeId => nodePath.rootNodeId;
229237

230-
String get rootNodeId => nodePath.rootId;
238+
String get rootNodeId => nodePath.rootNodeId;
231239

232240
NodePath get nodePath;
233241
}
@@ -380,7 +388,7 @@ class DocumentPosition {
380388
for (var i = nodePath.length - 1; i > 0; i -= 1) {
381389
resultPosition = CompositeNodePosition(nodePath.getAt(i), resultPosition);
382390
}
383-
return DocumentPosition(nodeId: nodePath.rootId, nodePosition: resultPosition);
391+
return DocumentPosition(nodeId: nodePath.rootNodeId, nodePosition: resultPosition);
384392
}
385393

386394
/// ID of a [DocumentNode] within a [Document].

super_editor/lib/src/core/editor.dart

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,7 @@ class MutableDocument with Iterable<DocumentNode> implements Document, Editable
12101210

12111211
@override
12121212
DocumentNode? getNodeAtPath(NodePath path) {
1213-
var node = getNodeById(path.rootId);
1213+
var node = getNodeById(path.rootNodeId);
12141214
for (var i = 1; i < path.length; i += 1) {
12151215
final childId = path.getAt(i);
12161216
assert(
@@ -1222,14 +1222,27 @@ class MutableDocument with Iterable<DocumentNode> implements Document, Editable
12221222
return node;
12231223
}
12241224

1225+
@override
1226+
int getNodeIndexInParent(NodePath path) {
1227+
final parentPath = path.toLeafParentPath();
1228+
if (parentPath == null) {
1229+
return getNodeIndexById(path.rootNodeId);
1230+
}
1231+
final parent = getNodeAtPath(parentPath) as CompositeNode;
1232+
return parent.getChildIndexByNodeId(path.leafNodeId);
1233+
}
1234+
12251235
@override
12261236
DocumentNode? getLeafNode(DocumentPosition position) => getNodeAtPath(NodePath.withDocumentPosition(position));
12271237

12281238
@override
12291239
CompositeNode? getLeafNodeParent(DocumentPosition position) {
12301240
final path = NodePath.withDocumentPosition(position).toLeafParentPath();
1231-
final node = path != null ? getNodeAtPath(path) : null;
1232-
assert(node is CompositeNode, 'Unexpected Leaf Parent Node ${node.runtimeType}. CompositeNode expected');
1241+
if (path == null) {
1242+
return null;
1243+
}
1244+
final node = getNodeAtPath(path);
1245+
assert(node is CompositeNode, 'Unexpected Leaf Parent Node "${node.runtimeType}". CompositeNode expected');
12331246
return node as CompositeNode;
12341247
}
12351248

@@ -1383,20 +1396,56 @@ class MutableDocument with Iterable<DocumentNode> implements Document, Editable
13831396
}
13841397
}
13851398

1399+
void replaceNodeByPath(NodePath path, DocumentNode newNode) {
1400+
var replacement = newNode;
1401+
if (!path.isRoot) {
1402+
final node = getNodeById(path.rootNodeId);
1403+
assert(node is CompositeNode);
1404+
replacement = (node as CompositeNode).copyAndReplaceLeafChildren(
1405+
nodePath: path.toLeafParentPath()!,
1406+
childrenReplacer: (CompositeNode leafNode, List<DocumentNode> children) {
1407+
return children.map((c) => c.id == path.leafNodeId ? newNode : c).toList();
1408+
},
1409+
);
1410+
}
1411+
return replaceNodeById(path.rootNodeId, replacement);
1412+
}
1413+
13861414
/// Replaces leaf node based on [position]. All CompositeNodes are copied and
13871415
/// given newNode is replaced by its id
13881416
void replaceLeafNodeByPosition(DocumentPosition position, DocumentNode newNode) {
1389-
final nodePosition = position.nodePosition;
1390-
var replacement = newNode;
1391-
if (nodePosition is CompositeNodePosition) {
1392-
final node = getNodeById(position.nodeId);
1417+
return replaceNodeByPath(NodePath.withDocumentPosition(position), newNode);
1418+
}
1419+
1420+
/// Inserts [newNode] immediately after the given [existingNode].
1421+
void insertNodeAfterPath({
1422+
required NodePath existingNodePath,
1423+
required DocumentNode newNode,
1424+
}) {
1425+
if (existingNodePath.isRoot) {
1426+
return insertNodeAfter(
1427+
existingNodeId: existingNodePath.rootNodeId,
1428+
newNode: newNode,
1429+
);
1430+
} else {
1431+
final leafParentPath = existingNodePath.toLeafParentPath()!;
1432+
final leafParent = getNodeAtPath(leafParentPath) as CompositeNode;
1433+
final insertIndex = leafParent.getChildIndexByNodeId(existingNodePath.leafNodeId);
1434+
1435+
final node = getNodeById(existingNodePath.rootNodeId);
13931436
assert(node is CompositeNode);
1394-
replacement = (node as CompositeNode).copyAndReplaceLeaf(
1395-
position: nodePosition,
1396-
newLeaf: newNode,
1437+
1438+
final replacement = (node as CompositeNode).copyAndReplaceLeafChildren(
1439+
nodePath: leafParentPath,
1440+
childrenReplacer: (CompositeNode leafNode, List<DocumentNode> children) {
1441+
final newChildren = List.of(children);
1442+
newChildren.insert(insertIndex, newNode);
1443+
return newChildren;
1444+
},
13971445
);
1446+
1447+
replaceNodeById(existingNodePath.rootNodeId, replacement);
13981448
}
1399-
return replaceNodeById(position.nodeId, replacement);
14001449
}
14011450

14021451
/// Returns [true] if the content of the [other] [Document] is equivalent

super_editor/lib/src/default_editor/common_editor_operations.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -930,10 +930,6 @@ class CommonEditorOperations {
930930
return true;
931931
}
932932

933-
// TODO: Implement support for CompositeNode
934-
if (composer.selection!.extent.nodePosition is UpstreamDownstreamNodePosition) {
935-
final nodePosition = composer.selection!.extent.nodePosition as UpstreamDownstreamNodePosition;
936-
if (nodePosition.affinity == TextAffinity.upstream) {
937933
final extentLeafPosition = composer.selection!.extent.leafNodePosition;
938934
if (extentLeafPosition is UpstreamDownstreamNodePosition) {
939935
if (extentLeafPosition.affinity == TextAffinity.upstream) {

super_editor/lib/src/default_editor/default_document_editor.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ final defaultRequestHandlers = List.unmodifiable(<EditRequestHandler>[
168168
? InsertNodeAtIndexCommand(nodeIndex: editor.document.length, newNode: request.newNode)
169169
: null,
170170
(editor, request) => request is InsertNodeAtIndexRequest
171-
? InsertNodeAtIndexCommand(nodeIndex: request.nodeIndex, newNode: request.newNode)
171+
? InsertNodeAtIndexCommand(
172+
parentPath: request.parentPath,
173+
nodeIndex: request.nodeIndex,
174+
newNode: request.newNode,
175+
)
172176
: null,
173177
(editor, request) => request is InsertNodeBeforeNodeRequest
174178
? InsertNodeBeforeNodeCommand(existingNodeId: request.existingNodeId, newNode: request.newNode)

super_editor/lib/src/default_editor/default_document_editor_reactions.dart

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:super_editor/src/core/editor.dart';
1313
import 'package:super_editor/src/default_editor/attributions.dart';
1414
import 'package:super_editor/src/default_editor/horizontal_rule.dart';
1515
import 'package:super_editor/src/default_editor/image.dart';
16+
import 'package:super_editor/src/default_editor/layout_single_column/composite_nodes.dart';
1617
import 'package:super_editor/src/default_editor/list_items.dart';
1718
import 'package:super_editor/src/default_editor/paragraph.dart';
1819
import 'package:super_editor/src/default_editor/tasks.dart';
@@ -314,7 +315,8 @@ class HorizontalRuleConversionReaction extends EditReaction {
314315
}
315316

316317
final textInsertionEvent = edit.change as TextInsertionEvent;
317-
final paragraph = document.getNodeById(textInsertionEvent.nodeId) as TextNode;
318+
final paragraphPath = textInsertionEvent.nodePath;
319+
final paragraph = document.getNodeAtPath(paragraphPath) as TextNode;
318320
final match = _hrPattern.firstMatch(paragraph.text.toPlainText())?.group(0);
319321
if (match == null) {
320322
return;
@@ -327,20 +329,21 @@ class HorizontalRuleConversionReaction extends EditReaction {
327329
requestDispatcher.execute([
328330
DeleteContentRequest(
329331
documentRange: DocumentRange(
330-
start: DocumentPosition(nodeId: paragraph.id, nodePosition: const TextNodePosition(offset: 0)),
331-
end: DocumentPosition(nodeId: paragraph.id, nodePosition: TextNodePosition(offset: match.length)),
332+
start: DocumentPosition.withPath(nodePath: paragraphPath, nodePosition: const TextNodePosition(offset: 0)),
333+
end: DocumentPosition.withPath(nodePath: paragraphPath, nodePosition: TextNodePosition(offset: match.length)),
332334
),
333335
),
334336
InsertNodeAtIndexRequest(
335-
nodeIndex: document.getNodeIndexById(paragraph.id),
337+
parentPath: paragraphPath.toLeafParentPath(),
338+
nodeIndex: document.getNodeIndexInParent(paragraphPath),
336339
newNode: HorizontalRuleNode(
337340
id: Editor.createNodeId(),
338341
),
339342
),
340343
ChangeSelectionRequest(
341344
DocumentSelection.collapsed(
342-
position: DocumentPosition(
343-
nodeId: paragraph.id,
345+
position: DocumentPosition.withPath(
346+
nodePath: paragraphPath,
344347
nodePosition: const TextNodePosition(offset: 0),
345348
),
346349
),
@@ -573,7 +576,7 @@ class LinkifyReaction extends EditReaction {
573576
final edit = edits[i];
574577
if (edit is DocumentEdit) {
575578
final change = edit.change;
576-
if (change is TextInsertionEvent && change.text.toPlainText() == " " && change.nodePath.isRoot) {
579+
if (change is TextInsertionEvent && change.text.toPlainText() == " ") {
577580
// Every space insertion might appear after a URL.
578581
linkifyCandidate = change;
579582
didInsertSpace = true;
@@ -597,14 +600,14 @@ class LinkifyReaction extends EditReaction {
597600
}
598601

599602
final caretPosition = selection.extent;
600-
if (caretPosition.nodeId != linkifyCandidate.nodeId) {
603+
if (caretPosition.leafNodeId != linkifyCandidate.nodePath.leafNodeId) {
601604
// The selection moved to some other node. Don't linkify.
602605
linkifyCandidate = null;
603606
continue;
604607
}
605608

606609
// +1 for the inserted space
607-
if ((caretPosition.nodePosition as TextNodePosition).offset != linkifyCandidate.offset + 1) {
610+
if ((caretPosition.leafNodePosition as TextNodePosition).offset != linkifyCandidate.offset + 1) {
608611
// The caret isn't sitting directly after the space. Whatever
609612
// these events represent, it doesn't represent the user typing
610613
// a URL and then press SPACE. Don't linkify.
@@ -614,7 +617,7 @@ class LinkifyReaction extends EditReaction {
614617

615618
// The caret sits directly after an inserted space. Get the word before
616619
// the space from the document, and linkify, if it fits a schema.
617-
final textNode = document.getNodeById(linkifyCandidate.nodeId) as TextNode;
620+
final textNode = document.getNodeAtPath(linkifyCandidate.nodePath) as TextNode;
618621
_extractUpstreamWordAndLinkify(textNode.text, linkifyCandidate.offset);
619622
} else if ((edit is SubmitParagraphIntention && edit.isStart) ||
620623
(edit is SplitParagraphIntention && edit.isStart) ||
@@ -631,7 +634,7 @@ class LinkifyReaction extends EditReaction {
631634

632635
final nextEdit = edits[i + 1];
633636
if (nextEdit is DocumentEdit && nextEdit.change is NodeChangeEvent) {
634-
final editedNode = document.getNodeById((nextEdit.change as NodeChangeEvent).nodeId);
637+
final editedNode = document.getNodeAtPath((nextEdit.change as NodeChangeEvent).nodePath);
635638
if (editedNode is TextNode) {
636639
_extractUpstreamWordAndLinkify(editedNode.text, editedNode.text.length);
637640
}
@@ -1117,11 +1120,11 @@ class EditInspector {
11171120
return false;
11181121
}
11191122

1120-
if (lastSelectionChangeEvent.newSelection!.extent.nodeId != textInsertionEvent.nodeId) {
1123+
if (lastSelectionChangeEvent.newSelection!.extent.leafNodeId != textInsertionEvent.nodePath.leafNodeId) {
11211124
return false;
11221125
}
11231126

1124-
final editedNode = document.getNodeById(textInsertionEvent.nodeId)!;
1127+
final editedNode = document.getNodeAtPath(textInsertionEvent.nodePath)!;
11251128
if (editedNode is! TextNode) {
11261129
return false;
11271130
}

super_editor/lib/src/default_editor/layout_single_column/composite_nodes.dart

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -260,31 +260,43 @@ abstract class CompositeNode extends DocumentNode implements ImeNodeSerializatio
260260
);
261261
}
262262

263-
/// Returns a new CompositeNode where leaf at [position] is replaced by
264-
/// provided [newLeaf]
265-
CompositeNode copyAndReplaceLeaf({
266-
required CompositeNodePosition position,
267-
required DocumentNode newLeaf,
268-
});
263+
CompositeNode copyWithChildren(List<DocumentNode> children);
269264

270-
CompositeNode internalCopyAndReplaceLeaf({
271-
required CompositeNodePosition position,
272-
required DocumentNode newLeaf,
273-
required CompositeNode Function(CompositeNode oldNode, List<DocumentNode> children) compositeNodeBuilder,
265+
/// Find a [CompositeNode] specified by [nodePath] and replace it's children using [childrenReplacer] function,
266+
/// then returns a new root [CompositeNode]
267+
CompositeNode copyAndReplaceLeafChildren({
268+
required NodePath nodePath,
269+
required List<DocumentNode> Function(CompositeNode leafNode, List<DocumentNode> leafChildren) childrenReplacer,
274270
}) {
275-
DocumentNode childReplacement;
276-
final childPosition = position.childNodePosition;
277-
if (childPosition is CompositeNodePosition) {
278-
final child = getChildByNodeId(position.childNodeId);
279-
assert(child is CompositeNode, 'Unexpected child type for CompositeNodePosition (${child.runtimeType})');
280-
childReplacement = (child as CompositeNode).copyAndReplaceLeaf(position: childPosition, newLeaf: newLeaf);
281-
} else {
282-
childReplacement = newLeaf;
271+
assert(
272+
nodePath.rootNodeId == id,
273+
'Invalid NodePathIterator state. `current` should point to children of current CompositeNode',
274+
);
275+
276+
var stack = <CompositeNode>[];
277+
var currentNode = this;
278+
stack.add(currentNode);
279+
for (var i = 1; i < nodePath.length; i += 1) {
280+
final childId = nodePath.getAt(i);
281+
final child = currentNode.getChildByNodeId(childId);
282+
assert(
283+
child is CompositeNode,
284+
'nodePath should be path to leaf parent, but $childId is not CompositeNode (${child.runtimeType})',
285+
);
286+
currentNode = child as CompositeNode;
287+
stack.add(currentNode);
283288
}
284-
final newChildren = children.map((child) {
285-
return child.id == childReplacement.id ? childReplacement : child;
286-
}).toList();
287-
return compositeNodeBuilder(this, newChildren);
289+
290+
var adjustedNode = currentNode.copyWithChildren(
291+
childrenReplacer(currentNode, currentNode.children.toList()),
292+
);
293+
for (var i = stack.length - 2; i >= 0; i -= 1) {
294+
final child = stack[i];
295+
adjustedNode =
296+
child.copyWithChildren(child.children.map((c) => c.id == adjustedNode.id ? adjustedNode : c).toList());
297+
}
298+
299+
return adjustedNode;
288300
}
289301

290302
@override
@@ -504,7 +516,7 @@ extension DocumentPositionCompositeEx on DocumentPosition {
504516

505517
NodePath get nodePath => NodePath.withDocumentPosition(this);
506518

507-
String get leafNodeId => nodePath.leafId;
519+
String get leafNodeId => nodePath.leafNodeId;
508520
}
509521

510522
extension NodePositionCompositeEx on NodePosition {

0 commit comments

Comments
 (0)