Skip to content

Commit bc0cbc1

Browse files
authored
Add spell check TextSpan creation logic that doesn't rely on composing region (flutter#123481)
Fixes flutter#119534 by adding extra logic to allow the proper `TextSpan` trees to be build with spell check results that does not rely on the composing region. Also, refreshes the algorithm used to correct spell check results to improve cases where spell check results are out of date, but the composing region line does not help smooth over this fact by giving the framework extra time to update while a word is being composed (because a composing region line would cover up any flashing red underlines). ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] All existing and new tests are passing. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
1 parent 87392c2 commit bc0cbc1

File tree

2 files changed

+245
-147
lines changed

2 files changed

+245
-147
lines changed

packages/flutter/lib/src/widgets/spell_check.dart

Lines changed: 163 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
56
import 'package:flutter/painting.dart';
67
import 'package:flutter/services.dart'
78
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
@@ -108,71 +109,63 @@ class SpellCheckConfiguration {
108109
List<SuggestionSpan> _correctSpellCheckResults(
109110
String newText, String resultsText, List<SuggestionSpan> results) {
110111
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
111-
112112
int spanPointer = 0;
113113
int offset = 0;
114-
int foundIndex;
115-
int spanLength;
116-
SuggestionSpan currentSpan;
117-
SuggestionSpan adjustedSpan;
118-
String currentSpanText;
119-
String newSpanText = '';
120-
bool currentSpanValid = false;
121-
RegExp regex;
122114

123115
// Assumes that the order of spans has not been jumbled for optimization
124116
// purposes, and will only search since the previously found span.
125117
int searchStart = 0;
126118

127119
while (spanPointer < results.length) {
128-
// Try finding SuggestionSpan from old results (currentSpan) in new text.
129-
currentSpan = results[spanPointer];
130-
currentSpanText =
120+
final SuggestionSpan currentSpan = results[spanPointer];
121+
final String currentSpanText =
131122
resultsText.substring(currentSpan.range.start, currentSpan.range.end);
123+
final int spanLength = currentSpan.range.end - currentSpan.range.start;
132124

133-
try {
134-
// currentSpan was found and can be applied to new text.
135-
newSpanText = newText.substring(
136-
currentSpan.range.start + offset, currentSpan.range.end + offset);
137-
currentSpanValid = true;
138-
} catch (e) {
139-
// currentSpan is invalid and needs to be searched for in newText.
140-
currentSpanValid = false;
141-
}
125+
// Try finding SuggestionSpan from resultsText in new text.
126+
final RegExp currentSpanTextRegexp = RegExp('\\b$currentSpanText\\b');
127+
final int foundIndex = newText.substring(searchStart).indexOf(currentSpanTextRegexp);
128+
129+
// Check whether word was found exactly where expected or elsewhere in the newText.
130+
final bool currentSpanFoundExactly = currentSpan.range.start == foundIndex + searchStart;
131+
final bool currentSpanFoundExactlyWithOffset = currentSpan.range.start + offset == foundIndex + searchStart;
132+
final bool currentSpanFoundElsewhere = foundIndex >= 0;
142133

143-
if (currentSpanValid && newSpanText == currentSpanText) {
144-
// currentSpan was found at the same index in new text and old text
145-
// (resultsText), so apply it to new text by adding it to the list of
134+
if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) {
135+
// currentSpan was found at the same index in newText and resutsText
136+
// or at the same index with the previously calculated adjustment by
137+
// the offset value, so apply it to new text by adding it to the list of
146138
// corrected results.
147-
searchStart = currentSpan.range.end + offset;
148-
adjustedSpan = SuggestionSpan(
149-
TextRange(
150-
start: currentSpan.range.start + offset, end: searchStart),
151-
currentSpan.suggestions
139+
final SuggestionSpan adjustedSpan = SuggestionSpan(
140+
TextRange(
141+
start: currentSpan.range.start + offset,
142+
end: currentSpan.range.end + offset,
143+
),
144+
currentSpan.suggestions,
152145
);
146+
147+
// Start search for the next misspelled word at the end of currentSpan.
148+
searchStart = currentSpan.range.end + 1 + offset;
153149
correctedSpellCheckResults.add(adjustedSpan);
154-
} else {
155-
// Search for currentSpan in new text and if found, apply it to new text
156-
// by adding it to the list of corrected results.
157-
regex = RegExp('\\b$currentSpanText\\b');
158-
foundIndex = newText.substring(searchStart).indexOf(regex);
159-
160-
if (foundIndex >= 0) {
161-
foundIndex += searchStart;
162-
spanLength = currentSpan.range.end - currentSpan.range.start;
163-
searchStart = foundIndex + spanLength;
164-
adjustedSpan = SuggestionSpan(
165-
TextRange(start: foundIndex, end: searchStart),
166-
currentSpan.suggestions
167-
);
168-
offset = foundIndex - currentSpan.range.start;
150+
} else if (currentSpanFoundElsewhere) {
151+
// Word was pushed forward but not modified.
152+
final int adjustedSpanStart = searchStart + foundIndex;
153+
final int adjustedSpanEnd = adjustedSpanStart + spanLength;
154+
final SuggestionSpan adjustedSpan = SuggestionSpan(
155+
TextRange(start: adjustedSpanStart, end: adjustedSpanEnd),
156+
currentSpan.suggestions,
157+
);
169158

170-
correctedSpellCheckResults.add(adjustedSpan);
171-
}
159+
// Start search for the next misspelled word at the end of the
160+
// adjusted currentSpan.
161+
searchStart = adjustedSpanEnd + 1;
162+
// Adjust offset to reflect the difference between where currentSpan
163+
// was positioned in resultsText versus in newText.
164+
offset = adjustedSpanStart - currentSpan.range.start;
165+
correctedSpellCheckResults.add(adjustedSpan);
172166
}
173167
spanPointer++;
174168
}
175-
176169
return correctedSpellCheckResults;
177170
}
178171

@@ -201,31 +194,121 @@ TextSpan buildTextSpanWithSpellCheckSuggestions(
201194
value.text, spellCheckResultsText, spellCheckResultsSpans);
202195
}
203196

204-
return TextSpan(
197+
// We will draw the TextSpan tree based on the composing region, if it is
198+
// available.
199+
// TODO(camsim99): The two separate stratgies for building TextSpan trees
200+
// based on the availability of a composing region should be merged:
201+
// https://github.com/flutter/flutter/issues/124142.
202+
final bool shouldConsiderComposingRegion = defaultTargetPlatform == TargetPlatform.android;
203+
if (shouldConsiderComposingRegion) {
204+
return TextSpan(
205205
style: style,
206-
children: _buildSubtreesWithMisspelledWordsIndicated(
206+
children: _buildSubtreesWithComposingRegion(
207207
spellCheckResultsSpans,
208208
value,
209209
style,
210210
misspelledTextStyle,
211-
composingWithinCurrentTextRange
211+
composingWithinCurrentTextRange,
212+
),
213+
);
214+
}
215+
216+
return TextSpan(
217+
style: style,
218+
children: _buildSubtreesWithoutComposingRegion(
219+
spellCheckResultsSpans,
220+
value,
221+
style,
222+
misspelledTextStyle,
223+
value.selection.baseOffset,
224+
),
225+
);
226+
}
227+
228+
/// Builds the [TextSpan] tree for spell check without considering the composing
229+
/// region. Instead, uses the cursor to identify the word that's actively being
230+
/// edited and shouldn't be spell checked. This is useful for platforms and IMEs
231+
/// that don't use the composing region for the active word.
232+
List<TextSpan> _buildSubtreesWithoutComposingRegion(
233+
List<SuggestionSpan>? spellCheckSuggestions,
234+
TextEditingValue value,
235+
TextStyle? style,
236+
TextStyle misspelledStyle,
237+
int cursorIndex,
238+
) {
239+
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
240+
241+
int textPointer = 0;
242+
int currentSpanPointer = 0;
243+
int endIndex;
244+
final String text = value.text;
245+
final TextStyle misspelledJointStyle =
246+
style?.merge(misspelledStyle) ?? misspelledStyle;
247+
bool cursorInCurrentSpan = false;
248+
249+
// Add text interwoven with any misspelled words to the tree.
250+
if (spellCheckSuggestions != null) {
251+
while (textPointer < text.length &&
252+
currentSpanPointer < spellCheckSuggestions.length) {
253+
final SuggestionSpan currentSpan = spellCheckSuggestions[currentSpanPointer];
254+
255+
if (currentSpan.range.start > textPointer) {
256+
endIndex = currentSpan.range.start < text.length
257+
? currentSpan.range.start
258+
: text.length;
259+
textSpanTreeChildren.add(
260+
TextSpan(
261+
style: style,
262+
text: text.substring(textPointer, endIndex),
263+
)
264+
);
265+
textPointer = endIndex;
266+
} else {
267+
endIndex =
268+
currentSpan.range.end < text.length ? currentSpan.range.end : text.length;
269+
cursorInCurrentSpan = currentSpan.range.start <= cursorIndex && currentSpan.range.end >= cursorIndex;
270+
textSpanTreeChildren.add(
271+
TextSpan(
272+
style: cursorInCurrentSpan
273+
? style
274+
: misspelledJointStyle,
275+
text: text.substring(currentSpan.range.start, endIndex),
276+
)
277+
);
278+
279+
textPointer = endIndex;
280+
currentSpanPointer++;
281+
}
282+
}
283+
}
284+
285+
// Add any remaining text to the tree if applicable.
286+
if (textPointer < text.length) {
287+
textSpanTreeChildren.add(
288+
TextSpan(
289+
style: style,
290+
text: text.substring(textPointer, text.length),
212291
)
213292
);
293+
}
294+
295+
return textSpanTreeChildren;
214296
}
215297

216-
/// Builds [TextSpan] subtree for text with misspelled words.
217-
List<TextSpan> _buildSubtreesWithMisspelledWordsIndicated(
298+
/// Builds [TextSpan] subtree for text with misspelled words with logic based on
299+
/// a valid composing region.
300+
List<TextSpan> _buildSubtreesWithComposingRegion(
218301
List<SuggestionSpan>? spellCheckSuggestions,
219302
TextEditingValue value,
220303
TextStyle? style,
221304
TextStyle misspelledStyle,
222305
bool composingWithinCurrentTextRange) {
223-
final List<TextSpan> tsTreeChildren = <TextSpan>[];
306+
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
224307

225308
int textPointer = 0;
226-
int currSpanPointer = 0;
309+
int currentSpanPointer = 0;
227310
int endIndex;
228-
SuggestionSpan currSpan;
311+
SuggestionSpan currentSpan;
229312
final String text = value.text;
230313
final TextRange composingRegion = value.composing;
231314
final TextStyle composingTextStyle =
@@ -234,59 +317,59 @@ List<TextSpan> _buildSubtreesWithMisspelledWordsIndicated(
234317
final TextStyle misspelledJointStyle =
235318
style?.merge(misspelledStyle) ?? misspelledStyle;
236319
bool textPointerWithinComposingRegion = false;
237-
bool currSpanIsComposingRegion = false;
320+
bool currentSpanIsComposingRegion = false;
238321

239322
// Add text interwoven with any misspelled words to the tree.
240323
if (spellCheckSuggestions != null) {
241324
while (textPointer < text.length &&
242-
currSpanPointer < spellCheckSuggestions.length) {
243-
currSpan = spellCheckSuggestions[currSpanPointer];
325+
currentSpanPointer < spellCheckSuggestions.length) {
326+
currentSpan = spellCheckSuggestions[currentSpanPointer];
244327

245-
if (currSpan.range.start > textPointer) {
246-
endIndex = currSpan.range.start < text.length
247-
? currSpan.range.start
328+
if (currentSpan.range.start > textPointer) {
329+
endIndex = currentSpan.range.start < text.length
330+
? currentSpan.range.start
248331
: text.length;
249332
textPointerWithinComposingRegion =
250333
composingRegion.start >= textPointer &&
251334
composingRegion.end <= endIndex &&
252335
!composingWithinCurrentTextRange;
253336

254337
if (textPointerWithinComposingRegion) {
255-
_addComposingRegionTextSpans(tsTreeChildren, text, textPointer,
338+
_addComposingRegionTextSpans(textSpanTreeChildren, text, textPointer,
256339
composingRegion, style, composingTextStyle);
257-
tsTreeChildren.add(
340+
textSpanTreeChildren.add(
258341
TextSpan(
259342
style: style,
260-
text: text.substring(composingRegion.end, endIndex)
343+
text: text.substring(composingRegion.end, endIndex),
261344
)
262345
);
263346
} else {
264-
tsTreeChildren.add(
347+
textSpanTreeChildren.add(
265348
TextSpan(
266349
style: style,
267-
text: text.substring(textPointer, endIndex)
350+
text: text.substring(textPointer, endIndex),
268351
)
269352
);
270353
}
271354

272355
textPointer = endIndex;
273356
} else {
274357
endIndex =
275-
currSpan.range.end < text.length ? currSpan.range.end : text.length;
276-
currSpanIsComposingRegion = textPointer >= composingRegion.start &&
358+
currentSpan.range.end < text.length ? currentSpan.range.end : text.length;
359+
currentSpanIsComposingRegion = textPointer >= composingRegion.start &&
277360
endIndex <= composingRegion.end &&
278361
!composingWithinCurrentTextRange;
279-
tsTreeChildren.add(
362+
textSpanTreeChildren.add(
280363
TextSpan(
281-
style: currSpanIsComposingRegion
364+
style: currentSpanIsComposingRegion
282365
? composingTextStyle
283366
: misspelledJointStyle,
284-
text: text.substring(currSpan.range.start, endIndex)
367+
text: text.substring(currentSpan.range.start, endIndex),
285368
)
286369
);
287370

288371
textPointer = endIndex;
289-
currSpanPointer++;
372+
currentSpanPointer++;
290373
}
291374
}
292375
}
@@ -295,27 +378,27 @@ List<TextSpan> _buildSubtreesWithMisspelledWordsIndicated(
295378
if (textPointer < text.length) {
296379
if (textPointer < composingRegion.start &&
297380
!composingWithinCurrentTextRange) {
298-
_addComposingRegionTextSpans(tsTreeChildren, text, textPointer,
381+
_addComposingRegionTextSpans(textSpanTreeChildren, text, textPointer,
299382
composingRegion, style, composingTextStyle);
300383

301384
if (composingRegion.end != text.length) {
302-
tsTreeChildren.add(
385+
textSpanTreeChildren.add(
303386
TextSpan(
304387
style: style,
305-
text: text.substring(composingRegion.end, text.length)
388+
text: text.substring(composingRegion.end, text.length),
306389
)
307390
);
308391
}
309392
} else {
310-
tsTreeChildren.add(
393+
textSpanTreeChildren.add(
311394
TextSpan(
312-
style: style, text: text.substring(textPointer, text.length)
395+
style: style, text: text.substring(textPointer, text.length),
313396
)
314397
);
315398
}
316399
}
317400

318-
return tsTreeChildren;
401+
return textSpanTreeChildren;
319402
}
320403

321404
/// Helper method to create [TextSpan] tree children for specified range of
@@ -330,13 +413,13 @@ void _addComposingRegionTextSpans(
330413
treeChildren.add(
331414
TextSpan(
332415
style: style,
333-
text: text.substring(start, composingRegion.start)
416+
text: text.substring(start, composingRegion.start),
334417
)
335418
);
336419
treeChildren.add(
337420
TextSpan(
338421
style: composingTextStyle,
339-
text: text.substring(composingRegion.start, composingRegion.end)
422+
text: text.substring(composingRegion.start, composingRegion.end),
340423
)
341424
);
342425
}

0 commit comments

Comments
 (0)