Hide keyboard shortcuts

Hot-keys on this page

r m x p   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 >> 

19 

20def cmd(name): 

21 """Command decorator for the SpellCommandsClass class.""" 

22 return g.new_cmd_decorator(name, ['c', 'spellCommands',]) 

23 

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): 

92 

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.""" 

128 

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): 

147 

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): 

158 

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] 

162 

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 

172 

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)) 

182 

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. 

191 

192 Based on http://norvig.com/spell-correct.html 

193 

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): 

277 

278 self.save_dict('main', self.main_fn, trace=trace) 

279 

280 def save_user_dict(self, trace=False): 

281 

282 self.save_dict('user', self.user_fn, trace=trace) 

283 #@+node:ekr.20180209141933.1: *3* default.show_info 

284 def show_info(self): 

285 

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): 

317 

318 return enchant.DictWithPWL(language, fn) 

319 #@+node:ekr.20180207074613.1: *3* enchant.default_dict 

320 def default_dict(self, language): 

321 

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): 

367 

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): 

401 

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() 

425 

426 def reloadSettings(self): 

427 """SpellCommandsClass.reloadSettings.""" 

428 c = self.c 

429 self.page_width = c.config.getInt("page-width") 

430 # for wrapping 

431 #@+node:ekr.20150514063305.484: *3* openSpellTab 

432 @cmd('spell-tab-open') 

433 def openSpellTab(self, event=None): 

434 """Open the Spell Checker tab in the log pane.""" 

435 if g.unitTesting: 

436 return 

437 c = self.c 

438 log = c.frame.log 

439 tabName = 'Spell' 

440 if log.frameDict.get(tabName): 

441 log.selectTab(tabName) 

442 else: 

443 log.selectTab(tabName) 

444 self.handler = SpellTabHandler(c, tabName) 

445 # Bug fix: 2013/05/22. 

446 if not self.handler.loaded: 

447 log.deleteTab(tabName) 

448 # spell as you type stuff 

449 self.suggestions = [] 

450 self.suggestions_idx = None 

451 self.word = None 

452 self.spell_as_you_type = False 

453 self.wrap_as_you_type = False 

454 #@+node:ekr.20150514063305.485: *3* commands...(SpellCommandsClass) 

455 #@+node:ekr.20171205043931.1: *4* add 

456 @cmd('spell-add') 

457 def add(self, event=None): 

458 """ 

459 Simulate pressing the 'add' button in the Spell tab. 

460 

461 Just open the Spell tab if it has never been opened. 

462 For minibuffer commands, we must also force the Spell tab to be visible. 

463 """ 

464 # self.handler is a SpellTabHandler object (inited by openSpellTab) 

465 if self.handler: 

466 self.openSpellTab() 

467 self.handler.add() 

468 else: 

469 self.openSpellTab() 

470 #@+node:ekr.20150514063305.486: *4* find 

471 @cmd('spell-find') 

472 def find(self, event=None): 

473 """ 

474 Simulate pressing the 'Find' button in the Spell tab. 

475 

476 Just open the Spell tab if it has never been opened. 

477 For minibuffer commands, we must also force the Spell tab to be visible. 

