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

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.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 >>
17def cmd(name):
18 """Command decorator for the abbrevCommands class."""
19 return g.new_cmd_decorator(name, ['c', 'abbrevCommands',])
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()
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)
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 try:
203 old_disable = g.app.disable_redraw
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.
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).
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)
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