Skip to main content

inset_services/
spell_check.rs

1//! Flutter counterpart: `services/spell_check.dart`.
2
3use inset_embedder::{Locale, TextRange};
4use inset_foundation::App;
5
6/// A data structure representing a range of misspelled text and the suggested
7/// replacements for this range.
8///
9/// For example, one [`SuggestionSpan`] of the
10/// [`Vec<SuggestionSpan>`] suggestions of the [`SpellCheckResults`] corresponding
11/// to "Hello, wrold!" may be:
12///
13/// ```
14/// # use inset_embedder::TextRange;
15/// # use inset_services::SuggestionSpan;
16/// let suggestion_span = SuggestionSpan::new(
17///     TextRange::new(7, 12),
18///     vec!["word".into(), "world".into(), "old".into()],
19/// );
20/// ```
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub struct SuggestionSpan {
23    /// The misspelled range of text.
24    pub range: TextRange,
25    /// The alternate suggestions for the misspelled range of text.
26    pub suggestions: Vec<String>,
27}
28
29impl SuggestionSpan {
30    /// Creates a span representing a misspelled range of text and the replacements
31    /// suggested by a spell checker.
32    pub fn new(range: TextRange, suggestions: Vec<String>) -> SuggestionSpan {
33        SuggestionSpan { range, suggestions }
34    }
35}
36
37impl std::fmt::Display for SuggestionSpan {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(
40            f,
41            "SuggestionSpan(range: {:?}, suggestions: {:?})",
42            self.range, self.suggestions
43        )
44    }
45}
46
47/// A data structure grouping together the [`SuggestionSpan`]s and related text of
48/// results returned by a spell checker.
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
50pub struct SpellCheckResults {
51    /// The text that the [`suggestion_spans`](Self::suggestion_spans) correspond to.
52    pub spell_checked_text: String,
53    /// The spell check results of the [`spell_checked_text`](Self::spell_checked_text).
54    ///
55    /// See also:
56    ///
57    ///  * [`SuggestionSpan`], the ranges of misspelled text and corresponding
58    ///    replacement suggestions.
59    pub suggestion_spans: Vec<SuggestionSpan>,
60}
61
62impl SpellCheckResults {
63    /// Creates results based off those received by spell checking some text input.
64    pub fn new(
65        spell_checked_text: impl Into<String>,
66        suggestion_spans: Vec<SuggestionSpan>,
67    ) -> SpellCheckResults {
68        SpellCheckResults {
69            spell_checked_text: spell_checked_text.into(),
70            suggestion_spans,
71        }
72    }
73}
74
75impl std::fmt::Display for SpellCheckResults {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(
78            f,
79            "SpellCheckResults(spellCheckText: {}, suggestionSpans: {:?})",
80            self.spell_checked_text, self.suggestion_spans
81        )
82    }
83}
84
85/// Determines how spell check results are received for text input.
86pub trait SpellCheckService {
87    /// Facilitates a spell check request.
88    ///
89    /// Returns a [`Vec`] of [`SuggestionSpan`]s for all misspelled words in the
90    /// given [`str`] for the given [`Locale`].
91    ///
92    /// A return value of [`None`] indicates that fetching the spell check
93    /// suggestions was unsuccessful. If fetching the suggestions succeeded but
94    /// none were found, the result is an empty list.
95    fn fetch_spell_check_suggestions(
96        &self,
97        app: &App,
98        locale: &Locale,
99        text: &str,
100    ) -> Option<Vec<SuggestionSpan>>;
101}
102
103/// The service used by default to fetch spell check results for text input.
104///
105/// Any widget may use this service to spell check text by calling
106/// [`fetch_spell_check_suggestions`](Self::fetch_spell_check_suggestions) with an
107/// instance of this class. This is currently only supported by Android and iOS.
108///
109/// See also:
110///
111///  * [`SpellCheckService`], the service that this implements that may be
112///    overridden for use by `EditableText`.
113///  * `EditableText`, which may use this service to fetch results.
114#[derive(Clone, Debug, Default)]
115pub struct DefaultSpellCheckService {
116    /// The last received results from the shell side.
117    pub last_saved_results: Option<SpellCheckResults>,
118}
119
120impl DefaultSpellCheckService {
121    /// Creates service to spell check text input by default.
122    pub fn new() -> DefaultSpellCheckService {
123        DefaultSpellCheckService::default()
124    }
125
126    /// Merges two lists of spell check [`SuggestionSpan`]s.
127    ///
128    /// Used in cases where the text has not changed, but the spell check results
129    /// received from the shell side have. This case is caused by IMEs (GBoard,
130    /// for instance) that ignore the composing region when spell checking text.
131    ///
132    /// Assumes that the lists provided as parameters are sorted by range start
133    /// and that both list of [`SuggestionSpan`]s apply to the same text.
134    pub fn merge_results(
135        old_results: &[SuggestionSpan],
136        new_results: &[SuggestionSpan],
137    ) -> Vec<SuggestionSpan> {
138        let mut merged_results = Vec::new();
139
140        let mut old_span_pointer = 0;
141        let mut new_span_pointer = 0;
142
143        while old_span_pointer < old_results.len() && new_span_pointer < new_results.len() {
144            let old_span = &old_results[old_span_pointer];
145            let new_span = &new_results[new_span_pointer];
146
147            if old_span.range.start == new_span.range.start {
148                merged_results.push(old_span.clone());
149                old_span_pointer += 1;
150                new_span_pointer += 1;
151            } else if old_span.range.start < new_span.range.start {
152                merged_results.push(old_span.clone());
153                old_span_pointer += 1;
154            } else {
155                merged_results.push(new_span.clone());
156                new_span_pointer += 1;
157            }
158        }
159
160        merged_results.extend_from_slice(&old_results[old_span_pointer..]);
161        merged_results.extend_from_slice(&new_results[new_span_pointer..]);
162
163        merged_results
164    }
165}
166
167impl SpellCheckService for DefaultSpellCheckService {
168    fn fetch_spell_check_suggestions(
169        &self,
170        app: &App,
171        locale: &Locale,
172        text: &str,
173    ) -> Option<Vec<SuggestionSpan>> {
174        let _ = (app, locale, text);
175        None
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use inset_foundation::AppCell;
182
183    use super::*;
184
185    #[test]
186    fn suggestion_span_equality() {
187        let span = SuggestionSpan::new(
188            TextRange::new(7, 12),
189            vec!["word".into(), "world".into(), "old".into()],
190        );
191        assert_eq!(
192            span,
193            SuggestionSpan::new(
194                TextRange::new(7, 12),
195                vec!["word".into(), "world".into(), "old".into()],
196            )
197        );
198        assert_ne!(
199            span,
200            SuggestionSpan::new(TextRange::new(7, 12), vec!["world".into()])
201        );
202    }
203
204    #[test]
205    fn spell_check_results_equality() {
206        let span = SuggestionSpan::new(TextRange::new(7, 12), vec!["world".into()]);
207        let results = SpellCheckResults::new("Hello, wrold!", vec![span.clone()]);
208        assert_eq!(results, SpellCheckResults::new("Hello, wrold!", vec![span]));
209        assert_ne!(results, SpellCheckResults::new("Hello, world!", Vec::new()));
210    }
211
212    #[test]
213    fn merge_results_keeps_old_span_on_same_start() {
214        let old = vec![SuggestionSpan::new(
215            TextRange::new(0, 4),
216            vec!["old".into()],
217        )];
218        let new = vec![SuggestionSpan::new(
219            TextRange::new(0, 4),
220            vec!["new".into()],
221        )];
222        let merged = DefaultSpellCheckService::merge_results(&old, &new);
223        assert_eq!(merged, old);
224    }
225
226    #[test]
227    fn default_service_is_unsupported() {
228        let cell = AppCell::new();
229        let app = cell.borrow_mut();
230        let service = DefaultSpellCheckService::new();
231        assert!(
232            service
233                .fetch_spell_check_suggestions(&app, &Locale::new("en"), "wrold")
234                .is_none()
235        );
236    }
237}