478 """ 

479 # self.handler is a SpellTabHandler object (inited by openSpellTab) 

480 if self.handler: 

481 self.openSpellTab() 

482 self.handler.find() 

483 else: 

484 self.openSpellTab() 

485 #@+node:ekr.20150514063305.487: *4* change 

486 @cmd('spell-change') 

487 def change(self, event=None): 

488 """Simulate pressing the 'Change' button in the Spell tab.""" 

489 if self.handler: 

490 self.openSpellTab() 

491 self.handler.change() 

492 else: 

493 self.openSpellTab() 

494 #@+node:ekr.20150514063305.488: *4* changeThenFind 

495 @cmd('spell-change-then-find') 

496 def changeThenFind(self, event=None): 

497 """Simulate pressing the 'Change, Find' button in the Spell tab.""" 

498 if self.handler: 

499 self.openSpellTab() 

500 # A workaround for a pylint warning: 

501 # self.handler.changeThenFind() 

502 f = getattr(self.handler, 'changeThenFind') 

503 f() 

504 else: 

505 self.openSpellTab() 

506 #@+node:ekr.20150514063305.489: *4* hide 

507 @cmd('spell-tab-hide') 

508 def hide(self, event=None): 

509 """Hide the Spell tab.""" 

510 if self.handler: 

511 self.c.frame.log.selectTab('Log') 

512 self.c.bodyWantsFocus() 

513 #@+node:ekr.20150514063305.490: *4* ignore 

514 @cmd('spell-ignore') 

515 def ignore(self, event=None): 

516 """Simulate pressing the 'Ignore' button in the Spell tab.""" 

517 if self.handler: 

518 self.openSpellTab() 

519 self.handler.ignore() 

520 else: 

521 self.openSpellTab() 

522 #@+node:ekr.20150514063305.491: *4* focusToSpell 

523 @cmd('focus-to-spell-tab') 

524 def focusToSpell(self, event=None): 

525 """Put focus in the spell tab.""" 

526 self.openSpellTab() 

527 # Makes Spell tab visible. 

528 # This is not a great idea. There is no indication of focus. 

529 # if self.handler and self.handler.tab: 

530 # self.handler.tab.setFocus() 

531 #@+node:ekr.20150514063305.492: *3* as_you_type_* commands 

532 #@+node:ekr.20150514063305.493: *4* as_you_type_toggle 

533 @cmd('spell-as-you-type-toggle') 

534 def as_you_type_toggle(self, event): 

535 """as_you_type_toggle - toggle spell as you type.""" 

536 # c = self.c 

537 if self.spell_as_you_type: 

538 self.spell_as_you_type = False 

539 if not self.wrap_as_you_type: 

540 g.unregisterHandler('bodykey2', self.as_you_type_onkey) 

541 g.es("Spell as you type disabled") 

542 return 

543 self.spell_as_you_type = True 

544 if not self.wrap_as_you_type: 

545 g.registerHandler('bodykey2', self.as_you_type_onkey) 

546 g.es("Spell as you type enabled") 

547 #@+node:ekr.20150514063305.494: *4* as_you_type_wrap 

548 @cmd('spell-as-you-type-wrap') 

549 def as_you_type_wrap(self, event): 

550 """as_you_type_wrap - toggle wrap as you type.""" 

551 # c = self.c 

552 if self.wrap_as_you_type: 

553 self.wrap_as_you_type = False 

554 if not self.spell_as_you_type: 

555 g.unregisterHandler('bodykey2', self.as_you_type_onkey) 

556 g.es("Wrap as you type disabled") 

557 return 

558 self.wrap_as_you_type = True 

559 if not self.spell_as_you_type: 

560 g.registerHandler('bodykey2', self.as_you_type_onkey) 

561 g.es("Wrap as you type enabled") 

562 #@+node:ekr.20150514063305.495: *4* as_you_type_next 

563 @cmd('spell-as-you-type-next') 

564 def as_you_type_next(self, event): 

565 """as_you_type_next - cycle word behind cursor to next suggestion.""" 

566 if not self.suggestions: 

567 g.es('[no suggestions]') 

568 return 

569 word = self.suggestions[self.suggestion_idx] # type:ignore 

570 self.suggestion_idx = (self.suggestion_idx + 1) % len(self.suggestions) # type:ignore 

571 self.as_you_type_replace(word) 

572 #@+node:ekr.20150514063305.496: *4* as_you_type_undo 

573 @cmd('spell-as-you-type-undo') 

574 def as_you_type_undo(self, event): 

575 """as_you_type_undo - replace word behind cursor with word 

576 user typed before it started cycling suggestions. 

577 """ 

578 if not self.word: 

579 g.es('[no previous word]') 

580 return 

581 self.as_you_type_replace(self.word) 

582 #@+node:ekr.20150514063305.497: *4* as_you_type_onkey 

583 def as_you_type_onkey(self, tag, kwargs): 

584 """as_you_type_onkey - handle a keystroke in the body when 

585 spell as you type is active 

586 

587 :Parameters: 

588 - `tag`: hook tag 

589 - `kwargs`: hook arguments 

590 """ 

591 if kwargs['c'] != self.c: 

592 return 

593 if kwargs['ch'] not in '\'",.:) \n\t': 

594 return 

595 c = self.c 

596 spell_ok = True 

597 if self.spell_as_you_type: # might just be for wrapping 

598 w = c.frame.body.wrapper 

599 txt = w.getAllText() 

600 i = w.getInsertPoint() 

601 word = txt[:i].rsplit(None, 1)[-1] 

602 word = ''.join(i if i.isalpha() else ' ' for i in word).split() 

603 if word: 

604 word = word[-1] 

605 ec = c.spellCommands.handler.spellController 

606 suggests = ec.process_word(word) 

607 if suggests: 

608 spell_ok = False 

609 g.es(' '.join(suggests[:5]) + 

610 ('...' if len(suggests) > 5 else ''), 

611 color='red') 

612 elif suggests is not None: 

613 spell_ok = False 

614 g.es('[no suggestions]') 

615 self.suggestions = suggests 

616 self.suggestion_idx = 0 

617 self.word = word 

618 if spell_ok and self.wrap_as_you_type and kwargs['ch'] == ' ': 

619 w = c.frame.body.wrapper 

620 txt = w.getAllText() 

621 i = w.getInsertPoint() 

622 # calculate the current column 

623 parts = txt.split('\n') 

624 popped = 0 # chars on previous lines 

625 while len(parts[0]) + popped < i: 

626 popped += len(parts.pop(0)) + 1 # +1 for the \n that's gone 

627 col = i - popped 

628 if col > self.page_width: 

629 txt = txt[:i] + '\n' + txt[i:] # replace space with \n 

630 w.setAllText(txt) 

631 c.p.b = txt 

632 w.setInsertPoint(i + 1) # must come after c.p.b assignment 

633 #@+node:ekr.20150514063305.498: *4* as_you_type_replace 

634 def as_you_type_replace(self, word): 

635 """as_you_type_replace - replace the word behind the cursor 

