Coverage for C:\leo.repo\leo-editor\leo\commands\abbrevCommands.py: 22%

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

542 statements  

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20150514035236.1: * @file ../commands/abbrevCommands.py 

4#@@first 

5"""Leo's abbreviations commands.""" 

6#@+<< imports >> 

7#@+node:ekr.20150514045700.1: ** << imports >> (abbrevCommands.py) 

8import functools 

9import re 

10import string 

11from typing import Dict 

12from leo.core import leoGlobals as g 

13from leo.core import leoNodes 

14from leo.commands.baseCommands import BaseEditCommandsClass 

15#@-<< imports >> 

16 

17def cmd(name): 

18 """Command decorator for the abbrevCommands class.""" 

19 return g.new_cmd_decorator(name, ['c', 'abbrevCommands',]) 

20 

21#@+others 

22#@+node:ekr.20160514095531.1: ** class AbbrevCommands 

23class AbbrevCommandsClass(BaseEditCommandsClass): 

24 """ 

25 A class to handle user-defined abbreviations. 

26 See apropos-abbreviations for details. 

27 """ 

28 #@+others 

29 #@+node:ekr.20150514043850.2: *3* abbrev.Birth 

30 #@+node:ekr.20150514043850.3: *4* abbrev.ctor 

31 def __init__(self, c): 

32 """Ctor for AbbrevCommandsClass class.""" 

33 # pylint: disable=super-init-not-called 

34 self.c = c 

35 # Set local ivars. 

36 self.abbrevs = {} # Keys are names, values are (abbrev,tag). 

37 self.daRanges = [] 

38 self.dynaregex = re.compile( # For dynamic abbreviations 

39 r'[%s%s\-_]+' % (string.ascii_letters, string.digits)) 

40 # Not a unicode problem. 

41 self.n_regex = re.compile(r'(?<!\\)\\n') # to replace \\n but not \\\\n 

42 self.expanding = False # True: expanding abbreviations. 

43 self.event = None 

44 self.last_hit = None # Distinguish between text and tree abbreviations. 

45 self.root = None # The root of tree abbreviations. 

46 self.save_ins = None # Saved insert point. 

47 self.save_sel = None # Saved selection range. 

48 self.store = {'rlist': [], 'stext': ''} # For dynamic expansion. 

49 self.tree_abbrevs_d = {} # Keys are names, values are (tree,tag). 

50 self.w = None 

51 #@+node:ekr.20150514043850.5: *4* abbrev.finishCreate & helpers 

52 def finishCreate(self): 

53 """AbbrevCommandsClass.finishCreate.""" 

54 self.reload_settings() 

55 # Annoying. 

56 # c = self.c 

57 # if (not g.app.initing and not g.unitTesting and 

58 # not g.app.batchMode and not c.gui.isNullGui 

59 # ): 

60 # g.red('Abbreviations %s' % ('on' if c.k.abbrevOn else 'off')) 

61 #@+node:ekr.20170221035644.1: *5* abbrev.reload_settings & helpers 

62 def reload_settings(self): 

63 """Reload all abbreviation settings.""" 

64 self.abbrevs = {} 

65 self.init_settings() 

66 self.init_abbrev() 

67 self.init_tree_abbrev() 

68 self.init_env() 

69 

70 reloadSettings = reload_settings 

71 #@+node:ekr.20150514043850.6: *6* abbrev.init_abbrev 

72 def init_abbrev(self): 

73 """ 

74 Init the user abbreviations from @data global-abbreviations 

75 and @data abbreviations nodes. 

76 """ 

77 c = self.c 

78 table = ( 

79 ('global-abbreviations', 'global'), 

80 ('abbreviations', 'local'), 

81 ) 

82 for source, tag in table: 

83 aList = c.config.getData(source, strip_data=False) or [] 

84 abbrev, result = [], [] 

85 for s in aList: 

86 if s.startswith('\\:'): 

87 # Continue the previous abbreviation. 

88 abbrev.append(s[2:]) 

89 else: 

90 # End the previous abbreviation. 

91 if abbrev: 

92 result.append(''.join(abbrev)) 

93 abbrev = [] 

94 # Start the new abbreviation. 

95 if s.strip(): 

96 abbrev.append(s) 

97 # End any remaining abbreviation. 

98 if abbrev: 

