11use crate :: Error ;
22
3- use std:: ops:: Deref ;
4- use std:: slice;
5- use std:: str;
6- use std:: sync:: { Mutex , OnceLock } ;
7-
8- use cocoa:: {
9- appkit:: NSSpellChecker ,
10- base:: { id, nil, NO } ,
11- foundation:: { NSInteger , NSNotFound , NSRange , NSString , NSUInteger } ,
12- } ;
13- use objc:: { msg_send, sel, sel_impl} ;
14-
15- fn checker ( ) -> & ' static Mutex < NSSpellCheckerWrapper > {
16- static CHECKER : OnceLock < Mutex < NSSpellCheckerWrapper > > = OnceLock :: new ( ) ;
17- CHECKER . get_or_init ( || {
18- Mutex :: new ( unsafe { NSSpellCheckerWrapper ( NSSpellChecker :: sharedSpellChecker ( nil) ) } )
19- } )
3+ use std:: ptr;
4+ use std:: sync:: Mutex ;
5+
6+ use objc2:: rc:: Retained ;
7+ use objc2_app_kit:: NSSpellChecker ;
8+ use objc2_foundation:: { NSInteger , NSNotFound , NSRange , NSString } ;
9+
10+ /// `NSSpellChecker` is not thread-safe; serialize access.
11+ fn with_checker < R > ( f : impl FnOnce ( & NSSpellChecker ) -> R ) -> R {
12+ static LOCK : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
13+ let _lock = LOCK . lock ( ) . unwrap ( ) ;
14+ f ( & NSSpellChecker :: sharedSpellChecker ( ) )
2015}
2116
22- fn ns_string ( s : & str ) -> id {
23- unsafe { NSString :: alloc ( nil ) . init_str ( s ) }
17+ fn ns_string ( s : & str ) -> Retained < NSString > {
18+ NSString :: from_str ( s )
2419}
2520
26- fn language_id ( language : & Option < String > ) -> id {
27- match language {
28- Some ( tag) => ns_string ( tag) ,
29- None => nil,
30- }
21+ fn language_ref ( language : & Option < String > ) -> Option < Retained < NSString > > {
22+ language. as_ref ( ) . map ( |tag| ns_string ( tag) )
3123}
3224
33- fn nsarray_to_strings ( array : id , max : usize ) -> Vec < String > {
34- if array . is_null ( ) {
25+ fn nsarray_to_strings ( array : Option < & objc2_foundation :: NSArray < NSString > > , max : usize ) -> Vec < String > {
26+ let Some ( array ) = array else {
3527 return Vec :: new ( ) ;
28+ } ;
29+ let take = ( array. count ( ) as usize ) . min ( max) ;
30+ let mut out = Vec :: with_capacity ( take) ;
31+ for i in 0 ..take {
32+ out. push ( array. objectAtIndex ( i as _ ) . to_string ( ) ) ;
3633 }
37- unsafe {
38- let count: NSUInteger = msg_send ! [ array, count] ;
39- let take = ( count as usize ) . min ( max) ;
40- let mut out = Vec :: with_capacity ( take) ;
41- for i in 0 ..take {
42- let item: id = msg_send ! [ array, objectAtIndex: i] ;
43- if item. is_null ( ) {
44- continue ;
45- }
46- let bytes = item. UTF8String ( ) as * const u8 ;
47- let len = item. len ( ) ;
48- if let Ok ( s) = str:: from_utf8 ( slice:: from_raw_parts ( bytes, len) ) {
49- out. push ( s. to_owned ( ) ) ;
50- }
51- }
52- out
53- }
34+ out
5435}
5536
56- /// `NSSpellChecker` is not thread safe. It should only be used from one thread, or it will cause
57- /// spurious `EXC_BAD_ACCESS` errors. If access to it is synchronized, however, it should be safe
58- /// to send across threads.
59- struct NSSpellCheckerWrapper ( id ) ;
60-
61- unsafe impl Send for NSSpellCheckerWrapper { }
62-
63- impl Deref for NSSpellCheckerWrapper {
64- type Target = id ;
65-
66- fn deref ( & self ) -> & id {
67- & self . 0
37+ fn utf16_offset_to_utf8 ( s : & str , utf16_units : usize ) -> usize {
38+ let mut units = 0 ;
39+ for ( byte_idx, ch) in s. char_indices ( ) {
40+ if units >= utf16_units {
41+ return byte_idx;
42+ }
43+ units += ch. len_utf16 ( ) ;
6844 }
45+ s. len ( )
6946}
7047
7148#[ derive( Debug ) ]
@@ -77,31 +54,25 @@ pub struct Checker {
7754
7855impl Drop for Checker {
7956 fn drop ( & mut self ) {
80- unsafe {
81- checker ( )
82- . lock ( )
83- . unwrap ( )
84- . closeSpellDocumentWithTag ( self . document_tag )
85- } ;
57+ let tag = self . document_tag ;
58+ with_checker ( |c| c. closeSpellDocumentWithTag ( tag) ) ;
8659 }
8760}
8861
8962impl Checker {
9063 pub fn new ( ) -> Result < Self , Error > {
9164 Ok ( Self {
92- document_tag : unsafe { NSSpellChecker :: uniqueSpellDocumentTag ( nil ) } ,
65+ document_tag : NSSpellChecker :: uniqueSpellDocumentTag ( ) ,
9366 language : None ,
9467 } )
9568 }
9669
9770 pub fn with_locale ( _hunspell : & str , bcp47 : & str ) -> Result < Self , Error > {
98- // Soft validation: reject empty; unavailable languages still may “work”
99- // with system fallback — zz_ZZ is only strictly Err on Unix.
10071 if bcp47. is_empty ( ) {
10172 return Err ( Error :: Unavailable ) ;
10273 }
10374 Ok ( Self {
104- document_tag : unsafe { NSSpellChecker :: uniqueSpellDocumentTag ( nil ) } ,
75+ document_tag : NSSpellChecker :: uniqueSpellDocumentTag ( ) ,
10576 language : Some ( bcp47. to_owned ( ) ) ,
10677 } )
10778 }
@@ -112,34 +83,28 @@ impl Checker {
11283 }
11384
11485 let ns_word = ns_string ( word) ;
115- let lang = language_id ( & self . language ) ;
116- let length: NSUInteger = unsafe { msg_send ! [ ns_word, length] } ;
86+ let lang = language_ref ( & self . language ) ;
11787 let range = NSRange {
11888 location : 0 ,
119- length,
89+ length : word . encode_utf16 ( ) . count ( ) ,
12090 } ;
121-
122- let guesses: id = unsafe {
123- let guard = checker ( ) . lock ( ) . unwrap ( ) ;
124- msg_send ! [
125- * * guard,
126- guessesForWordRange: range
127- inString: ns_word
128- language: lang
129- inSpellDocumentWithTag: self . document_tag
130- ]
131- } ;
132- nsarray_to_strings ( guesses, MAX )
91+ let tag = self . document_tag ;
92+
93+ let guesses = with_checker ( |c| {
94+ c. guessesForWordRange_inString_language_inSpellDocumentWithTag (
95+ range,
96+ & ns_word,
97+ lang. as_deref ( ) ,
98+ tag,
99+ )
100+ } ) ;
101+ nsarray_to_strings ( guesses. as_deref ( ) , MAX )
133102 }
134103
135104 pub fn ignore ( & mut self , word : & str ) {
136- let word = unsafe { NSString :: alloc ( nil) . init_str ( word) } ;
137- unsafe {
138- checker ( )
139- . lock ( )
140- . unwrap ( )
141- . ignoreWord_inSpellDocumentWithTag ( word, self . document_tag )
142- } ;
105+ let ns_word = ns_string ( word) ;
106+ let tag = self . document_tag ;
107+ with_checker ( |c| c. ignoreWord_inSpellDocumentWithTag ( & ns_word, tag) ) ;
143108 }
144109
145110 pub fn check ( & mut self , text : & str ) -> impl Iterator < Item = SpellingError > {
@@ -148,7 +113,6 @@ impl Checker {
148113 ns_text : ns_string ( text) ,
149114 ns_offset : 0 ,
150115 original : text. to_owned ( ) ,
151- byte_cursor : 0 ,
152116 language : self . language . clone ( ) ,
153117 }
154118 }
@@ -175,48 +139,46 @@ impl SpellingError {
175139
176140struct SpellcheckIter {
177141 document_tag : NSInteger ,
178- ns_text : id , /* NSString */
179- ns_offset : NSUInteger ,
142+ ns_text : Retained < NSString > ,
143+ ns_offset : usize ,
180144 original : String ,
181- byte_cursor : usize ,
182145 language : Option < String > ,
183146}
184147
185148impl Iterator for SpellcheckIter {
186149 type Item = SpellingError ;
187150
188151 fn next ( & mut self ) -> Option < Self :: Item > {
189- let lang = language_id ( & self . language ) ;
190- let ( range, _) =
191- unsafe {
192- checker ( ) . lock ( ) . unwrap ( )
193- . checkSpellingOfString_startingAt_language_wrap_inSpellDocumentWithTag_wordCount (
194- self . ns_text ,
195- self . ns_offset as NSInteger ,
196- lang,
197- NO ,
198- self . document_tag ,
199- )
200- } ;
201-
202- if range. location == NSNotFound as NSUInteger {
152+ let lang = language_ref ( & self . language ) ;
153+ let tag = self . document_tag ;
154+ let ns_text = self . ns_text . clone ( ) ;
155+ let starting = self . ns_offset as NSInteger ;
156+
157+ let range = with_checker ( |c| unsafe {
158+ c. checkSpellingOfString_startingAt_language_wrap_inSpellDocumentWithTag_wordCount (
159+ & ns_text,
160+ starting,
161+ lang. as_deref ( ) ,
162+ false ,
163+ tag,
164+ ptr:: null_mut ( ) ,
165+ )
166+ } ) ;
167+
168+ if range. length == 0 || range. location == NSNotFound as usize {
203169 return None ;
204- } ;
170+ }
205171
206- let misspelling = unsafe {
207- let misspelling = self . ns_text . substringWithRange ( range) ;
208- let misspelling_bytes = misspelling. UTF8String ( ) as * const u8 ;
209- str:: from_utf8 ( slice:: from_raw_parts ( misspelling_bytes, misspelling. len ( ) ) ) . unwrap ( )
210- } ;
211- let rest = & self . original [ self . byte_cursor ..] ;
212- let rel = rest. find ( misspelling) ?;
213- let start = self . byte_cursor + rel;
214- let end = start + misspelling. len ( ) ;
215- self . byte_cursor = end;
216- self . ns_offset = range. location + range. length ;
172+ let utf16_start = range. location ;
173+ let utf16_end = range. location + range. length ;
174+ let start = utf16_offset_to_utf8 ( & self . original , utf16_start) ;
175+ let end = utf16_offset_to_utf8 ( & self . original , utf16_end) ;
176+ let misspelling = self . original [ start..end] . to_owned ( ) ;
177+
178+ self . ns_offset = utf16_end;
217179
218180 Some ( SpellingError {
219- text : misspelling. to_owned ( ) ,
181+ text : misspelling,
220182 start,
221183 end,
222184 } )
0 commit comments