Coverage for C:\leo.repo\leo-editor\leo\commands\spellCommands.py: 17%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20150514040239.1: * @file ../commands/spellCommands.py
4#@@first
5"""Leo's spell-checking commands."""
6#@+<< imports >>
7#@+node:ekr.20150514050530.1: ** << imports >> (spellCommands.py)
8import re
9try:
10 # pylint: disable=import-error
11 # We can't assume the user has this.
12 # pip install pyenchant
13 import enchant
14except Exception: # May throw WinError(!)
15 enchant = None
16from leo.commands.baseCommands import BaseEditCommandsClass
17from leo.core import leoGlobals as g
18#@-<< imports >>
20def cmd(name):
21 """Command decorator for the SpellCommandsClass class."""
22 return g.new_cmd_decorator(name, ['c', 'spellCommands',])
24#@+others
25#@+node:ekr.20180207071908.1: ** class BaseSpellWrapper
26class BaseSpellWrapper:
27 """Code common to EnchantWrapper and DefaultWrapper"""
28 # pylint: disable=no-member
29 # Subclasses set self.c and self.d
30 #@+others
31 #@+node:ekr.20180207071114.3: *3* spell.add
32 def add(self, word):
33 """Add a word to the user dictionary."""
34 self.d.add(word)
35 #@+node:ekr.20150514063305.513: *3* spell.clean_dict
36 def clean_dict(self, fn):
37 if g.os_path_exists(fn):
38 f = open(fn, mode='rb')
39 s = f.read()
40 f.close()
41 # Blanks lines cause troubles.
42 s2 = s.replace(b'\r', b'').replace(b'\n\n', b'\n')
43 if s2.startswith(b'\n'):
44 s2 = s2[1:]
45 if s != s2:
46 g.es_print('cleaning', fn)
47 f = open(fn, mode='wb') # type:ignore
48 f.write(s2)
49 f.close()
50 #@+node:ekr.20180207071114.5: *3* spell.create
51 def create(self, fn):
52 """Create the given file with empty contents."""
53 # Make the directories as needed.
54 theDir = g.os_path_dirname(fn)
55 if theDir:
56 ok = g.makeAllNonExistentDirectories(theDir)
57 # #1453: Don't assume the directory exists.
58 if not ok:
59 g.error(f"did not create directory: {theDir}")
60 return
61 # Create the file.
62 try:
63 f = open(fn, mode='wb')
64 f.close()
65 g.note(f"created: {fn}")
66 except IOError:
67 g.error(f"can not create: {fn}")
68 except Exception:
69 g.error(f"unexpected error creating: {fn}")
70 g.es_exception()
71 #@+node:ekr.20180207072351.1: *3* spell.find_user_dict
72 def find_user_dict(self):
73 """Return the full path to the local dictionary."""
74 c = self.c
75 join = g.os_path_finalize_join
76 table = (
77 c.config.getString('enchant-local-dictionary'),
78 # Settings first.
79 join(g.app.homeDir, '.leo', 'spellpyx.txt'),
80 # #108: then the .leo directory.
81 join(g.app.loadDir, "..", "plugins", 'spellpyx.txt'),
82 # The plugins directory as a last resort.
83 )
84 for path in table:
85 if g.os_path_exists(path):
86 return path
87 g.es_print('Creating ~/.leo/spellpyx.txt')
88 # #1453: Return the default path.
89 return join(g.app.homeDir, '.leo', 'spellpyx.txt')
90 #@+node:ekr.20150514063305.515: *3* spell.ignore
91 def ignore(self, word):
93 self.d.add_to_session(word)
94 #@+node:ekr.20150514063305.517: *3* spell.process_word
95 def process_word(self, word):
96 """
97 Check the word. Return None if the word is properly spelled.
98 Otherwise, return a list of alternatives.
99 """
100 d = self.d
101 if not d:
102 return None
103 if d.check(word):
104 return None
105 # Speed doesn't matter here. The more we find, the more convenient.
106 word = ''.join([i for i in word if not i.isdigit()])
107 # Remove all digits.
108 if d.check(word) or d.check(word.lower()):
109 return None
110 if word.find('_') > -1:
111 # Snake case.
112 words = word.split('_')
113 for word2 in words:
114 if not d.check(word2) and not d.check(word2.lower()):
115 return d.suggest(word)
116 return None
117 words = g.unCamel(word)
118 if words:
119 for word2 in words:
120 if not d.check(word2) and not d.check(word2.lower()):
121 return d.suggest(word)
122 return None
123 return d.suggest(word)
124 #@-others
125#@+node:ekr.20180207075606.1: ** class DefaultDict
126class DefaultDict:
127 """A class with the same interface as the enchant dict class."""
129 def __init__(self, words=None):
130 self.added_words = set()
131 self.ignored_words = set()
132 self.words = set() if words is None else set(words)
133 #@+others
134 #@+node:ekr.20180207075740.1: *3* dict.add
135 def add(self, word):
136 """Add a word to the dictionary."""
137 self.words.add(word)
138 self.added_words.add(word)
139 #@+node:ekr.20180207101513.1: *3* dict.add_words_from_dict
140 def add_words_from_dict(self, kind, fn, words):
141 """For use by DefaultWrapper."""
142 for word in words or []:
143 self.words.add(word)
144 self.words.add(word.lower())
145 #@+node:ekr.20180207075751.1: *3* dict.add_to_session
146 def add_to_session(self, word):
148 self.ignored_words.add(word)
149 #@+node:ekr.20180207080007.1: *3* dict.check
150 def check(self, word):
151 """Return True if the word is in the dict."""
152 for s in (word, word.lower(), word.capitalize()):
153 if s in self.words or s in self.ignored_words:
154 return True
155 return False
156 #@+node:ekr.20180207081634.1: *3* dict.suggest & helpers
157 def suggest(self, word):
159 def known(words):
160 """Return the words that are in the dictionary."""
161 return [z for z in list(set(words)) if z in self.words]
163 assert not known([word]), repr(word)
164 suggestions = (
165 known(self.edits1(word)) or
166 known(self.edits2(word))
167 # [word] # Fall back to the unknown word itself.
168 )
169 return suggestions
170 #@+node:ekr.20180207085717.1: *4* dict.edits1 & edits2
171 #@@nobeautify
173 def edits1(self, word):
174 "All edits that are one edit away from `word`."
175 letters = 'abcdefghijklmnopqrstuvwxyz'
176 splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
177 deletes = [L + R[1:] for L, R in splits if R]
178 transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]
179 replaces = [L + c + R[1:] for L, R in splits if R for c in letters]
180 inserts = [L + c + R for L, R in splits for c in letters]
181 return list(set(deletes + transposes + replaces + inserts))
183 def edits2(self, word):
184 "All edits that are two edits away from `word`."
185 return [e2 for e1 in self.edits1(word) for e2 in self.edits1(e1)]
186 #@-others
187#@+node:ekr.20180207071114.1: ** class DefaultWrapper (BaseSpellWrapper)
188class DefaultWrapper(BaseSpellWrapper):
189 """
190 A default spell checker for when pyenchant is not available.
192 Based on http://norvig.com/spell-correct.html
194 Main dictionary: ~/.leo/main_spelling_dict.txt
195 User dictionary:
196 - @string enchant_local_dictionary or
197 - leo/plugins/spellpyx.txt or
198 - ~/.leo/spellpyx.txt
199 """
200 #@+others
201 #@+node:ekr.20180207071114.2: *3* default. __init__
202 def __init__(self, c):
203 """Ctor for DefaultWrapper class."""
204 # pylint: disable=super-init-not-called
205 self.c = c
206 if not g.app.spellDict:
207 g.app.spellDict = DefaultDict()
208 self.d = g.app.spellDict
209 self.user_fn = self.find_user_dict()
210 if not g.os_path_exists(self.user_fn):
211 # Fix bug 1175013: leo/plugins/spellpyx.txt is
212 # both source controlled and customized.
213 self.create(self.user_fn)
214 self.main_fn = self.find_main_dict()
215 table = (
216 ('user', self.user_fn),
217 ('main', self.main_fn),
218 )
219 for kind, fn in table:
220 if fn:
221 words = self.read_words(kind, fn)
222 self.d.add_words_from_dict(kind, fn, words)
223 #@+node:ekr.20180207110701.1: *3* default.add
224 def add(self, word):
225 """Add a word to the user dictionary."""
226 self.d.add(word)
227 self.d.add(word.lower())
228 self.save_user_dict()
229 #@+node:ekr.20180207100238.1: *3* default.find_main_dict
230 def find_main_dict(self):
231 """Return the full path to the global dictionary."""
232 c = self.c
233 fn = c.config.getString('main-spelling-dictionary')
234 if fn and g.os_path_exists(fn):
235 return fn
236 # Default to ~/.leo/main_spelling_dict.txt
237 fn = g.os_path_finalize_join(
238 g.app.homeDir, '.leo', 'main_spelling_dict.txt')
239 return fn if g.os_path_exists(fn) else None
240 #@+node:ekr.20180207073815.1: *3* default.read_words
241 def read_words(self, kind, fn):
242 """Return all the words from the dictionary file."""
243 words = set()
244 try:
245 with open(fn, 'rb') as f:
246 s = g.toUnicode(f.read())
247 # #1688: Do this in place.
248 for line in g.splitLines(s):
249 line = line.strip()
250 if line and not line.startswith('#'):
251 words.add(line)
252 except Exception:
253 g.es_print(f"can not open {kind} dictionary: {fn}")
254 return words
255 #@+node:ekr.20180207110718.1: *3* default.save_dict
256 def save_dict(self, kind, fn, trace=False):
257 """
258 Save the dictionary whose name is given, alphabetizing the file.
259 Write added words to the file if kind is 'user'.
260 """
261 if not fn:
262 return
263 words = self.read_words(kind, fn)
264 if not words:
265 return
266 words = set(words)
267 if kind == 'user':
268 for word in self.d.added_words:
269 words.add(word)
270 aList = sorted(words, key=lambda s: s.lower())
271 f = open(fn, mode='wb')
272 s = '\n'.join(aList) + '\n'
273 f.write(g.toEncodedString(s))
274 f.close()
275 #@+node:ekr.20180211104628.1: *3* default.save_main/user_dict
276 def save_main_dict(self, trace=False):
278 self.save_dict('main', self.main_fn, trace=trace)
280 def save_user_dict(self, trace=False):
282 self.save_dict('user', self.user_fn, trace=trace)
283 #@+node:ekr.20180209141933.1: *3* default.show_info
284 def show_info(self):
286 if self.main_fn:
287 g.es_print('Default spell checker')
288 table = (
289 ('main', self.main_fn),
290 ('user', self.user_fn),
291 )
292 else:
293 g.es_print('\nSpell checking has been disabled.')
294 g.es_print('To enable, put a main dictionary at:')
295 g.es_print('~/.leo/main_spelling_dict.txt')
296 table = ( # type:ignore
297 ('user', self.user_fn),
298 )
299 for kind, fn in table:
300 g.es_print(
301 f"{kind} dictionary: {(g.os_path_normpath(fn) if fn else 'None')}")
302 #@-others
303#@+node:ekr.20150514063305.510: ** class EnchantWrapper (BaseSpellWrapper)
304class EnchantWrapper(BaseSpellWrapper):
305 """A wrapper class for PyEnchant spell checker"""
306 #@+others
307 #@+node:ekr.20150514063305.511: *3* enchant. __init__
308 def __init__(self, c):
309 """Ctor for EnchantWrapper class."""
310 # pylint: disable=super-init-not-called
311 self.c = c
312 self.init_language()
313 fn = self.find_user_dict()
314 g.app.spellDict = self.d = self.open_dict_file(fn)
315 #@+node:ekr.20180207073536.1: *3* enchant.create_dict_from_file
316 def create_dict_from_file(self, fn, language):
318 return enchant.DictWithPWL(language, fn)
319 #@+node:ekr.20180207074613.1: *3* enchant.default_dict
320 def default_dict(self, language):
322 return enchant.Dict(language)
323 #@+node:ekr.20180207072846.1: *3* enchant.init_language
324 def init_language(self):
325 """Init self.language."""
326 c = self.c
327 language = g.checkUnicode(c.config.getString('enchant-language'))
328 if language:
329 try:
330 ok = enchant.dict_exists(language)
331 except Exception:
332 ok = False
333 if not ok:
334 g.warning('Invalid language code for Enchant', repr(language))
335 g.es_print('Using "en_US" instead')
336 g.es_print('Use @string enchant_language to specify your language')
337 language = 'en_US'
338 self.language = language
339 #@+node:ekr.20180207102856.1: *3* enchant.open_dict_file
340 def open_dict_file(self, fn):
341 """Open or create the dict with the given fn."""
342 language = self.language
343 if not fn or not language:
344 return None
345 if g.app.spellDict:
346 return g.app.spellDict
347 if not g.os_path_exists(fn):
348 # Fix bug 1175013: leo/plugins/spellpyx.txt is
349 # both source controlled and customized.
350 self.create(fn)
351 if g.os_path_exists(fn):
352 # Merge the local and global dictionaries.
353 try:
354 self.clean_dict(fn)
355 d = enchant.DictWithPWL(language, fn)
356 except Exception:
357 # This is off-putting, and not necessary.
358 # g.es('Error reading dictionary file', fn)
359 # g.es_exception()
360 d = enchant.Dict(language)
361 else:
362 # A fallback. Unlikely to happen.
363 d = enchant.Dict(language)
364 return d
365 #@+node:ekr.20150514063305.515: *3* spell.ignore
366 def ignore(self, word):
368 self.d.add_to_session(word)
369 #@+node:ekr.20150514063305.517: *3* spell.process_word
370 def process_word(self, word):
371 """
372 Check the word. Return None if the word is properly spelled.
373 Otherwise, return a list of alternatives.
374 """
375 d = self.d
376 if not d:
377 return None
378 if d.check(word):
379 return None
380 # Speed doesn't matter here. The more we find, the more convenient.
381 word = ''.join([i for i in word if not i.isdigit()])
382 # Remove all digits.
383 if d.check(word) or d.check(word.lower()):
384 return None
385 if word.find('_') > -1:
386 # Snake case.
387 words = word.split('_')
388 for word2 in words:
389 if not d.check(word2) and not d.check(word2.lower()):
390 return d.suggest(word)
391 return None
392 words = g.unCamel(word)
393 if words:
394 for word2 in words:
395 if not d.check(word2) and not d.check(word2.lower()):
396 return d.suggest(word)
397 return None
398 return d.suggest(word)
399 #@+node:ekr.20180209142310.1: *3* spell.show_info
400 def show_info(self):
402 g.es_print('pyenchant spell checker')
403 g.es_print(f"user dictionary: {self.find_user_dict()}")
404 try:
405 aList = enchant.list_dicts()
406 aList2 = [a for a, b in aList]
407 g.es_print(f"main dictionaries: {', '.join(aList2)}")
408 except Exception:
409 g.es_exception()
410 #@-others
411#@+node:ekr.20150514063305.481: ** class SpellCommandsClass
412class SpellCommandsClass(BaseEditCommandsClass):
413 """Commands to support the Spell Tab."""
414 #@+others
415 #@+node:ekr.20150514063305.482: *3* ctor & reloadSettings(SpellCommandsClass)
416 def __init__(self, c):
417 """
418 Ctor for SpellCommandsClass class.
419 Inits happen when the first frame opens.
420 """
421 # pylint: disable=super-init-not-called
422 self.c = c
423 self.handler = None
424 self.reloadSettings()
426 def reloadSettings(self):
427 """SpellCommandsClass.reloadSettings."""
428 c = self.c
429 self.page_width = c.config.getInt("page-width") # for wrapping
430 #@+node:ekr.20150514063305.484: *3* openSpellTab
431 @cmd('spell-tab-open')
432 def openSpellTab(self, event=None):
433 """Open the Spell Checker tab in the log pane."""
434 if g.unitTesting:
435 return
436 c = self.c
437 log = c.frame.log
438 tabName = 'Spell'
439 if log.frameDict.get(tabName):
440 log.selectTab(tabName)
441 else:
442 log.selectTab(tabName)
443 self.handler = SpellTabHandler(c, tabName)
444 # Bug fix: 2013/05/22.
445 if not self.handler.loaded:
446 log.deleteTab(tabName)
447 # spell as you type stuff
448 self.suggestions = []
449 self.suggestions_idx = None
450 self.word = None
451 self.spell_as_you_type = False
452 self.wrap_as_you_type = False
453 #@+node:ekr.20150514063305.485: *3* commands...(SpellCommandsClass)
454 #@+node:ekr.20171205043931.1: *4* add
455 @cmd('spell-add')
456 def add(self, event=None):
457 """
458 Simulate pressing the 'add' button in the Spell tab.
460 Just open the Spell tab if it has never been opened.
461 For minibuffer commands, we must also force the Spell tab to be visible.
462 """
463 # self.handler is a SpellTabHandler object (inited by openSpellTab)
464 if self.handler:
465 self.openSpellTab()
466 self.handler.add()
467 else:
468 self.openSpellTab()
469 #@+node:ekr.20150514063305.486: *4* find
470 @cmd('spell-find')
471 def find(self, event=None):
472 """
473 Simulate pressing the 'Find' button in the Spell tab.
475 Just open the Spell tab if it has never been opened.
476 For minibuffer commands, we must also force the Spell tab to be visible.
477 """
478 # self.handler is a SpellTabHandler object (inited by openSpellTab)
479 if self.handler:
480 self.openSpellTab()
481 self.handler.find()
482 else:
483 self.openSpellTab()
484 #@+node:ekr.20150514063305.487: *4* change
485 @cmd('spell-change')
486 def change(self, event=None):
487 """Simulate pressing the 'Change' button in the Spell tab."""
488 if self.handler:
489 self.openSpellTab()
490 self.handler.change()
491 else:
492 self.openSpellTab()
493 #@+node:ekr.20150514063305.488: *4* changeThenFind
494 @cmd('spell-change-then-find')
495 def changeThenFind(self, event=None):
496 """Simulate pressing the 'Change, Find' button in the Spell tab."""
497 if self.handler:
498 self.openSpellTab()
499 # A workaround for a pylint warning:
500 # self.handler.changeThenFind()
501 f = getattr(self.handler, 'changeThenFind')
502 f()
503 else:
504 self.openSpellTab()
505 #@+node:ekr.20150514063305.489: *4* hide
506 @cmd('spell-tab-hide')
507 def hide(self, event=None):
508 """Hide the Spell tab."""
509 if self.handler:
510 self.c.frame.log.selectTab('Log')
511 self.c.bodyWantsFocus()
512 #@+node:ekr.20150514063305.490: *4* ignore
513 @cmd('spell-ignore')
514 def ignore(self, event=None):
515 """Simulate pressing the 'Ignore' button in the Spell tab."""
516 if self.handler:
517 self.openSpellTab()
518 self.handler.ignore()
519 else:
520 self.openSpellTab()
521 #@+node:ekr.20150514063305.491: *4* focusToSpell
522 @cmd('focus-to-spell-tab')
523 def focusToSpell(self, event=None):
524 """Put focus in the spell tab."""
525 self.openSpellTab()
526 # Makes Spell tab visible.
527 # This is not a great idea. There is no indication of focus.
528 # if self.handler and self.handler.tab:
529 # self.handler.tab.setFocus()
530 #@+node:ekr.20150514063305.492: *3* as_you_type_* commands
531 #@+node:ekr.20150514063305.493: *4* as_you_type_toggle
532 @cmd('spell-as-you-type-toggle')
533 def as_you_type_toggle(self, event):
534 """as_you_type_toggle - toggle spell as you type."""
535 # c = self.c
536 if self.spell_as_you_type:
537 self.spell_as_you_type = False
538 if not self.wrap_as_you_type:
539 g.unregisterHandler('bodykey2', self.as_you_type_onkey)
540 g.es("Spell as you type disabled")
541 return
542 self.spell_as_you_type = True
543 if not self.wrap_as_you_type:
544 g.registerHandler('bodykey2', self.as_you_type_onkey)
545 g.es("Spell as you type enabled")
546 #@+node:ekr.20150514063305.494: *4* as_you_type_wrap
547 @cmd('spell-as-you-type-wrap')
548 def as_you_type_wrap(self, event):
549 """as_you_type_wrap - toggle wrap as you type."""
550 # c = self.c
551 if self.wrap_as_you_type:
552 self.wrap_as_you_type = False
553 if not self.spell_as_you_type:
554 g.unregisterHandler('bodykey2', self.as_you_type_onkey)
555 g.es("Wrap as you type disabled")
556 return
557 self.wrap_as_you_type = True
558 if not self.spell_as_you_type:
559 g.registerHandler('bodykey2', self.as_you_type_onkey)
560 g.es("Wrap as you type enabled")
561 #@+node:ekr.20150514063305.495: *4* as_you_type_next
562 @cmd('spell-as-you-type-next')
563 def as_you_type_next(self, event):
564 """as_you_type_next - cycle word behind cursor to next suggestion."""
565 if not self.suggestions:
566 g.es('[no suggestions]')
567 return
568 word = self.suggestions[self.suggestion_idx] # type:ignore
569 self.suggestion_idx = (self.suggestion_idx + 1) % len(self.suggestions) # type:ignore
570 self.as_you_type_replace(word)
571 #@+node:ekr.20150514063305.496: *4* as_you_type_undo
572 @cmd('spell-as-you-type-undo')
573 def as_you_type_undo(self, event):
574 """as_you_type_undo - replace word behind cursor with word
575 user typed before it started cycling suggestions.
576 """
577 if not self.word:
578 g.es('[no previous word]')
579 return
580 self.as_you_type_replace(self.word)
581 #@+node:ekr.20150514063305.497: *4* as_you_type_onkey
582 def as_you_type_onkey(self, tag, kwargs):
583 """as_you_type_onkey - handle a keystroke in the body when
584 spell as you type is active
586 :Parameters:
587 - `tag`: hook tag
588 - `kwargs`: hook arguments
589 """
590 if kwargs['c'] != self.c:
591 return
592 if kwargs['ch'] not in '\'",.:) \n\t':
593 return
594 c = self.c
595 spell_ok = True
596 if self.spell_as_you_type: # might just be for wrapping
597 w = c.frame.body.wrapper
598 txt = w.getAllText()
599 i = w.getInsertPoint()
600 word = txt[:i].rsplit(None, 1)[-1]
601 word = ''.join(i if i.isalpha() else ' ' for i in word).split()
602 if word:
603 word = word[-1]
604 ec = c.spellCommands.handler.spellController
605 suggests = ec.process_word(word)
606 if suggests:
607 spell_ok = False
608 g.es(' '.join(suggests[:5]) +
609 ('...' if len(suggests) > 5 else ''),
610 color='red')
611 elif suggests is not None:
612 spell_ok = False
613 g.es('[no suggestions]')
614 self.suggestions = suggests
615 self.suggestion_idx = 0
616 self.word = word
617 if spell_ok and self.wrap_as_you_type and kwargs['ch'] == ' ':
618 w = c.frame.body.wrapper
619 txt = w.getAllText()
620 i = w.getInsertPoint()
621 # calculate the current column
622 parts = txt.split('\n')
623 popped = 0 # chars on previous lines
624 while len(parts[0]) + popped < i:
625 popped += len(parts.pop(0)) + 1 # +1 for the \n that's gone
626 col = i - popped
627 if col > self.page_width:
628 txt = txt[:i] + '\n' + txt[i:] # replace space with \n
629 w.setAllText(txt)
630 c.p.b = txt
631 w.setInsertPoint(i + 1) # must come after c.p.b assignment
632 #@+node:ekr.20150514063305.498: *4* as_you_type_replace
633 def as_you_type_replace(self, word):
634 """as_you_type_replace - replace the word behind the cursor
635 with `word`
637 :Parameters:
638 - `word`: word to use as replacement
639 """
640 c = self.c
641 w = c.frame.body.wrapper
642 txt = w.getAllText()
643 j = i = w.getInsertPoint()
644 i -= 1
645 while i and not txt[i].isalpha():
646 i -= 1
647 xtra = j - i
648 j = i + 1
649 while i and txt[i].isalpha():
650 i -= 1
651 if i or (txt and not txt[0].isalpha()):
652 i += 1
653 txt = txt[:i] + word + txt[j:]
654 w.setAllText(txt)
655 c.p.b = txt
656 w.setInsertPoint(i + len(word) + xtra - 1)
657 c.bodyWantsFocusNow()
658 #@-others
659#@+node:ekr.20150514063305.499: ** class SpellTabHandler
660class SpellTabHandler:
661 """A class to create and manage Leo's Spell Check dialog."""
662 #@+others
663 #@+node:ekr.20150514063305.501: *3* SpellTabHandler.__init__
664 def __init__(self, c, tabName):
665 """Ctor for SpellTabHandler class."""
666 if g.app.gui.isNullGui:
667 return
668 self.c = c
669 self.body = c.frame.body
670 self.currentWord = None
671 # Don't include underscores in words. It just complicates things.
672 # [^\W\d_] means any unicode char except underscore or digit.
673 self.re_word = re.compile(r"([^\W\d_]+)(['`][^\W\d_]+)?", flags=re.UNICODE)
674 self.outerScrolledFrame = None
675 self.seen = set() # Adding a word to seen will ignore it until restart.
676 # A text widget for scanning. Must have a parent frame.
677 self.workCtrl = g.app.gui.plainTextWidget(c.frame.top)
678 if enchant:
679 self.spellController = EnchantWrapper(c)
680 self.tab = g.app.gui.createSpellTab(c, self, tabName)
681 self.loaded = True
682 return
683 # Create the spellController for the show-spell-info command.
684 self.spellController = DefaultWrapper(c) # type:ignore
685 self.loaded = bool(self.spellController.main_fn)
686 if self.loaded:
687 # Create the spell tab only if the main dict exists.
688 self.tab = g.app.gui.createSpellTab(c, self, tabName)
689 else:
690 # g.es_print('No main dictionary')
691 self.tab = None
692 #@+node:ekr.20150514063305.502: *3* Commands
693 #@+node:ekr.20150514063305.503: *4* add (spellTab)
694 def add(self, event=None):
695 """Add the selected suggestion to the dictionary."""
696 if self.loaded:
697 w = self.currentWord
698 if w:
699 self.spellController.add(w)
700 self.tab.onFindButton()
701 #@+node:ekr.20150514063305.504: *4* change (spellTab)
702 def change(self, event=None):
703 """Make the selected change to the text"""
704 if not self.loaded:
705 return False
706 c, p, u = self.c, self.c.p, self.c.undoer
707 w = c.frame.body.wrapper
708 selection = self.tab.getSuggestion()
709 if selection:
710 bunch = u.beforeChangeBody(p)
711 # Use getattr to keep pylint happy.
712 i = getattr(self.tab, 'change_i', None)
713 j = getattr(self.tab, 'change_j', None)
714 if i is not None:
715 start, end = i, j
716 else:
717 start, end = w.getSelectionRange()
718 if start is not None:
719 if start > end:
720 start, end = end, start
721 w.delete(start, end)
722 w.insert(start, selection)
723 w.setSelectionRange(start, start + len(selection))
724 p.v.b = w.getAllText()
725 u.afterChangeBody(p, 'Change', bunch)
726 c.invalidateFocus()
727 c.bodyWantsFocus()
728 return True
729 # The focus must never leave the body pane.
730 c.invalidateFocus()
731 c.bodyWantsFocus()
732 return False
733 #@+node:ekr.20150514063305.505: *4* find & helper
734 def find(self, event=None):
735 """Find the next unknown word."""
736 if not self.loaded:
737 return
738 c, n, p = self.c, 0, self.c.p
739 sc = self.spellController
740 w = c.frame.body.wrapper
741 c.selectPosition(p)
742 s = w.getAllText().rstrip()
743 ins = w.getInsertPoint()
744 # New in Leo 5.3: use regex to find words.
745 last_p = p.copy()
746 while True:
747 for m in self.re_word.finditer(s[ins:]):
748 start, word = m.start(0), m.group(0)
749 if word in self.seen:
750 continue
751 n += 1
752 # Ignore the word if numbers precede or follow it.
753 # Seems difficult to do this in the regex itself.
754 k1 = ins + start - 1
755 if k1 >= 0 and s[k1].isdigit():
756 continue
757 k2 = ins + start + len(word)
758 if k2 < len(s) and s[k2].isdigit():
759 continue
760 alts = sc.process_word(word)
761 if alts:
762 self.currentWord = word
763 i = ins + start
764 j = i + len(word)
765 self.showMisspelled(p)
766 self.tab.fillbox(alts, word)
767 c.invalidateFocus()
768 c.bodyWantsFocus()
769 w.setSelectionRange(i, j, insert=j)
770 k = g.see_more_lines(s, j, 4)
771 w.see(k)
772 return
773 self.seen.add(word)
774 # No more misspellings in p
775 p.moveToThreadNext()
776 if p:
777 ins = 0
778 s = p.b
779 else:
780 g.es("no more misspellings")
781 c.selectPosition(last_p)
782 self.tab.fillbox([])
783 c.invalidateFocus()
784 c.bodyWantsFocus()
785 return
786 #@+node:ekr.20160415033936.1: *5* showMisspelled
787 def showMisspelled(self, p):
788 """Show the position p, contracting the tree as needed."""
789 c = self.c
790 redraw = not p.isVisible(c)
791 # New in Leo 4.4.8: show only the 'sparse' tree when redrawing.
792 if c.sparse_spell and not c.p.isAncestorOf(p):
793 for p2 in c.p.self_and_parents(copy=False):
794 p2.contract()
795 redraw = True
796 for p2 in p.parents(copy=False):
797 if not p2.isExpanded():
798 p2.expand()
799 redraw = True
800 if redraw:
801 c.redraw(p)
802 else:
803 c.selectPosition(p)
804 #@+node:ekr.20150514063305.508: *4* hide
805 def hide(self, event=None):
806 self.c.frame.log.selectTab('Log')
807 #@+node:ekr.20150514063305.509: *4* ignore
808 def ignore(self, event=None):
809 """Ignore the incorrect word for the duration of this spell check session."""
810 if self.loaded:
811 w = self.currentWord
812 if w:
813 self.spellController.ignore(w)
814 self.tab.onFindButton()
815 #@-others
816#@+node:ekr.20180209141207.1: ** @g.command('show-spell-info')
817@g.command('show-spell-info')
818def show_spell_info(event=None):
819 c = event.get('c')
820 if c:
821 c.spellCommands.handler.spellController.show_info()
822#@+node:ekr.20180211104019.1: ** @g.command('clean-main-spell-dict')
823@g.command('clean-main-spell-dict')
824def clean_main_spell_dict(event):
825 """
826 Clean the main spelling dictionary used *only* by the default spell
827 checker.
829 This command works regardless of the spell checker being used.
830 """
831 c = event and event.get('c')
832 if c:
833 DefaultWrapper(c).save_main_dict(trace=True)
834#@+node:ekr.20180211105748.1: ** @g.command('clean-user-spell-dict')
835@g.command('clean-user-spell-dict')
836def clean_user_spell_dict(event):
837 """
838 Clean the user spelling dictionary used *only* by the default spell
839 checker. Mostly for debugging, because this happens automatically.
841 This command works regardless of the spell checker being used.
842 """
843 c = event and event.get('c')
844 if c:
845 DefaultWrapper(c).save_user_dict(trace=True)
846#@-others
847#@@language python
848#@@tabwidth -4
849#@-leo