Skip to content

Commit

Permalink
Fix tags crash when first character typed is emoji (#4)
Browse files Browse the repository at this point in the history
* fixed compositag issue with consecutive trigger characters + added corresponding test

* removed useless import

* removed _previousVersion

* reorganized emoji tests and added more tests

* return findTagAroundPosition result directly

* fixes

* early return + comments

* try to improve comment
  • Loading branch information
BazinC authored Mar 14, 2024
1 parent 6a9db1d commit cb1832c
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 199 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,9 @@ class ComposingStableTag {

@override
int get hashCode => contentBounds.hashCode ^ token.hashCode;

@override
String toString() => 'ComposingStableTag{contentBounds: $contentBounds, token: $token}';
}

/// An [EditReaction] that prevents partial selection of a stable user tag.
Expand Down
137 changes: 23 additions & 114 deletions super_editor/lib/src/default_editor/text_tokenizing/tags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,140 +23,51 @@ class TagFinder {
return null;
}

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

if (tagRule.excludedCharacters.contains(rawText[splitIndex])) {
// The character where we're supposed to begin our expansion is a
// character that's not allowed in a tag. Therefore, no tag exists
// around the search offset.
return 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"
final charactersBefore = rawText.substring(0, splitIndex).characters;
final iteratorUpstream = charactersBefore.iteratorAtEnd;

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

var movedBack = iteratorUpstream.moveBack();

if (iteratorUpstream.current != tagRule.trigger) {
while (movedBack) {
final current = iteratorUpstream.current;
if (tagRule.excludedCharacters.contains(current)) {
// The upstream character isn't allowed to appear in a tag. Break before moving
// the starting character index any further upstream.
break;
}

if (current == tagRule.trigger) {
// The character we just added to the token bounds is the trigger.
// We include it and stop looking any further upstream
iteratorUpstream.moveBack();
break;
}
movedBack = iteratorUpstream.moveBack();
}
} else if (movedBack) {
iteratorUpstream.moveNext();
}

while (iteratorDownstream.moveNext()) {
final current = iteratorDownstream.current;
if (current != tagRule.trigger && tagRule.excludedCharacters.contains(current)) {
break;
}
}

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

final tagText = text.substringInRange(tokenRange);
if (!tagText.startsWith(tagRule.trigger)) {
return null;
}

final tokenAttributions = text.getAttributionSpansInRange(attributionFilter: (a) => true, range: tokenRange);
if (!isTokenCandidate(tokenAttributions.map((span) => span.attribution).toSet())) {
return null;
}

final tagAroundPosition = TagAroundPosition(
indexedTag: IndexedTag(
Tag(tagRule.trigger, tagText.substring(1)),
nodeId,
tokenStartOffset,
),
searchOffset: expansionPosition.offset,
);

return tagAroundPosition;
}

static TagAroundPosition? _previousVersionOffindTagAroundPosition({
required TagRule tagRule,
required String nodeId,
required AttributedText text,
required TextNodePosition expansionPosition,
required bool Function(Set<Attribution> tokenAttributions) isTokenCandidate,
}) {
final rawText = text.text;
if (rawText.isEmpty) {
return null;
}

int tokenStartOffset = min(expansionPosition.offset - 1, rawText.length - 1);
tokenStartOffset = max(tokenStartOffset, 0);
if (tagRule.excludedCharacters.contains(rawText[tokenStartOffset])) {
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
// around the search offset.
return null;
}

int tokenEndOffset = min(expansionPosition.offset - 1, rawText.length - 1);
tokenEndOffset = max(tokenEndOffset, 0);

if (rawText[tokenStartOffset] != tagRule.trigger) {
while (tokenStartOffset > 0) {
final upstreamCharacterIndex = rawText.moveOffsetUpstreamByCharacter(tokenStartOffset)!;
final upstreamCharacter = rawText[upstreamCharacterIndex];
if (tagRule.excludedCharacters.contains(upstreamCharacter)) {
// The upstream character isn't allowed to appear in a tag. Break before moving
// the starting character index any further upstream.
break;
}

// Move the starting character index upstream.
tokenStartOffset = upstreamCharacterIndex;
// Move upstream until we find the trigger character or an excluded character.
while (iteratorUpstream.moveBack()) {
final currentCharacter = iteratorUpstream.current;
if (tagRule.excludedCharacters.contains(currentCharacter)) {
// The upstream character isn't allowed to appear in a tag. end the search.
return null;
}

if (upstreamCharacter == tagRule.trigger) {
// The character we just added to the token bounds is the trigger.
// We don't want to move the start any further upstream.
break;
}
if (currentCharacter == tagRule.trigger) {
// The character we are reading is the trigger.
// We move the iteratorUpstream one last time to include the trigger in the tokenRange and stop looking any further upstream
iteratorUpstream.moveBack();
break;
}
}

while (tokenEndOffset < rawText.length - 1) {
final downstreamCharacterIndex = rawText.moveOffsetDownstreamByCharacter(tokenEndOffset)!;
final downstreamCharacter = rawText[downstreamCharacterIndex];
if (downstreamCharacter != tagRule.trigger && tagRule.excludedCharacters.contains(downstreamCharacter)) {
// 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 (current != tagRule.trigger && tagRule.excludedCharacters.contains(current)) {
break;
}

tokenEndOffset = downstreamCharacterIndex;
}
// Make end off exclusive.
tokenEndOffset += 1;

final tokenRange = SpanRange(tokenStartOffset, tokenEndOffset);
if (tokenRange.end - tokenRange.start <= 0) {
return null;
}
final tokenStartOffset = splitIndex - iteratorUpstream.stringAfterLength;
final tokenRange = SpanRange(tokenStartOffset, splitIndex + iteratorDownstream.stringBeforeLength);

final tagText = text.substringInRange(tokenRange);
if (!tagText.startsWith(tagRule.trigger)) {
Expand All @@ -168,16 +79,14 @@ class TagFinder {
return null;
}

final tagAroundPosition = TagAroundPosition(
return TagAroundPosition(
indexedTag: IndexedTag(
Tag(tagRule.trigger, tagText.substring(1)),
nodeId,
tokenStartOffset,
),
searchOffset: expansionPosition.offset,
);

return tagAroundPosition;
}

/// Finds and returns all tags in the given [textNode], which meet the given [rule].
Expand Down
Loading

0 comments on commit cb1832c

Please sign in to comment.