99 result.append(''.join(abbrev)) 

100 for s in result: 

101 self.addAbbrevHelper(s, tag) 

102 

103 # fake the next placeholder abbreviation 

104 if c.config.getString("abbreviations-next-placeholder"): 

105 self.addAbbrevHelper( 

106 f'{c.config.getString("abbreviations-next-placeholder")}' 

107 f'=__NEXT_PLACEHOLDER', 

108 'global') 

109 #@+node:ekr.20150514043850.7: *6* abbrev.init_env 

110 def init_env(self): 

111 """ 

112 Init c.abbrev_subst_env by executing the contents of the 

113 @data abbreviations-subst-env node. 

114 """ 

115 c = self.c 

116 at = c.atFileCommands 

117 if c.abbrev_place_start and self.enabled: 

118 aList = self.subst_env 

119 script_list = [] 

120 for z in aList: 

121 # Compatibility with original design. 

122 if z.startswith('\\:'): 

123 script_list.append(z[2:]) 

124 else: 

125 script_list.append(z) 

126 script = ''.join(script_list) 

127 # Allow Leo directives in @data abbreviations-subst-env trees. 

128 # #1674: Avoid unnecessary entries in c.fileCommands.gnxDict. 

129 root = c.rootPosition() 

130 if root: 

131 v = root.v 

132 else: 

133 # Defensive programming. Probably will never happen. 

134 v = leoNodes.VNode(context=c) 

135 root = leoNodes.Position(v) 

136 # Similar to g.getScript. 

137 script = at.stringToString( 

138 root=root, 

139 s=script, 

140 forcePythonSentinels=True, 

141 sentinels=False) 

142 script = script.replace("\r\n", "\n") 

143 try: 

144 exec(script, c.abbrev_subst_env, c.abbrev_subst_env) # type:ignore 

145 except Exception: 

146 g.es('Error exec\'ing @data abbreviations-subst-env') 

147 g.es_exception() 

148 else: 

149 c.abbrev_subst_start = False 

150 #@+node:ekr.20150514043850.8: *6* abbrev.init_settings (called from reload_settings) 

151 def init_settings(self): 

152 """Called from AbbrevCommands.reload_settings aka reloadSettings.""" 

153 c = self.c 

154 c.k.abbrevOn = c.config.getBool('enable-abbreviations', default=False) 

155 c.abbrev_place_end = c.config.getString('abbreviations-place-end') 

156 c.abbrev_place_start = c.config.getString('abbreviations-place-start') 

157 c.abbrev_subst_end = c.config.getString('abbreviations-subst-end') 

158 c.abbrev_subst_env = {'c': c, 'g': g, '_values': {},} 

159 # The environment for all substitutions. 

160 # May be augmented in init_env. 

161 c.abbrev_subst_start = c.config.getString('abbreviations-subst-start') 

162 # Local settings. 

163 self.enabled = ( 

164 c.config.getBool('scripting-at-script-nodes') or 

165 c.config.getBool('scripting-abbreviations')) 

166 self.globalDynamicAbbrevs = c.config.getBool('globalDynamicAbbrevs') 

167 # @data abbreviations-subst-env must *only* be defined in leoSettings.leo or myLeoSettings.leo! 

168 if c.config: 

169 key = 'abbreviations-subst-env' 

170 if c.config.isLocalSetting(key, 'data'): 

171 g.issueSecurityWarning(f"@data {key}") 

172 self.subst_env = "" 

173 else: 

174 self.subst_env = c.config.getData(key, strip_data=False) 

175 #@+node:ekr.20150514043850.9: *6* abbrev.init_tree_abbrev 

176 def init_tree_abbrev(self): 

177 """Init tree_abbrevs_d from @data tree-abbreviations nodes.""" 

178 c = self.c 

179 # 

180 # Careful. This happens early in startup. 

181 root = c.rootPosition() 

182 if not root: 

183 return 

184 if not c.p: 

185 c.selectPosition(root) 

186 if not c.p: 

187 return 

188 data = c.config.getOutlineData('tree-abbreviations') 

189 if data is None: 

190 return 

191 d: Dict[str, str] = {} 

192 # #904: data may be a string or a list of two strings. 

193 aList = [data] if isinstance(data, str) else data 

194 for tree_s in aList: 

195 # 

