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;
56import 'package:flutter/painting.dart' ;
67import 'package:flutter/services.dart'
78 show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
@@ -108,71 +109,63 @@ class SpellCheckConfiguration {
108109List <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