Skip to content

Commit

Permalink
[SuperEditor] Fix crash with selection across nodes when using the ta…
Browse files Browse the repository at this point in the history
…gs plugin (Resolves #2479) (#2480)
  • Loading branch information
angelosilvestre authored and web-flow committed Jan 3, 2025
1 parent 1f08366 commit 800cbff
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 99 deletions.
114 changes: 45 additions & 69 deletions super_editor/lib/src/default_editor/text_tokenizing/action_tags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,26 +123,32 @@ class SubmitComposingActionTagCommand extends EditCommand {
void execute(EditContext context, CommandExecutor executor) {
final document = context.document;
final composer = context.find<MutableDocumentComposer>(Editor.composerKey);
if (composer.selection == null) {
final selection = composer.selection;

if (selection == null) {
return;
}

if (!selection.isCollapsed) {
// Action tags are composed while the user is typing. Since the
// selection is expanded, the user is not typing.
return;
}

final extent = composer.selection!.extent;
final extent = selection.extent;
final extentPosition = extent.nodePosition;
if (extentPosition is! TextNodePosition) {
return;
}

final textNode = document.getNodeById(extent.nodeId) as TextNode;

final normalizedSelection = composer.selection!.normalize(document);
final tagAroundPosition = _findTag(
final tagAroundPosition = _findTagUpstream(
// TODO: deal with these tag rules in requests and commands, should the user really pass them?
tagRule: defaultActionTagRule,
nodeId: composer.selection!.extent.nodeId,
text: textNode.text,
expansionPosition: extentPosition,
endPosition: normalizedSelection.end.nodePosition,
caretPosition: extentPosition,
isTokenCandidate: (attributions) => !attributions.contains(actionTagCancelledAttribution),
);

Expand Down Expand Up @@ -213,32 +219,25 @@ class CancelComposingActionTagCommand extends EditCommand {
return;
}

// Look for a composing tag at the extent, or the base.
if (!selection.isCollapsed) {
// Action tags are composed while the user is typing. Since the
// selection is expanded, the user is not typing.
return;
}

// Look for a composing tag at the extent.
final base = selection.base;
final extent = selection.extent;
TagAroundPosition? composingToken;
TextNode? textNode;

final normalizedSelection = selection.normalize(document);
if (base.nodePosition is TextNodePosition) {
textNode = document.getNodeById(selection.base.nodeId) as TextNode;
composingToken = _findTag(
tagRule: _tagRule,
nodeId: textNode.id,
text: textNode.text,
expansionPosition: base.nodePosition as TextNodePosition,
endPosition: normalizedSelection.end.nodePosition,
isTokenCandidate: (tokenAttributions) => tokenAttributions.contains(actionTagComposingAttribution),
);
}
if (composingToken == null && extent.nodePosition is TextNodePosition) {
if (extent.nodePosition is TextNodePosition) {
textNode = document.getNodeById(selection.extent.nodeId) as TextNode;
composingToken = _findTag(
composingToken = _findTagUpstream(
tagRule: _tagRule,
nodeId: textNode.id,
text: textNode.text,
expansionPosition: base.nodePosition as TextNodePosition,
endPosition: normalizedSelection.end.nodePosition,
caretPosition: base.nodePosition as TextNodePosition,
isTokenCandidate: (tokenAttributions) => tokenAttributions.contains(actionTagComposingAttribution),
);
}
Expand Down Expand Up @@ -293,7 +292,9 @@ class ActionTagComposingReaction extends EditReaction {

_healCancelledTags(requestDispatcher, document, changeList);

if (composer.selection == null) {
if (composer.selection?.isCollapsed != true) {
// Action tags are composed while the user is typing. Since the
// selection is either null or expanded, the user is not typing.
_cancelComposingTag(requestDispatcher);
editorContext.composingActionTag.value = null;
_onUpdateComposingActionTag(null);
Expand All @@ -302,33 +303,18 @@ class ActionTagComposingReaction extends EditReaction {

final selection = composer.selection!;

// Look for a composing tag at the extent, or the base.
final base = selection.base;
// Look for a composing tag at the extent.
final extent = selection.extent;
TagAroundPosition? tagAroundPosition;
TextNode? textNode;

final normalizedSelection = selection.normalize(document);

if (base.nodePosition is TextNodePosition) {
textNode = document.getNodeById(selection.base.nodeId) as TextNode;
tagAroundPosition = _findTag(
tagRule: _tagRule,
nodeId: textNode.id,
text: textNode.text,
expansionPosition: base.nodePosition as TextNodePosition,
endPosition: normalizedSelection.end.nodePosition,
isTokenCandidate: (attributions) => !attributions.contains(actionTagCancelledAttribution),
);
}
if (tagAroundPosition == null && extent.nodePosition is TextNodePosition) {
if (extent.nodePosition is TextNodePosition) {
textNode = document.getNodeById(selection.extent.nodeId) as TextNode;
tagAroundPosition = _findTag(
tagAroundPosition = _findTagUpstream(
tagRule: _tagRule,
nodeId: textNode.id,
text: textNode.text,
expansionPosition: extent.nodePosition as TextNodePosition,
endPosition: normalizedSelection.end.nodePosition,
caretPosition: extent.nodePosition as TextNodePosition,
isTokenCandidate: (attributions) => !attributions.contains(actionTagCancelledAttribution),
);
}
Expand Down Expand Up @@ -467,37 +453,35 @@ class ActionTagComposingReaction extends EditReaction {
}
}

/// Finds a tag that touches the given [expansionPosition], constaining it
/// to not cross the [endPosition].
/// Finds a tag that starts upstream to the [caretPosition] and ends
/// at the [caretPosition].
///
/// If [endPosition] is not a `TextNodePosition`, it will be ignored .
TagAroundPosition? _findTag({
/// For example, considering the following text, where '|' represents the caret:
///
/// "hello/wo|rld"
///
/// This method will extract "/wo" as the tag.
TagAroundPosition? _findTagUpstream({
required TagRule tagRule,
required String nodeId,
required AttributedText text,
required TextNodePosition expansionPosition,
required NodePosition endPosition,
required TextNodePosition caretPosition,
required bool Function(Set<Attribution> tokenAttributions) isTokenCandidate,
}) {
final rawText = text.text;
final rawText = text.toPlainText();
if (rawText.isEmpty) {
return null;
}

int splitIndex = min(expansionPosition.offset, rawText.length);
int splitIndex = min(caretPosition.offset, rawText.length);
splitIndex = max(splitIndex, 0);

final endOffset = endPosition is TextNodePosition ? endPosition.offset : null;

// Create 2 splits of characters to navigate upstream and downstream the caret position.
// ex: "this is a very|long string"
// -> split around the caret into charactersBefore="this is a very" and charactersAfter="long string"
// Extract the text upstream to the caret.
// For example: "hello/wor|ld"
// -> extracts the text "hello/wor"
final charactersBefore = rawText.substring(0, splitIndex).characters;
final iteratorUpstream = charactersBefore.iteratorAtEnd;

final charactersAfter = rawText.substring(splitIndex, endOffset).characters;
final iteratorDownstream = charactersAfter.iterator;

if (charactersBefore.isNotEmpty && tagRule.excludedCharacters.contains(charactersBefore.last)) {
// The character where we're supposed to begin our expansion is a
// character that's not allowed in a tag. Therefore, no tag exists
Expand All @@ -521,16 +505,8 @@ TagAroundPosition? _findTag({
}
}

// Move downstream the caret position until we find excluded character or reach the end of the text.
while (iteratorDownstream.moveNext()) {
final current = iteratorDownstream.current;
if (tagRule.excludedCharacters.contains(current)) {
break;
}
}

final tokenStartOffset = splitIndex - iteratorUpstream.stringAfterLength;
final tokenRange = SpanRange(tokenStartOffset, splitIndex + iteratorDownstream.stringBeforeLength);
final tokenRange = SpanRange(tokenStartOffset, splitIndex);

final tagText = text.substringInRange(tokenRange);
if (!tagText.startsWith(tagRule.trigger)) {
Expand All @@ -548,7 +524,7 @@ TagAroundPosition? _findTag({
nodeId,
tokenStartOffset,
),
searchOffset: expansionPosition.offset,
searchOffset: caretPosition.offset,
);
}

Expand Down
Loading

0 comments on commit 800cbff

Please sign in to comment.