636 with `word` 

637 

638 :Parameters: 

639 - `word`: word to use as replacement 

640 """ 

641 c = self.c 

642 w = c.frame.body.wrapper 

643 txt = w.getAllText() 

644 j = i = w.getInsertPoint() 

645 i -= 1 

646 while i and not txt[i].isalpha(): 

647 i -= 1 

648 xtra = j - i 

649 j = i + 1 

650 while i and txt[i].isalpha(): 

651 i -= 1 

652 if i or (txt and not txt[0].isalpha()): 

653 i += 1 

654 txt = txt[:i] + word + txt[j:] 

655 w.setAllText(txt) 

656 c.p.b = txt 

657 w.setInsertPoint(i + len(word) + xtra - 1) 

658 c.bodyWantsFocusNow() 

659 #@-others 

660#@+node:ekr.20150514063305.499: ** class SpellTabHandler 

661class SpellTabHandler: 

662 """A class to create and manage Leo's Spell Check dialog.""" 

663 #@+others 

664 #@+node:ekr.20150514063305.501: *3* SpellTabHandler.__init__ 

665 def __init__(self, c, tabName): 

666 """Ctor for SpellTabHandler class.""" 

667 if g.app.gui.isNullGui: 

668 return 

669 self.c = c 

670 self.body = c.frame.body 

671 self.currentWord = None 

672 self.re_word = re.compile( 

673 # Don't include underscores in words. It just complicates things. 

674 # [^\W\d_] means any unicode char except underscore or digit. 

675 r"([^\W\d_]+)(['`][^\W\d_]+)?", 

676 flags=re.UNICODE) 

677 self.outerScrolledFrame = None 

678 self.seen = set() 

679 # Adding a word to seen will ignore it until restart. 

680 self.workCtrl = g.app.gui.plainTextWidget(c.frame.top) 

681 # A text widget for scanning. 

682 # Must have a parent frame even though it is not packed. 

683 if enchant: 

684 self.spellController = EnchantWrapper(c) 

685 self.tab = g.app.gui.createSpellTab(c, self, tabName) 

686 self.loaded = True 

687 return 

688 # Create the spellController for the show-spell-info command. 

689 self.spellController = DefaultWrapper(c) # type:ignore 

690 self.loaded = bool(self.spellController.main_fn) 

691 if self.loaded: 

692 # Create the spell tab only if the main dict exists. 

693 self.tab = g.app.gui.createSpellTab(c, self, tabName) 

694 else: 

695 # g.es_print('No main dictionary') 

696 self.tab = None 

697 #@+node:ekr.20150514063305.502: *3* Commands 

698 #@+node:ekr.20150514063305.503: *4* add (spellTab) 

699 def add(self, event=None): 

700 """Add the selected suggestion to the dictionary.""" 

701 if self.loaded: 

702 w = self.currentWord 

703 if w: 

704 self.spellController.add(w) 

705 self.tab.onFindButton() 

706 #@+node:ekr.20150514063305.504: *4* change (spellTab) 

707 def change(self, event=None): 

708 """Make the selected change to the text""" 

709 if not self.loaded: 

710 return False 

711 c, p, u = self.c, self.c.p, self.c.undoer 

712 w = c.frame.body.wrapper 

713 selection = self.tab.getSuggestion() 

714 if selection: 

715 bunch = u.beforeChangeBody(p) 

716 # Use getattr to keep pylint happy. 

717 i = getattr(self.tab, 'change_i', None) 

718 j = getattr(self.tab, 'change_j', None) 

719 if i is not None: 

720 start, end = i, j 

721 else: 

722 start, end = w.getSelectionRange() 

723 if start is not None: 

724 if start > end: 

725 start, end = end, start 

726 w.delete(start, end) 

727 w.insert(start, selection) 

728 w.setSelectionRange(start, start + len(selection)) 

729 p.v.b = w.getAllText() 

730 u.afterChangeBody(p, 'Change', bunch) 