196 # Expand the tree so we can traverse it. 

197 if not c.canPasteOutline(tree_s): 

198 return 

199 c.fileCommands.leo_file_encoding = 'utf-8' 

200 # 

201 # As part of #427, disable all redraws. 

202 old_disable = g.app.disable_redraw 

203 try: 

204 g.app.disable_redraw = True 

205 self.init_tree_abbrev_helper(d, tree_s) 

206 finally: 

207 g.app.disable_redraw = old_disable 

208 self.tree_abbrevs_d = d 

209 #@+node:ekr.20170227062001.1: *7* abbrev.init_tree_abbrev_helper 

210 def init_tree_abbrev_helper(self, d, tree_s): 

211 """Init d from tree_s, the text of a copied outline.""" 

212 c = self.c 

213 hidden_root = c.fileCommands.getPosFromClipboard(tree_s) 

214 if not hidden_root: 

215 g.trace('no pasted node') 

216 return 

217 for p in hidden_root.children(): 

218 for s in g.splitLines(p.b): 

219 if s.strip() and not s.startswith('#'): 

220 abbrev_name = s.strip() 

221 # #926: Allow organizer nodes by searching all descendants. 

222 for child in p.subtree(): 

223 if child.h.strip() == abbrev_name: 

224 abbrev_s = c.fileCommands.outline_to_clipboard_string(child) 

225 d[abbrev_name] = abbrev_s 

226 break 

227 else: 

228 g.trace(f"no definition for {abbrev_name}") 

229 #@+node:ekr.20150514043850.11: *3* abbrev.expandAbbrev & helpers (entry point) 

230 def expandAbbrev(self, event, stroke): 

231 """ 

232 Not a command. Expand abbreviations in event.widget. 

233 

234 Words start with '@'. 

235 """ 

236 # Trace for *either* 'abbrev' or 'keys' 

237 trace = any(z in g.app.debug for z in ('abbrev', 'keys')) 

238 # Verbose only for *both* 'abbrev' and 'verbose'. 

239 verbose = all(z in g.app.debug for z in ('abbrev', 'verbose')) 

240 c, p = self.c, self.c.p 

241 w = self.editWidget(event, forceFocus=False) 

242 w_name = g.app.gui.widget_name(w) 

243 if not w: 

244 if trace and verbose: 

245 g.trace('no w') 

246 return False 

247 ch = self.get_ch(event, stroke, w) 

248 if not ch: 

249 if trace and verbose: 

250 g.trace('no ch') 

251 return False 

252 s, i, j, prefixes = self.get_prefixes(w) 

253 for prefix in prefixes: 

254 i, tag, word, val = self.match_prefix(ch, i, j, prefix, s) 

255 if word: 

256 # Fix another part of #438. 

257 if w_name.startswith('head'): 

258 if val == '__NEXT_PLACEHOLDER': 

259 i = w.getInsertPoint() 

260 if i > 0: 

261 w.delete(i - 1) 

262 p.h = w.getAllText() 

263 # Do not call c.endEditing here. 

264 break 

265 else: 

266 if trace and verbose: 

267 g.trace(f"No prefix in {s!r}") 

268 return False 

269 c.abbrev_subst_env['_abr'] = word 

270 if trace: 

271 g.trace(f"Found {word!r} = {val!r}") 

272 if tag == 'tree': 

273 self.root = p.copy() 

274 self.last_hit = p.copy() 

275 self.expand_tree(w, i, j, val, word) 

276 else: 

277 # Never expand a search for text matches. 

278 place_holder = '__NEXT_PLACEHOLDER' in val 

279 if place_holder: 

280 expand_search = bool(self.last_hit) 

281 else: 

282 self.last_hit = None 

283 expand_search = False 

284 self.expand_text(w, i, j, val, word, expand_search) 

285 # Restore the selection range. 

286 if self.save_ins: 

287 ins = self.save_ins 

288 # pylint: disable=unpacking-non-sequence 

289 sel1, sel2 = self.save_sel 

290 if sel1 == sel2: 

291 # New in Leo 5.5 

292 self.post_pass() 

293 else: 

294 # some abbreviations *set* the selection range 

295 # so only restore non-empty ranges 

296 w.setSelectionRange(sel1, sel2, insert=ins) 

297 return True 

