Skip to content

Commit f3bf999

Browse files
Cherry Pick: [Spellcheck] Expose macOS spellcheck API's in dart (Resolves #2189) (#2231)
1 parent 4998442 commit f3bf999

15 files changed

+2206
-169
lines changed

super_editor_spellcheck/example/integration_test/plugin_integration_test.dart

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 189 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import 'dart:math';
2+
import 'dart:ui';
3+
14
import 'package:flutter/material.dart';
25
import 'dart:async';
36

4-
import 'package:flutter/services.dart';
57
import 'package:super_editor_spellcheck/super_editor_spellcheck.dart';
68

79
void main() {
@@ -16,48 +18,214 @@ class MyApp extends StatefulWidget {
1618
}
1719

1820
class _MyAppState extends State<MyApp> {
19-
String _platformVersion = 'Unknown';
20-
final _superEditorSpellcheckPlugin = SuperEditorSpellcheck();
21+
final _superEditorSpellcheckPlugin = SuperEditorSpellCheckerPlugin();
22+
final _textController = TextEditingController(text: 'She go to the store everys day.');
23+
24+
List<TextSuggestion> _suggestions = [];
25+
26+
TextRange _firstMispelledWord = TextRange.empty;
27+
List<String> _firstMispelledWordSuggestions = [];
28+
CheckGrammarResult? _grammarAnalysis;
29+
int? _wordCount;
30+
int? _documentTag;
31+
Map<String, String> _userReplacementsDictionary = <String, String>{};
32+
List<String> _completionsForLastWord = [];
33+
Timer? _searchTimer;
2134

2235
@override
2336
void initState() {
2437
super.initState();
25-
initPlatformState();
38+
39+
_textController.addListener(_onTextChanged);
40+
_fetchSuggestions();
2641
}
2742

28-
// Platform messages are asynchronous, so we initialize in an async method.
29-
Future<void> initPlatformState() async {
30-
String platformVersion;
31-
// Platform messages may fail, so we use a try/catch PlatformException.
32-
// We also handle the message potentially returning null.
33-
try {
34-
platformVersion =
35-
await _superEditorSpellcheckPlugin.getPlatformVersion() ?? 'Unknown platform version';
36-
} on PlatformException {
37-
platformVersion = 'Failed to get platform version.';
43+
@override
44+
void dispose() {
45+
_textController.removeListener(_onTextChanged);
46+
_textController.dispose();
47+
if (_documentTag != null) {
48+
_superEditorSpellcheckPlugin.macSpellChecker.closeSpellDocumentWithTag(_documentTag!);
3849
}
50+
super.dispose();
51+
}
52+
53+
void _onTextChanged() {
54+
_searchTimer?.cancel();
55+
_searchTimer = Timer(
56+
const Duration(milliseconds: 300),
57+
_fetchSuggestions,
58+
);
59+
}
60+
61+
Future<void> _fetchSuggestions() async {
62+
final textToSearch = _textController.text;
63+
final locale = PlatformDispatcher.instance.locale;
64+
65+
int? tag = _documentTag;
66+
tag ??= await _superEditorSpellcheckPlugin.macSpellChecker.uniqueSpellDocumentTag();
3967

40-
// If the widget was removed from the tree while the asynchronous platform
41-
// message was in flight, we want to discard the reply rather than calling
42-
// setState to update our non-existent appearance.
43-
if (!mounted) return;
68+
final language = _superEditorSpellcheckPlugin.macSpellChecker.convertDartLocaleToMacLanguageCode(locale)!;
69+
70+
final suggestions = await _superEditorSpellcheckPlugin.fetchSuggestions(
71+
locale,
72+
textToSearch,
73+
);
74+
75+
if (_shouldAbortCurrentSearch(textToSearch)) {
76+
return;
77+
}
78+
79+
final firstMisspelled = await _superEditorSpellcheckPlugin.macSpellChecker.checkSpelling(
80+
stringToCheck: textToSearch,
81+
startingOffset: 0,
82+
language: language,
83+
);
84+
85+
if (_shouldAbortCurrentSearch(textToSearch)) {
86+
return;
87+
}
88+
89+
final firstSuggestions = firstMisspelled.isValid
90+
? await _superEditorSpellcheckPlugin.macSpellChecker.guesses(
91+
range: firstMisspelled,
92+
text: textToSearch,
93+
language: language,
94+
)
95+
: <String>[];
96+
97+
if (_shouldAbortCurrentSearch(textToSearch)) {
98+
return;
99+
}
100+
101+
final grammarAnalysis = await _superEditorSpellcheckPlugin.macSpellChecker.checkGrammar(
102+
stringToCheck: textToSearch,
103+
startingOffset: 0,
104+
language: language,
105+
);
106+
107+
if (_shouldAbortCurrentSearch(textToSearch)) {
108+
return;
109+
}
110+
111+
final wordCount = await _superEditorSpellcheckPlugin.macSpellChecker.countWords(
112+
text: textToSearch,
113+
language: language,
114+
);
115+
116+
if (_shouldAbortCurrentSearch(textToSearch)) {
117+
return;
118+
}
119+
120+
final replacements = await _superEditorSpellcheckPlugin.macSpellChecker.userReplacementsDictionary();
121+
122+
if (_shouldAbortCurrentSearch(textToSearch)) {
123+
return;
124+
}
125+
126+
final completionOffset = max(textToSearch.lastIndexOf(' '), 0);
127+
128+
final completions = await _superEditorSpellcheckPlugin.macSpellChecker.completions(
129+
partialWordRange: TextRange(start: completionOffset, end: textToSearch.length),
130+
text: textToSearch,
131+
language: language,
132+
);
133+
134+
if (_shouldAbortCurrentSearch(textToSearch)) {
135+
return;
136+
}
44137

45138
setState(() {
46-
_platformVersion = platformVersion;
139+
_documentTag = tag;
140+
_suggestions = suggestions;
141+
_firstMispelledWord = firstMisspelled;
142+
_firstMispelledWordSuggestions = firstSuggestions;
143+
_grammarAnalysis = grammarAnalysis;
144+
_wordCount = wordCount;
145+
_userReplacementsDictionary = replacements;
146+
_completionsForLastWord = completions;
47147
});
48148
}
49149

150+
bool _shouldAbortCurrentSearch(String textToSearch) {
151+
if (!mounted) {
152+
return true;
153+
}
154+
155+
if (textToSearch != _textController.text) {
156+
// The user changed the text while the search was happening. Ignore the results,
157+
// because a new search will happen.
158+
return true;
159+
}
160+
161+
return false;
162+
}
163+
50164
@override
51165
Widget build(BuildContext context) {
52166
return MaterialApp(
53167
home: Scaffold(
54168
appBar: AppBar(
55169
title: const Text('Plugin example app'),
56170
),
57-
body: Center(
58-
child: Text('Running on: $_platformVersion\n'),
171+
body: Padding(
172+
padding: const EdgeInsets.all(8.0),
173+
child: Column(
174+
children: [
175+
TextField(
176+
controller: _textController,
177+
),
178+
if (_documentTag != null) //
179+
Text('Document tag: $_documentTag'),
180+
if (_wordCount != null) //
181+
Text('Word count: $_wordCount'),
182+
if (_firstMispelledWord.isValid)
183+
Text('First misspelled word: ${_textController.text.substring(
184+
_firstMispelledWord.start,
185+
_firstMispelledWord.end,
186+
)}'),
187+
if (_firstMispelledWordSuggestions.isNotEmpty)
188+
Text('Suggestions for first misspelled word: ${_firstMispelledWordSuggestions.join(', ')}'),
189+
const SizedBox(height: 10),
190+
_suggestions.isEmpty
191+
? const Text('No spelling errors found.')
192+
: Column(
193+
crossAxisAlignment: CrossAxisAlignment.start,
194+
children: _suggestions.map(_buildSuggestions).toList(),
195+
),
196+
if (_grammarAnalysis != null)
197+
Column(
198+
crossAxisAlignment: CrossAxisAlignment.start,
199+
children: _grammarAnalysis!.details.map(_buildGrammarAnalysis).toList(),
200+
),
201+
Column(
202+
crossAxisAlignment: CrossAxisAlignment.start,
203+
children: _userReplacementsDictionary.entries.map(_buildReplacement).toList(),
204+
),
205+
if (_completionsForLastWord.isNotEmpty)
206+
Text('Completions for last word: ${_completionsForLastWord.join(', ')}'),
207+
],
208+
),
59209
),
60210
),
61211
);
62212
}
213+
214+
Widget _buildSuggestions(TextSuggestion span) {
215+
return Text(
216+
'${_textController.text.substring(span.range.start, span.range.end)}: ${span.suggestions.join(', ')}',
217+
);
218+
}
219+
220+
Widget _buildGrammarAnalysis(GrammaticalAnalysisDetail? detail) {
221+
return Text(
222+
'${_textController.text.substring(detail!.range.start, detail.range.end)}: ${detail.userDescription}',
223+
);
224+
}
225+
226+
Widget _buildReplacement(MapEntry<String, String> entry) {
227+
return Text(
228+
'Replace ${entry.key} with ${entry.value}',
229+
);
230+
}
63231
}

super_editor_spellcheck/example/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
66
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
77
return true
88
}
9+
10+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
11+
return true
12+
}
913
}

0 commit comments

Comments
 (0)