blob: 86113dba850bb9471d4a6c5d29c3a22bdd98eced [file] [log] [blame]
// 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 'dart:ui';
import 'package:flutter/foundation.dart';
import 'system_channels.dart';
/// A data structure representing a range of misspelled text and the suggested
/// replacements for this range.
///
/// For example, one [SuggestionSpan] of the
/// [List<SuggestionSpan>] suggestions of the [SpellCheckResults] corresponding
/// to "Hello, wrold!" may be:
/// ```dart
/// SuggestionSpan suggestionSpan =
/// SuggestionSpan(
/// const TextRange(start: 7, end: 12),
/// List<String>.of(<String>['word', 'world', 'old']),
/// );
/// ```
@immutable
class SuggestionSpan {
/// Creates a span representing a misspelled range of text and the replacements
/// suggested by a spell checker.
///
/// The [range] and replacement [suggestions] must all not
/// be null.
const SuggestionSpan(this.range, this.suggestions)
: assert(range != null),
assert(suggestions != null);
/// The misspelled range of text.
final TextRange range;
/// The alternate suggestions for the misspelled range of text.
final List<String> suggestions;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is SuggestionSpan &&
other.range.start == range.start &&
other.range.end == range.end &&
listEquals<String>(other.suggestions, suggestions);
}
@override
int get hashCode => Object.hash(range.start, range.end, Object.hashAll(suggestions));
}
/// A data structure grouping together the [SuggestionSpan]s and related text of
/// results returned by a spell checker.
@immutable
class SpellCheckResults {
/// Creates results based off those received by spell checking some text input.
const SpellCheckResults(this.spellCheckedText, this.suggestionSpans)
: assert(spellCheckedText != null),
assert(suggestionSpans != null);
/// The text that the [suggestionSpans] correspond to.
final String spellCheckedText;
/// The spell check results of the [spellCheckedText].
///
/// See also:
///
/// * [SuggestionSpan], the ranges of misspelled text and corresponding
/// replacement suggestions.
final List<SuggestionSpan> suggestionSpans;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is SpellCheckResults &&
other.spellCheckedText == spellCheckedText &&
listEquals<SuggestionSpan>(other.suggestionSpans, suggestionSpans);
}
@override
int get hashCode => Object.hash(spellCheckedText, Object.hashAll(suggestionSpans));
}
/// Determines how spell check results are received for text input.
abstract class SpellCheckService {
/// Facilitates a spell check request.
///
/// Returns a [Future] that resolves with a [List] of [SuggestionSpan]s for
/// all misspelled words in the given [String] for the given [Locale].
Future<List<SuggestionSpan>?> fetchSpellCheckSuggestions(
Locale locale, String text
);
}
/// The service used by default to fetch spell check results for text input.
///
/// Any widget may use this service to spell check text by calling
/// `fetchSpellCheckSuggestions(locale, text)` with an instance of this class.
/// This is currently only supported by Android.
///
/// See also:
///
/// * [SpellCheckService], the service that this implements that may be
/// overridden for use by [EditableText].
/// * [EditableText], which may use this service to fetch results.
class DefaultSpellCheckService implements SpellCheckService {
/// Creates service to spell check text input by default via communication
/// over the spell check [MethodChannel].
DefaultSpellCheckService() {
spellCheckChannel = SystemChannels.spellCheck;
}
/// The last received results from the shell side.
SpellCheckResults? lastSavedResults;
/// The channel used to communicate with the shell side to complete spell
/// check requests.
late MethodChannel spellCheckChannel;
/// Merges two lists of spell check [SuggestionSpan]s.
///
/// Used in cases where the text has not changed, but the spell check results
/// received from the shell side have. This case is caused by IMEs (GBoard,
/// for instance) that ignore the composing region when spell checking text.
///
/// Assumes that the lists provided as parameters are sorted by range start
/// and that both list of [SuggestionSpan]s apply to the same text.
static List<SuggestionSpan> mergeResults(
List<SuggestionSpan> oldResults, List<SuggestionSpan> newResults) {
final List<SuggestionSpan> mergedResults = <SuggestionSpan>[];
SuggestionSpan oldSpan;
SuggestionSpan newSpan;
int oldSpanPointer = 0;
int newSpanPointer = 0;
while (oldSpanPointer < oldResults.length &&
newSpanPointer < newResults.length) {
oldSpan = oldResults[oldSpanPointer];
newSpan = newResults[newSpanPointer];
if (oldSpan.range.start == newSpan.range.start) {
mergedResults.add(oldSpan);
oldSpanPointer++;
newSpanPointer++;
} else {
if (oldSpan.range.start < newSpan.range.start) {
mergedResults.add(oldSpan);
oldSpanPointer++;
} else {
mergedResults.add(newSpan);
newSpanPointer++;
}
}
}
mergedResults.addAll(oldResults.sublist(oldSpanPointer));
mergedResults.addAll(newResults.sublist(newSpanPointer));
return mergedResults;
}
@override
Future<List<SuggestionSpan>?> fetchSpellCheckSuggestions(
Locale locale, String text) async {
assert(locale != null);
assert(text != null);
final List<dynamic> rawResults;
final String languageTag = locale.toLanguageTag();
try {
rawResults = await spellCheckChannel.invokeMethod(
'SpellCheck.initiateSpellCheck',
<String>[languageTag, text],
) as List<dynamic>;
} catch (e) {
// Spell check request canceled due to pending request.
return null;
}
List<SuggestionSpan> suggestionSpans = <SuggestionSpan>[];
for (final dynamic result in rawResults) {
final Map<String, dynamic> resultMap =
Map<String,dynamic>.from(result as Map<dynamic, dynamic>);
suggestionSpans.add(
SuggestionSpan(
TextRange(
start: resultMap['startIndex'] as int,
end: resultMap['endIndex'] as int),
(resultMap['suggestions'] as List<dynamic>).cast<String>(),
)
);
}
if (lastSavedResults != null) {
// Merge current and previous spell check results if between requests,
// the text has not changed but the spell check results have.
final bool textHasNotChanged = lastSavedResults!.spellCheckedText == text;
final bool spansHaveChanged =
listEquals(lastSavedResults!.suggestionSpans, suggestionSpans);
if (textHasNotChanged && spansHaveChanged) {
suggestionSpans = mergeResults(lastSavedResults!.suggestionSpans, suggestionSpans);
}
}
lastSavedResults = SpellCheckResults(text, suggestionSpans);
return suggestionSpans;
}
}