298 #@+node:ekr.20161121121636.1: *4* abbrev.exec_content 

299 def exec_content(self, content): 

300 """Execute the content in the environment, and return the result.""" 

301 #@+node:ekr.20150514043850.12: *4* abbrev.expand_text 

302 def expand_text(self, w, i, j, val, word, expand_search=False): 

303 """Make a text expansion at location i,j of widget w.""" 

304 c = self.c 

305 if word == c.config.getString("abbreviations-next-placeholder"): 

306 val = '' 

307 do_placeholder = True 

308 else: 

309 val, do_placeholder = self.make_script_substitutions(i, j, val) 

310 self.replace_selection(w, i, j, val) 

311 # Search to the end. We may have been called via a tree abbrev. 

312 p = c.p.copy() 

313 if expand_search: 

314 while p: 

315 if self.find_place_holder(p, do_placeholder): 

316 return 

317 p.moveToThreadNext() 

318 else: 

319 self.find_place_holder(p, do_placeholder) 

320 #@+node:ekr.20150514043850.13: *4* abbrev.expand_tree (entry) & helpers 

321 def expand_tree(self, w, i, j, tree_s, word): 

322 """ 

323 Paste tree_s as children of c.p. 

324 This happens *before* any substitutions are made. 

325 """ 

326 c, u = self.c, self.c.undoer 

327 if not c.canPasteOutline(tree_s): 

328 g.trace(f"bad copied outline: {tree_s}") 

329 return 

330 old_p = c.p.copy() 

331 bunch = u.beforeChangeTree(old_p) 

332 self.replace_selection(w, i, j, None) 

333 self.paste_tree(old_p, tree_s) 

334 # Make all script substitutions first. 

335 # Original code. Probably unwise to change it. 

336 do_placeholder = False 

337 for p in old_p.self_and_subtree(): 

338 # Search for the next place-holder. 

339 val, do_placeholder = self.make_script_substitutions(0, 0, p.b) 

340 if not do_placeholder: 

341 p.b = val 

342 # Now search for all place-holders. 

343 for p in old_p.subtree(): 

344 if self.find_place_holder(p, do_placeholder): 

345 break 

346 u.afterChangeTree(old_p, 'tree-abbreviation', bunch) 

347 #@+node:ekr.20150514043850.17: *5* abbrev.paste_tree 

348 def paste_tree(self, old_p, s): 

349 """Paste the tree corresponding to s (xml) into the tree.""" 

350 c = self.c 

351 c.fileCommands.leo_file_encoding = 'utf-8' 

352 p = c.pasteOutline(s=s, undoFlag=False) 

353 if p: 

354 # Promote the name node, then delete it. 

355 p.moveToLastChildOf(old_p) 

356 c.selectPosition(p) 

357 c.promote(undoFlag=False) 

358 p.doDelete() 

359 c.redraw(old_p) # 2017/02/27: required. 

360 else: 

361 g.trace('paste failed') 

362 #@+node:ekr.20150514043850.14: *4* abbrev.find_place_holder 

363 def find_place_holder(self, p, do_placeholder): 

364 """ 

365 Search for the next place-holder. 

366 If found, select the place-holder (without the delims). 

367 """ 

368 c, u = self.c, self.c.undoer 

369 # Do #438: Search for placeholder in headline. 

370 s = p.h 

371 if do_placeholder or c.abbrev_place_start and c.abbrev_place_start in s: 

372 new_s, i, j = self.next_place(s, offset=0) 

373 if i is not None: 

374 p.h = new_s 

375 c.redraw(p) 

376 c.editHeadline() 

377 w = c.edit_widget(p) 

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

379 return True 

380 s = p.b 

381 if do_placeholder or c.abbrev_place_start and c.abbrev_place_start in s: 

382 new_s, i, j = self.next_place(s, offset=0) 

383 if i is None: 

384 return False 

385 w = c.frame.body.wrapper 

386 bunch = u.beforeChangeBody(c.p) 

387 switch = p != c.p 

388 if switch: 

389 c.selectPosition(p) 

390 else: 

391 scroll = w.getYScrollPosition() 

392 w.setAllText(new_s) 

393 p.v.b = new_s 

394 u.afterChangeBody(p, 'find-place-holder', bunch) 

395 if switch: 

396 c.redraw() 

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

398 if switch: 

