-
Notifications
You must be signed in to change notification settings - Fork 250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SuperEditor] Limit tag expansion to caret position (Resolves #2240, #2241, #2242) #2387
Conversation
@alterhuman @KevinBrendel Would you mind trying this PR? |
@angelosilvestre thanks for the PR Working correctly:
Issues:
_document = MutableDocument(nodes: [
ParagraphNode(
id: Editor.createNodeId(),
text: AttributedText("Some Infor/mation"),
),
]);
|
@alterhuman I fixed the first issue you mentioned. For the second issue, I'm not sure I followed. It doesn't seem to be related to any of the existing issues. |
@angelosilvestre tested the first issue, it's working correctly. This is the video of second issue, this text is already present in the document when it is opened: Screen.Recording.2024-10-31.at.6.16.39.PM.movFocusing on a word which already has a |
@alterhuman Is this happening on latest main? It doesn't look like it's related to the changes of this PR. If so, can you file a separate issue for this? |
@angelosilvestre yes this is also happening on main. Will file a separate issue for this. All good for this PR. |
@@ -10,11 +10,15 @@ import 'package:super_editor/src/default_editor/text.dart'; | |||
class TagFinder { | |||
/// Finds a tag that touches the given [expansionPosition] and returns that tag, | |||
/// indexed within the document, along with the [expansionPosition]. | |||
/// | |||
/// If [endPosition] is provided, the search will be limited to the range between |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this description of endPosition
. This method was intended to only look for tags that touch expansionPosition
, which is a singular position. There shouldn't be a range involved. So the idea of an endPosition
doesn't make sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So should we keep this method as it was before and modify the reaction to adjust the reported tag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point was that I don't know what you're trying to do here because it doesn't seem to make sense. I can't answer that question until I first understand what exactly you were trying to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I'm trying to do is to prevent the tag to expand beyond the caret position. For example, if we have the string "helloworld" and the user types a "/" at the middle, changing it to "hello/world", the string "world" shouldn't be attributed as a tag. The user can input a tag in the middle of a word, but the tag shouldn't expand beyond the caret position.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the feature goal. What I don't understand is the code that I commented on. I don't understand how/why achieving the feature goal that you described requires a caller to pass some thing called an "end position". Can you explain what this approach is doing and why it makes sense for the feature goal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@angelosilvestre just need an update whether this is in progress or stagnant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @alterhuman. I'm resuming the work on this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alterhuman Can you try this PR again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@angelosilvestre tested everything. All working correctly like previous solution except the previous remaining issue:
Action tag dropdown popping up after reopening a document and focus the caret on the text containing the tag rule /, after the slash. To reproduce:
Instead of having an empty document add this text:
_document = MutableDocument(nodes: [
ParagraphNode(
id: Editor.createNodeId(),
text: AttributedText("Some Infor/mation"),
),
]);
Focus caret at n, the dropdown pops up.
I have already filed a separate issue for this #2392
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthew-carroll been waiting for this PR to merge since a long time, can you please review this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@angelosilvestre found a crash/error while using it: How to ReproduceSelect a paragraph node completely (all text) and then continue the selection to another paragraph node above it. Or simply select multiple nodes partially. Error
at line 498 in action_tags.dart: of method _findTag: /// Finds a tag that touches the given [expansionPosition], constaining it
/// to not cross the [endPosition].
///
/// If [endPosition] is not a `TextNodePosition`, it will be ignored .
TagAroundPosition? _findTag({
required TagRule tagRule,
required String nodeId,
required AttributedText text,
required TextNodePosition expansionPosition,
required NodePosition endPosition,
required bool Function(Set<Attribution> tokenAttributions) isTokenCandidate,
}) {
final rawText = text.text;
if (rawText.isEmpty) {
return null;
}
int splitIndex = min(expansionPosition.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"
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
// around the search offset.
return null;
}
// 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 (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;
}
}
// 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 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;
}
return TagAroundPosition(
indexedTag: IndexedTag(
Tag(tagRule.trigger, tagText.substring(1)),
nodeId,
tokenStartOffset,
),
searchOffset: expansionPosition.offset,
);
} The stack trace points to the if (tagAroundPosition == null && extent.nodePosition is TextNodePosition) {
textNode = document.getNodeById(selection.extent.nodeId) as TextNode;
tagAroundPosition = _findTag(
tagRule: _tagRule,
nodeId: textNode.id,
text: textNode.text,
expansionPosition: extent.nodePosition as TextNodePosition,
endPosition: normalizedSelection.end.nodePosition,
isTokenCandidate: (attributions) => !attributions.contains(actionTagCancelledAttribution),
);
} @matthew-carroll would suggest reverting the merge until this issue is fixed. |
@angelosilvestre can you please file a follow up issue? |
@matthew-carroll Filed #2479 |
[SuperEditor] Limit tag expansion to caret position. Resolves #2240, #2241, #2242
Steps to reproduce:
The editor crashes with the following exception:
While investigating this, I noticed that typing "/" at the middle of the word causes all the text that comes after the "/" to be part of the action tag:
Screen.Recording.2024-10-29.at.20.23.32.mov
This doesn't look like is expected.
This PR changes this behavior. Now, the tag is expanded only until the caret position:
Screen.Recording.2024-10-29.at.20.25.46.mov
Changing this seems to already fix the issue, and also #2241 and #2242