1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart'
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
import 'editable_text.dart' show EditableTextContextMenuBuilder;
import 'framework.dart' show immutable;
/// Controls how spell check is performed for text input.
///
/// This configuration determines the [SpellCheckService] used to fetch the
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
/// mark misspelled words within text input.
@immutable
class SpellCheckConfiguration {
/// Creates a configuration that specifies the service and suggestions handler
/// for spell check.
const SpellCheckConfiguration({
this.spellCheckService,
this.misspelledTextStyle,
this.spellCheckSuggestionsToolbarBuilder,
}) : _spellCheckEnabled = true;
/// Creates a configuration that disables spell check.
const SpellCheckConfiguration.disabled()
: _spellCheckEnabled = false,
spellCheckService = null,
spellCheckSuggestionsToolbarBuilder = null,
misspelledTextStyle = null;
/// The service used to fetch spell check results for text input.
final SpellCheckService? spellCheckService;
/// Style used to indicate misspelled words.
///
/// This is nullable to allow style-specific wrappers of [EditableText]
/// to infer this, but this must be specified if this configuration is
/// provided directly to [EditableText] or its construction will fail with an
/// assertion error.
final TextStyle? misspelledTextStyle;
/// Builds the toolbar used to display spell check suggestions for misspelled
/// words.
final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
final bool _spellCheckEnabled;
/// Whether or not the configuration should enable or disable spell check.
bool get spellCheckEnabled => _spellCheckEnabled;
/// Returns a copy of the current [SpellCheckConfiguration] instance with
/// specified overrides.
SpellCheckConfiguration copyWith({
SpellCheckService? spellCheckService,
TextStyle? misspelledTextStyle,
EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder}) {
if (!_spellCheckEnabled) {
// A new configuration should be constructed to enable spell check.
return const SpellCheckConfiguration.disabled();
}
return SpellCheckConfiguration(
spellCheckService: spellCheckService ?? this.spellCheckService,
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
spellCheckSuggestionsToolbarBuilder : spellCheckSuggestionsToolbarBuilder ?? this.spellCheckSuggestionsToolbarBuilder,
);
}
@override
String toString() {
return '''
spell check enabled : $_spellCheckEnabled
spell check service : $spellCheckService
misspelled text style : $misspelledTextStyle
spell check suggestions toolbar builder: $spellCheckSuggestionsToolbarBuilder
'''
.trim();
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is SpellCheckConfiguration
&& other.spellCheckService == spellCheckService
&& other.misspelledTextStyle == misspelledTextStyle
&& other.spellCheckSuggestionsToolbarBuilder == spellCheckSuggestionsToolbarBuilder
&& other._spellCheckEnabled == _spellCheckEnabled;
}
@override
int get hashCode => Object.hash(spellCheckService, misspelledTextStyle, spellCheckSuggestionsToolbarBuilder, _spellCheckEnabled);
}
// Methods for displaying spell check results:
/// Adjusts spell check results to correspond to [newText] if the only results
/// that the handler has access to are the [results] corresponding to
/// [resultsText].
///
/// Used in the case where the request for the spell check results of the
/// [newText] is lagging in order to avoid display of incorrect results.
List<SuggestionSpan> _correctSpellCheckResults(
String newText, String resultsText, List<SuggestionSpan> results) {
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
int spanPointer = 0;
int offset = 0;
// Assumes that the order of spans has not been jumbled for optimization
// purposes, and will only search since the previously found span.
int searchStart = 0;
while (spanPointer < results.length) {
final SuggestionSpan currentSpan = results[spanPointer];
final String currentSpanText =
resultsText.substring(currentSpan.range.start, currentSpan.range.end);
final int spanLength = currentSpan.range.end - currentSpan.range.start;
// Try finding SuggestionSpan from resultsText in new text.
final RegExp currentSpanTextRegexp = RegExp('\\b$currentSpanText\\b');
final int foundIndex = newText.substring(searchStart).indexOf(currentSpanTextRegexp);
// Check whether word was found exactly where expected or elsewhere in the newText.
final bool currentSpanFoundExactly = currentSpan.range.start == foundIndex + searchStart;
final bool currentSpanFoundExactlyWithOffset = currentSpan.range.start + offset == foundIndex + searchStart;
final bool currentSpanFoundElsewhere = foundIndex >= 0;
if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) {
// currentSpan was found at the same index in newText and resutsText
// or at the same index with the previously calculated adjustment by
// the offset value, so apply it to new text by adding it to the list of
// corrected results.
final SuggestionSpan adjustedSpan = SuggestionSpan(
TextRange(
start: currentSpan.range.start + offset,
end: currentSpan.range.end + offset,
),
currentSpan.suggestions,
);
// Start search for the next misspelled word at the end of currentSpan.
searchStart = currentSpan.range.end + 1 + offset;
correctedSpellCheckResults.add(adjustedSpan);
} else if (currentSpanFoundElsewhere) {
// Word was pushed forward but not modified.
final int adjustedSpanStart = searchStart + foundIndex;
final int adjustedSpanEnd = adjustedSpanStart + spanLength;
final SuggestionSpan adjustedSpan = SuggestionSpan(
TextRange(start: adjustedSpanStart, end: adjustedSpanEnd),
currentSpan.suggestions,
);
// Start search for the next misspelled word at the end of the
// adjusted currentSpan.
searchStart = adjustedSpanEnd + 1;
// Adjust offset to reflect the difference between where currentSpan
// was positioned in resultsText versus in newText.
offset = adjustedSpanStart - currentSpan.range.start;
correctedSpellCheckResults.add(adjustedSpan);
}
spanPointer++;
}
return correctedSpellCheckResults;
}
/// Builds the [TextSpan] tree given the current state of the text input and
/// spell check results.
///
/// The [value] is the current [TextEditingValue] requested to be rendered
/// by a text input widget. The [composingWithinCurrentTextRange] value
/// represents whether or not there is a valid composing region in the
/// [value]. The [style] is the [TextStyle] to render the [value]'s text with,
/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled
/// words within the [value]'s text with. The [spellCheckResults] are the
/// results of spell checking the [value]'s text.
TextSpan buildTextSpanWithSpellCheckSuggestions(
TextEditingValue value,
bool composingWithinCurrentTextRange,
TextStyle? style,
TextStyle misspelledTextStyle,
SpellCheckResults spellCheckResults) {
List<SuggestionSpan> spellCheckResultsSpans =
spellCheckResults.suggestionSpans;
final String spellCheckResultsText = spellCheckResults.spellCheckedText;
if (spellCheckResultsText != value.text) {
spellCheckResultsSpans = _correctSpellCheckResults(
value.text, spellCheckResultsText, spellCheckResultsSpans);
}
// We will draw the TextSpan tree based on the composing region, if it is
// available.
// TODO(camsim99): The two separate stratgies for building TextSpan trees
// based on the availability of a composing region should be merged:
// https://github.com/flutter/flutter/issues/124142.
final bool shouldConsiderComposingRegion = defaultTargetPlatform == TargetPlatform.android;
if (shouldConsiderComposingRegion) {
return TextSpan(
style: style,
children: _buildSubtreesWithComposingRegion(
spellCheckResultsSpans,
value,
style,
misspelledTextStyle,
composingWithinCurrentTextRange,
),
);
}
return TextSpan(
style: style,
children: _buildSubtreesWithoutComposingRegion(
spellCheckResultsSpans,
value,
style,
misspelledTextStyle,
value.selection.baseOffset,
),
);
}
/// Builds the [TextSpan] tree for spell check without considering the composing
/// region. Instead, uses the cursor to identify the word that's actively being
/// edited and shouldn't be spell checked. This is useful for platforms and IMEs
/// that don't use the composing region for the active word.
List<TextSpan> _buildSubtreesWithoutComposingRegion(
List<SuggestionSpan>? spellCheckSuggestions,
TextEditingValue value,
TextStyle? style,
TextStyle misspelledStyle,
int cursorIndex,
) {
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
int textPointer = 0;
int currentSpanPointer = 0;
int endIndex;
final String text = value.text;
final TextStyle misspelledJointStyle =
style?.merge(misspelledStyle) ?? misspelledStyle;
bool cursorInCurrentSpan = false;
// Add text interwoven with any misspelled words to the tree.
if (spellCheckSuggestions != null) {
while (textPointer < text.length &&
currentSpanPointer < spellCheckSuggestions.length) {
final SuggestionSpan currentSpan = spellCheckSuggestions[currentSpanPointer];
if (currentSpan.range.start > textPointer) {
endIndex = currentSpan.range.start < text.length
? currentSpan.range.start
: text.length;
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(textPointer, endIndex),
)
);
textPointer = endIndex;
} else {
endIndex =
currentSpan.range.end < text.length ? currentSpan.range.end : text.length;
cursorInCurrentSpan = currentSpan.range.start <= cursorIndex && currentSpan.range.end >= cursorIndex;
textSpanTreeChildren.add(
TextSpan(
style: cursorInCurrentSpan
? style
: misspelledJointStyle,
text: text.substring(currentSpan.range.start, endIndex),
)
);
textPointer = endIndex;
currentSpanPointer++;
}
}
}
// Add any remaining text to the tree if applicable.
if (textPointer < text.length) {
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(textPointer, text.length),
)
);
}
return textSpanTreeChildren;
}
/// Builds [TextSpan] subtree for text with misspelled words with logic based on
/// a valid composing region.
List<TextSpan> _buildSubtreesWithComposingRegion(
List<SuggestionSpan>? spellCheckSuggestions,
TextEditingValue value,
TextStyle? style,
TextStyle misspelledStyle,
bool composingWithinCurrentTextRange) {
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
int textPointer = 0;
int currentSpanPointer = 0;
int endIndex;
SuggestionSpan currentSpan;
final String text = value.text;
final TextRange composingRegion = value.composing;
final TextStyle composingTextStyle =
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
const TextStyle(decoration: TextDecoration.underline);
final TextStyle misspelledJointStyle =
style?.merge(misspelledStyle) ?? misspelledStyle;
bool textPointerWithinComposingRegion = false;
bool currentSpanIsComposingRegion = false;
// Add text interwoven with any misspelled words to the tree.
if (spellCheckSuggestions != null) {
while (textPointer < text.length &&
currentSpanPointer < spellCheckSuggestions.length) {
currentSpan = spellCheckSuggestions[currentSpanPointer];
if (currentSpan.range.start > textPointer) {
endIndex = currentSpan.range.start < text.length
? currentSpan.range.start
: text.length;
textPointerWithinComposingRegion =
composingRegion.start >= textPointer &&
composingRegion.end <= endIndex &&
!composingWithinCurrentTextRange;
if (textPointerWithinComposingRegion) {
_addComposingRegionTextSpans(textSpanTreeChildren, text, textPointer,
composingRegion, style, composingTextStyle);
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(composingRegion.end, endIndex),
)
);
} else {
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(textPointer, endIndex),
)
);
}
textPointer = endIndex;
} else {
endIndex =
currentSpan.range.end < text.length ? currentSpan.range.end : text.length;
currentSpanIsComposingRegion = textPointer >= composingRegion.start &&
endIndex <= composingRegion.end &&
!composingWithinCurrentTextRange;
textSpanTreeChildren.add(
TextSpan(
style: currentSpanIsComposingRegion
? composingTextStyle
: misspelledJointStyle,
text: text.substring(currentSpan.range.start, endIndex),
)
);
textPointer = endIndex;
currentSpanPointer++;
}
}
}
// Add any remaining text to the tree if applicable.
if (textPointer < text.length) {
if (textPointer < composingRegion.start &&
!composingWithinCurrentTextRange) {
_addComposingRegionTextSpans(textSpanTreeChildren, text, textPointer,
composingRegion, style, composingTextStyle);
if (composingRegion.end != text.length) {
textSpanTreeChildren.add(
TextSpan(
style: style,
text: text.substring(composingRegion.end, text.length),
)
);
}
} else {
textSpanTreeChildren.add(
TextSpan(
style: style, text: text.substring(textPointer, text.length),
)
);
}
}
return textSpanTreeChildren;
}
/// Helper method to create [TextSpan] tree children for specified range of
/// text up to and including the composing region.
void _addComposingRegionTextSpans(
List<TextSpan> treeChildren,
String text,
int start,
TextRange composingRegion,
TextStyle? style,
TextStyle composingTextStyle) {
treeChildren.add(
TextSpan(
style: style,
text: text.substring(start, composingRegion.start),
)
);
treeChildren.add(
TextSpan(
style: composingTextStyle,
text: text.substring(composingRegion.start, composingRegion.end),
)
);
}