731 c.invalidateFocus() 

732 c.bodyWantsFocus() 

733 return True 

734 # The focus must never leave the body pane. 

735 c.invalidateFocus() 

736 c.bodyWantsFocus() 

737 return False 

738 #@+node:ekr.20150514063305.505: *4* find & helper 

739 def find(self, event=None): 

740 """Find the next unknown word.""" 

741 if not self.loaded: 

742 return 

743 c, n, p = self.c, 0, self.c.p 

744 sc = self.spellController 

745 w = c.frame.body.wrapper 

746 c.selectPosition(p) 

747 s = w.getAllText().rstrip() 

748 ins = w.getInsertPoint() 

749 # New in Leo 5.3: use regex to find words. 

750 last_p = p.copy() 

751 while True: 

752 for m in self.re_word.finditer(s[ins:]): 

753 start, word = m.start(0), m.group(0) 

754 if word in self.seen: 

755 continue 

756 n += 1 

757 # Ignore the word if numbers precede or follow it. 

758 # Seems difficult to do this in the regex itself. 

759 k1 = ins + start - 1 

760 if k1 >= 0 and s[k1].isdigit(): 

761 continue 

762 k2 = ins + start + len(word) 

763 if k2 < len(s) and s[k2].isdigit(): 

764 continue 

765 alts = sc.process_word(word) 

766 if alts: 

767 self.currentWord = word 

768 i = ins + start 

769 j = i + len(word) 

770 self.showMisspelled(p) 

771 self.tab.fillbox(alts, word) 

772 c.invalidateFocus() 

773 c.bodyWantsFocus() 

774 w.setSelectionRange(i, j, insert=j) 

775 k = g.see_more_lines(s, j, 4) 

776 w.see(k) 

777 return 

778 self.seen.add(word) 

779 # No more misspellings in p 

780 p.moveToThreadNext() 

781 if p: 

782 ins = 0 

783 s = p.b 

784 else: 

785 g.es("no more misspellings") 

786 c.selectPosition(last_p) 

787 self.tab.fillbox([]) 

788 c.invalidateFocus() 

789 c.bodyWantsFocus() 

790 return 

791 #@+node:ekr.20160415033936.1: *5* showMisspelled 

792 def showMisspelled(self, p): 

793 """Show the position p, contracting the tree as needed.""" 

794 c = self.c 

795 redraw = not p.isVisible(c) 

796 # New in Leo 4.4.8: show only the 'sparse' tree when redrawing. 

797 if c.sparse_spell and not c.p.isAncestorOf(p): 

798 for p2 in c.p.self_and_parents(copy=False): 

799 p2.contract() 

800 redraw = True 

801 for p2 in p.parents(copy=False): 

802 if not p2.isExpanded(): 

803 p2.expand() 

804 redraw = True 

805 if redraw: 

806 c.redraw(p) 

807 else: 

808 c.selectPosition(p) 

809 #@+node:ekr.20150514063305.508: *4* hide 

810 def hide(self, event=None): 

811 self.c.frame.log.selectTab('Log') 

812 #@+node:ekr.20150514063305.509: *4* ignore 

813 def ignore(self, event=None): 

814 """Ignore the incorrect word for the duration of this spell check session.""" 

815 if self.loaded: 

816 w = self.currentWord 

817 if w: 

818 self.spellController.ignore(w) 

819 self.tab.onFindButton() 

820 #@-others 

821#@+node:ekr.20180209141207.1: ** @g.command('show-spell-info') 

822@g.command('show-spell-info') 

823def show_spell_info(event=None): 

824 c = event.get('c') 

825 if c: 

826 c.spellCommands.handler.spellController.show_info() 

827#@+node:ekr.20180211104019.1: ** @g.command('clean-main-spell-dict') 

828@g.command('clean-main-spell-dict') 

829def clean_main_spell_dict(event): 

830 """ 

831 Clean the main spelling dictionary used *only* by the default spell 

832 checker. 

833 

834 This command works regardless of the spell checker being used. 

835 """ 

836 c = event and event.get('c') 

837 if c: 

838 DefaultWrapper(c).save_main_dict(trace=True) 

839#@+node:ekr.20180211105748.1: ** @g.command('clean-user-spell-dict') 

840@g.command('clean-user-spell-dict') 

841def clean_user_spell_dict(event): 

842 """ 

843 Clean the user spelling dictionary used *only* by the default spell 

844 checker. Mostly for debugging, because this happens automatically. 

845 

846 This command works regardless of the spell checker being used. 

847 """ 

848 c = event and event.get('c') 

849 if c: 

850 DefaultWrapper(c).save_user_dict(trace=True) 

851#@-others 

852#@@language python 

853#@@tabwidth -4 

854#@-leo