399 w.seeInsertPoint() 

400 else: 

401 # Keep the scroll point if possible. 

402 w.setYScrollPosition(scroll) 

403 w.seeInsertPoint() 

404 c.bodyWantsFocusNow() 

405 return True 

406 # #453: do nothing here. 

407 # c.frame.body.forceFullRecolor() 

408 # c.bodyWantsFocusNow() 

409 return False 

410 #@+node:ekr.20150514043850.15: *4* abbrev.make_script_substitutions 

411 def make_script_substitutions(self, i, j, val): 

412 """Make scripting substitutions in node p.""" 

413 c, u, w = self.c, self.c.undoer, self.c.frame.body.wrapper 

414 if not c.abbrev_subst_start: 

415 return val, False 

416 # Nothing to undo. 

417 if c.abbrev_subst_start not in val: 

418 return val, False 

419 # The *before* snapshot. 

420 bunch = u.beforeChangeBody(c.p) 

421 # Perform all scripting substitutions. 

422 self.save_ins = None 

423 self.save_sel = None 

424 while c.abbrev_subst_start in val: 

425 prefix, rest = val.split(c.abbrev_subst_start, 1) 

426 content = rest.split(c.abbrev_subst_end, 1) 

427 if len(content) != 2: 

428 break 

429 content, rest = content 

430 try: 

431 self.expanding = True 

432 c.abbrev_subst_env['x'] = '' 

433 exec(content, c.abbrev_subst_env, c.abbrev_subst_env) 

434 except Exception: 

435 g.es_print('exception evaluating', content) 

436 g.es_exception() 

437 finally: 

438 self.expanding = False 

439 x = c.abbrev_subst_env.get('x') 

440 if x is None: 

441 x = '' 

442 val = f"{prefix}{x}{rest}" 

443 # Save the selection range. 

444 self.save_ins = w.getInsertPoint() 

445 self.save_sel = w.getSelectionRange() 

446 if val == "__NEXT_PLACEHOLDER": 

447 # user explicitly called for next placeholder in an abbrev. 

448 # inserted previously 

449 val = '' 

450 do_placeholder = True 

451 else: 

452 do_placeholder = False 

453 c.p.v.b = w.getAllText() 

454 u.afterChangeBody(c.p, 'make-script-substitution', bunch) 

455 return val, do_placeholder 

456 #@+node:ekr.20161121102113.1: *4* abbrev.make_script_substitutions_in_headline 

457 def make_script_substitutions_in_headline(self, p): 

458 """Make scripting substitutions in p.h.""" 

459 c = self.c 

460 pattern = re.compile(r'^(.*)%s(.+)%s(.*)$' % ( 

461 re.escape(c.abbrev_subst_start), 

462 re.escape(c.abbrev_subst_end), 

463 )) 

464 changed = False 

465 # Perform at most one scripting substition. 

466 m = pattern.match(p.h) 

467 if m: 

468 content = m.group(2) 

469 c.abbrev_subst_env['x'] = '' 

470 try: 

471 exec(content, c.abbrev_subst_env, c.abbrev_subst_env) 

472 x = c.abbrev_subst_env.get('x') 

473 if x: 

474 p.h = f"{m.group(1)}{x}{m.group(3)}" 

475 changed = True 

476 except Exception: 

477 # Leave p.h alone. 

478 g.trace('scripting error in', p.h) 

479 g.es_exception() 

480 return changed 

481 #@+node:ekr.20161121112837.1: *4* abbrev.match_prefix 

482 def match_prefix(self, ch, i, j, prefix, s): 

483 """A match helper.""" 

484 i = j - len(prefix) 

485 word = g.checkUnicode(prefix) + g.checkUnicode(ch) 

486 tag = 'tree' 

487 val = self.tree_abbrevs_d.get(word) 

488 if not val: 

489 val, tag = self.abbrevs.get(word, (None, None)) 

490 if val: 

491 # Require a word match if the abbreviation is itself a word. 

492 if ch in ' \t\n': 

493 word = word.rstrip() 

494 if word.isalnum() and word[0].isalpha(): 

495 if i == 0 or s[i - 1] in ' \t\n': 

496 pass 

497 else: 

498 i -= 1 

499 word, val = None, None # 2017/03/19. 

500 else: 

501 i -= 1 

502 word, val = None, None 

