-
Notifications
You must be signed in to change notification settings - Fork 249
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
Changing AttributedSpans.collapseSpans to treat end positions as exclusive #642
base: main
Are you sure you want to change the base?
Conversation
7e9c46a
to
ddb1af5
Compare
It's true that inclusive end caps work differently than traditional string operations. Then again, we aren't doing traditional string operations. With regard to span operations, I made the decision for caps to be inclusive so that we avoid other points of confusion. For example, if a span covers a single character at index Additionally, I'm not convinced that making span ranges behave like typical string ranges is a good idea. This change might make some things more clear, but I think it also makes other things more confusing. I'm not sure this is a good tradeoff. Let me now if there's more to the argument than "span ranges don't behave like typical string ranges". If there's a fundamental problem, or if there's evidence that this behavior is adding non-trivial work to commonly re-occurring developer tasks, then maybe it's worth doing. If that's the case, I'd like to see such examples. If it's just off-by-one issues, those can either go one way or another. Adjusting things by |
To address
As a developer, I would expect test('SpanRange consistency', () {
const text = 'Hello there';
final attrText = AttributedText(text: text);
const range = SpanRange(start: 0, end: 5);
attrText.addAttribution(boldAttribution, range);
final substring = range.textInside(text);
for (var i = 0; i < attrText.text.length; i++) {
if (i < substring.length) {
expect(
attrText.getAllAttributionsAt(i),
contains(boldAttribution),
reason: 'Characters returned from textInside should be bold',
);
} else {
expect(
attrText.getAllAttributionsAt(i),
isNot(contains(boldAttribution)),
reason: 'Characters excluded from textInside should not be bold',
);
}
}
}); Output:
I agree that I disagree that having exclusive end indexes is inherently more confusing. It might be true in a vacuum, but this is a Dart library and for better or worse Dart was deliberately designed to be similar to other C-like languages, which includes exclusive end indexes. I think it is more confusing to learn Dart String, List, and maybe Flutter TextRange, and then suddenly have to contend with a different paradigm when using this library. Even once that paradigm is learned, mentally switching between vanilla Dart/Flutter and AttributedText index conventions adds extra cognitive overhead during development. One last thing I'll push back on:
This is true, but now it has to happen at least twice as often: once while you're calculating whatever your index is, and once moving in/out of this library to vanilla dart. More if you're crossing that boundary multiple times. Each one of these fiddly +/- 1 operations increases the risk of programmer error. The diff for this PR demonstrates how consistency with the rest of the ecosystem simplifies using |
Yeah, we definitely want consistency across
That's definitely an attractive point. If this were just about
To me, markers are like offsets. If you want the character at index Do you have any thoughts on improving marker clarity? If we can find a reasonable marker API that doesn't easily confuse the end index, then I'm open to making this change. |
If the sticking point here is specifically how
AttributedSpans({
List<AttributionSpan>? attributions,
}) : markers = (attributions ?? [])
.expand(
(span) => [
SpanMarker(attribution: span.attribution, offset: span.start, markerType: SpanMarkerType.start),
SpanMarker(attribution: span.attribution, offset: span.end, markerType: SpanMarkerType.end),
],
)
.toList() {
_sortAttributions();
} The old signature could also be retained as an internal constructor to make implementing certain things like AttributedSpans._fromMarkers({
List<SpanMarker>? attributions,
}) : markers = [...?attributions] {
_sortAttributions();
} Of course, this just pushes the problem elsewhere. I think I missed One last thought: if you want to make a really dramatic API change, Apple sidesteps this issue entirely in |
It sounds like the change is reasonable, and making the markers private should be OK. Changing First, I'd like to make sure that the API changes in this PR work throughout the mono repo. Can you run all tests locally for all projects to ensure that they pass? I think CI might use the public dependency versions, which won't pick up your changes, but I'm not sure about that. Also, given that we have plenty of holes in our test coverage, can you build and run each sub-project's example app and poke around for issues? This will be a breaking change, but it's important that we update all of our internal uses appropriately. For the spans, do you think it would be helpful for users to also be able to access the end index? We know the range will provide |
4545f89
to
7985a20
Compare
I don't think it would be that useful since most Dart developers should be used to dealing with exclusive end indexes anyway. But at the same time it's pretty trivial and Dart already encourages redundant getters like
Just did this,
Done, the tests pass locally. This seems like an issue worth addressing though. I'll open another PR to improve the monorepo setup for CI as well as local development shortly. |
This is true for spans, but not offsets. I think developers routinely specify, for example, a
Can you describe your intentions, first? I'd like to avoid any significant refactoring, if possible. |
SGTM
Sure, I'm just planning to use Melos. It's a command-line tool written specifically for managing Dart monorepos. You remove the If you want to avoid 3rd party tools we can still improve the CI situation by adding a step to each workflow that writes a Edit: here is the diff of what I have so far. I've actually been using Melos for development in this repo for a month or two without committing the file. The changes are mostly to various |
Sounds good. I was originally going to try that, but when I discovered the dependency overrides in the pubspec it was good enough at the time. If there are any new commands that we're supposed to run so that Melos does its thing, please add those to the README. Also, can you file an issue for this change and briefly describe what you're about to do, and why? |
c75dd9f
to
69b50a1
Compare
@jmatth now that we're introducing breaking changes, would you like to refresh this PR? I think we're ready to finalize this and merge it. |
Yeah, I'll rebase from |
69b50a1
to
3a7aef0
Compare
@matthew-carroll this should be ready to go. I poked around in the example apps and didn't find any issues. |
Currently,
AttributedSpans.collapseSpans
returns objects with anend
index that is inclusive. This is in contrast to every other list or string related method in Dart and Flutter, which use exclusive end indexes. This PR bringscollapseSpans
in line with the rest of the ecosystem by making theend
indexes in its return exclusive.There are currently multiple tests broken that I'm working my way through, but I wanted to open this PR to allow for discussion to take place in the meantime.The current behavior of
collapseSpans
produces output that is probably not what most users are expecting, and there are currently at least two open issues related to it: #593 and #632. The use of inclusive end indexes means that users have to perform annoying index manipulation to use the output with other methods such as substring. For example, naively printing the contents of each collapsed span drops a character at each boundary:Output:
This is also the case in
super_editor
, especially in the IME code, where incomingTextSelection
values need-1
applied to query attributions within a given range 1 2 3 4.Given the above, I think
collapseSpans
should be modified to return exclusive end indexes for better compatibility with the rest of the Dart/Flutter ecosystem. Doing so should probably be considered a breaking change, as any code written with the old behavior in mind would then contain off-by-one errors.These changes also resolve a couple of bugs I found while investigating this: one where
collapseSpans
would return a span where the end index was off by one, and one where it could throw aStateError
.Resloves #593, resolves #632.