503 return i, tag, word, val 

504 #@+node:ekr.20150514043850.16: *4* abbrev.next_place 

505 def next_place(self, s, offset=0): 

506 """ 

507 Given string s containing a placeholder like <| block |>, 

508 return (s2,start,end) where s2 is s without the <| and |>, 

509 and start, end are the positions of the beginning and end of block. 

510 """ 

511 c = self.c 

512 if c.abbrev_place_start is None or c.abbrev_place_end is None: 

513 return s, None, None # #1345. 

514 new_pos = s.find(c.abbrev_place_start, offset) 

515 new_end = s.find(c.abbrev_place_end, offset) 

516 if (new_pos < 0 or new_end < 0) and offset: 

517 new_pos = s.find(c.abbrev_place_start) 

518 new_end = s.find(c.abbrev_place_end) 

519 if not (new_pos < 0 or new_end < 0): 

520 g.es("Found earlier placeholder") 

521 if new_pos < 0 or new_end < 0: 

522 return s, None, None 

523 start = new_pos 

524 place_holder_delim = s[new_pos : new_end + len(c.abbrev_place_end)] 

525 place_holder = place_holder_delim[ 

526 len(c.abbrev_place_start) : -len(c.abbrev_place_end)] 

527 s2 = s[:start] + place_holder + s[start + len(place_holder_delim) :] 

528 end = start + len(place_holder) 

529 return s2, start, end 

530 #@+node:ekr.20161121114504.1: *4* abbrev.post_pass 

531 def post_pass(self): 

532 """The post pass: make script substitutions in all headlines.""" 

533 c = self.c 

534 if self.root: 

535 bunch = c.undoer.beforeChangeTree(c.p) 

536 changed = False 

537 for p in self.root.self_and_subtree(): 

538 changed2 = self.make_script_substitutions_in_headline(p) 

539 changed = changed or changed2 

540 if changed: 

541 c.undoer.afterChangeTree(c.p, 'tree-post-abbreviation', bunch) 

542 #@+node:ekr.20150514043850.18: *4* abbrev.replace_selection 

543 def replace_selection(self, w, i, j, s): 

544 """Replace w[i:j] by s.""" 

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

546 w_name = g.app.gui.widget_name(w) 

547 bunch = u.beforeChangeBody(p) 

548 if i == j: 

549 abbrev = '' 

550 else: 

551 abbrev = w.get(i, j) 

552 w.delete(i, j) 

553 if s is not None: 

554 w.insert(i, s) 

555 if w_name.startswith('head'): 

556 pass # Don't set p.h here! 

557 else: 

558 # Fix part of #438. Don't leave the headline. 

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

560 u.afterChangeBody(p, 'Abbreviation', bunch) 

561 # Adjust self.save_sel & self.save_ins 

562 if s is not None and self.save_sel is not None: 

563 # pylint: disable=unpacking-non-sequence 

564 i, j = self.save_sel 

565 ins = self.save_ins 

566 delta = len(s) - len(abbrev) 

567 self.save_sel = i + delta, j + delta 

568 self.save_ins = ins + delta 

569 #@+node:ekr.20161121111502.1: *4* abbrev_get_ch 

570 def get_ch(self, event, stroke, w): 

571 """Get the ch from the stroke.""" 

572 ch = g.checkUnicode(event and event.char or '') 

573 if self.expanding: 

574 return None 

575 if w.hasSelection(): 

576 return None 

577 assert g.isStrokeOrNone(stroke), stroke 

578 if stroke in ('BackSpace', 'Delete'): 

579 return None 

580 d = {'Return': '\n', 'Tab': '\t', 'space': ' ', 'underscore': '_'} 

581 if stroke: 

582 ch = d.get(stroke.s, stroke.s) 

583 if len(ch) > 1: 

584 if (stroke.find('Ctrl+') > -1 or 

585 stroke.find('Alt+') > -1 or 

586 stroke.find('Meta+') > -1 

587 ): 

588 ch = '' 

589 else: 

590 ch = event.char if event else '' 

591 else: 

592 ch = event.char 

593 return ch 

594 #@+node:ekr.20161121112346.1: *4* abbrev_get_prefixes 

595 def get_prefixes(self, w): 

596 """Return the prefixes at the current insertion point of w.""" 

597 # New code allows *any* sequence longer than 1 to be an abbreviation. 

598 # Any whitespace stops the search. 

599 s = w.getAllText() 

600 j = w.getInsertPoint() 

601 i, prefixes = j - 1, [] 

602 while len(s) > i >= 0 and s[i] not in ' \t\n': 

603 prefixes.append(s[i:j]) 

604 i -= 1 

605 prefixes = list(reversed(prefixes)) 

606 if '' not in prefixes: 

607 prefixes.append('') 

608 return s, i, j, prefixes 

609 #@+node:ekr.20150514043850.19: *3* abbrev.dynamic abbreviation... 

610 #@+node:ekr.20150514043850.20: *4* abbrev.dynamicCompletion C-M-/ 

611 @cmd('dabbrev-completion') 

612 def dynamicCompletion(self, event=None): 

613 """ 

614 dabbrev-completion 

615 Insert the common prefix of all dynamic abbrev's matching the present word. 

616 This corresponds to C-M-/ in Emacs. 

617 """ 

618 c, p = self.c, self.c.p 

619 w = self.editWidget(event) 

620 if not w: 

621 return 

622 s = w.getAllText() 

623 ins = ins1 = w.getInsertPoint() 

624 if 0 < ins < len(s) and not g.isWordChar(s[ins]): 

625 ins1 -= 1 

626 i, j = g.getWord(s, ins1) 

627 word = w.get(i, j) 

628 aList = self.getDynamicList(w, word) 

629 if not aList: 

630 return 

631 # Bug fix: remove s itself, otherwise we can not extend beyond it. 

632 if word in aList and len(aList) > 1: 

633 aList.remove(word) 

634 prefix = functools.reduce(g.longestCommonPrefix, aList) 

635 if prefix.strip(): 

636 ypos = w.getYScrollPosition() 

637 b = c.undoer.beforeChangeNodeContents(p) 

638 s = s[:i] + prefix + s[j:] 

639 w.setAllText(s) 

640 w.setInsertPoint(i + len(prefix)) 

641 w.setYScrollPosition(ypos) 

642 c.undoer.afterChangeNodeContents(p, command='dabbrev-completion', bunch=b) 

643 c.recolor() 

644 #@+node:ekr.20150514043850.21: *4* abbrev.dynamicExpansion M-/ & helper 

645 @cmd('dabbrev-expands') 

646 def dynamicExpansion(self, event=None): 

647 """ 

648 dabbrev-expands (M-/ in Emacs). 

649 

650 Inserts the longest common prefix of the word at the cursor. Displays 

651 all possible completions if the prefix is the same as the word. 

652 """ 

653 w = self.editWidget(event) 

654 if not w: 

655 return 

656 s = w.getAllText() 

657 ins = ins1 = w.getInsertPoint() 

658 if 0 < ins < len(s) and not g.isWordChar(s[ins]): 

659 ins1 -= 1 

660 i, j = g.getWord(s, ins1) 

661 w.setInsertPoint(j) 

662 # This allows the cursor to be placed anywhere in the word. 

663 word = w.get(i, j) 

664 aList = self.getDynamicList(w, word) 

665 if not aList: 

666 return 

667 if word in aList and len(aList) > 1: 

668 aList.remove(word) 

669 prefix = functools.reduce(g.longestCommonPrefix, aList) 

670 prefix = prefix.strip() 

671 self.dynamicExpandHelper(event, prefix, aList, w) 

672 #@+node:ekr.20150514043850.22: *5* abbrev.dynamicExpandHelper 

673 def dynamicExpandHelper(self, event, prefix=None, aList=None, w=None): 

674 """State handler for dabbrev-expands command.""" 

675 c, k = self.c, self.c.k 

676 self.w = w 

677 if prefix is None: 

678 prefix = '' 

679 prefix2 = 'dabbrev-expand: ' 

680 c.frame.log.deleteTab('Completion') 

681 g.es('', '\n'.join(aList or []), tabName='Completion') 

682 # Protect only prefix2 so tab completion and backspace to work properly. 

683 k.setLabelBlue(prefix2, protect=True) 

684 k.setLabelBlue(prefix2 + prefix, protect=False) 

685 k.get1Arg(event, handler=self.dynamicExpandHelper1, tabList=aList, prefix=prefix) 

686 

687 def dynamicExpandHelper1(self, event): 

688 """Finisher for dabbrev-expands.""" 

689 c, k = self.c, self.c.k 

690 p = c.p 

691 c.frame.log.deleteTab('Completion') 

692 k.clearState() 

693 k.resetLabel() 

694 if k.arg: 

695 w = self.w 

696 s = w.getAllText() 

697 ypos = w.getYScrollPosition() 

698 b = c.undoer.beforeChangeNodeContents(p) 

699 ins = ins1 = w.getInsertPoint() 

700 if 0 < ins < len(s) and not g.isWordChar(s[ins]): 

701 ins1 -= 1 

702 i, j = g.getWord(s, ins1) 

703 # word = s[i: j] 

704 s = s[:i] + k.arg + s[j:] 

705 w.setAllText(s) 

706 w.setInsertPoint(i + len(k.arg)) 

707 w.setYScrollPosition(ypos) 

708 c.undoer.afterChangeNodeContents(p, command='dabbrev-expand', bunch=b) 

709 c.recolor() 

710 #@+node:ekr.20150514043850.23: *4* abbrev.getDynamicList (helper) 

711 def getDynamicList(self, w, s): 

712 """Return a list of dynamic abbreviations.""" 

713 if self.globalDynamicAbbrevs: 

714 # Look in all nodes.h 

715 items = [] 

716 for p in self.c.all_unique_positions(): 

717 items.extend(self.dynaregex.findall(p.b)) 

718 else: 

719 # Just look in this node. 

720 items = self.dynaregex.findall(w.getAllText()) 

721 items = sorted(set([z for z in items if z.startswith(s)])) 

722 return items 

723 #@+node:ekr.20150514043850.24: *3* abbrev.static abbrevs 

724 #@+node:ekr.20150514043850.25: *4* abbrev.addAbbrevHelper 

725 def addAbbrevHelper(self, s, tag=''): 

726 """Enter the abbreviation 's' into the self.abbrevs dict.""" 

727 if not s.strip(): 

728 return 

729 try: 

730 d = self.abbrevs 

731 data = s.split('=') 

732 # Do *not* strip ws so the user can specify ws. 

733 name = data[0].replace('\\t', '\t').replace('\\n', '\n') 

734 val = '='.join(data[1:]) 

735 if val.endswith('\n'): 

736 val = val[:-1] 

737 val = self.n_regex.sub('\n', val).replace('\\\\n', '\\n') 

738 old, tag = d.get(name, (None, None),) 

739 if old and old != val and not g.unitTesting: 

740 g.es_print('redefining abbreviation', name, 

741 '\nfrom', repr(old), 'to', repr(val)) 

742 d[name] = val, tag 

743 except ValueError: 

744 g.es_print(f"bad abbreviation: {s}") 

745 #@+node:ekr.20150514043850.28: *4* abbrev.killAllAbbrevs 

746 @cmd('abbrev-kill-all') 

747 def killAllAbbrevs(self, event): 

748 """Delete all abbreviations.""" 

749 self.abbrevs = {} 

750 #@+node:ekr.20150514043850.29: *4* abbrev.listAbbrevs 

751 @cmd('abbrev-list') 

752 def listAbbrevs(self, event=None): 

753 """List all abbreviations.""" 

754 d = self.abbrevs 

755 if d: 

756 g.es_print('Abbreviations...') 

757 keys = list(d.keys()) 

758 keys.sort() 

759 for name in keys: 

760 val, tag = d.get(name) 

761 val = val.replace('\n', '\\n') 

762 tag = tag or '' 

763 tag = tag + ': ' if tag else '' 

764 g.es_print('', f"{tag}{name}={val}") 

765 else: 

766 g.es_print('No present abbreviations') 

767 #@+node:ekr.20150514043850.32: *4* abbrev.toggleAbbrevMode 

768 @cmd('toggle-abbrev-mode') 

769 def toggleAbbrevMode(self, event=None): 

770 """Toggle abbreviation mode.""" 

771 k = self.c.k 

772 k.abbrevOn = not k.abbrevOn 

773 k.keyboardQuit() 

774 if not g.unitTesting and not g.app.batchMode: 

775 g.es('Abbreviations are ' + 'on' if k.abbrevOn else 'off') 

776 #@-others 

777#@-others 

778#@-leo