Coverage for C:\Repos\leo-editor\leo\core\leoGlobals.py: 43%
4808 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20031218072017.3093: * @file leoGlobals.py
4#@@first
5"""
6Global constants, variables and utility functions used throughout Leo.
8Important: This module imports no other Leo module.
9"""
10#@+<< imports >>
11#@+node:ekr.20050208101229: ** << imports >> (leoGlobals)
12import binascii
13import codecs
14import fnmatch
15from functools import reduce
16import gc
17import gettext
18import glob
19import importlib
20import inspect
21import io
22import operator
23import os
24from pathlib import Path
25# import pdb # Do NOT import pdb here! g.pdb is a *function*
26import re
27import shlex
28import string
29import sys
30import subprocess
31import tempfile
32import textwrap
33import time
34import traceback
35import types
36from typing import TYPE_CHECKING
37from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Sequence, Set, Tuple, Union
38import unittest
39import urllib
40import urllib.parse as urlparse
41# Third-party tools.
42import webbrowser
43try:
44 import tkinter as Tk
45except Exception:
46 Tk = None
47#
48# Leo never imports any other Leo module.
49if TYPE_CHECKING: # Always False at runtime.
50 from leo.core.leoCommands import Commands as Cmdr
51 from leo.core.leoNodes import Position as Pos
52 from leo.core.leoNodes import VNode
53else:
54 Cmdr = Pos = VNode = Any
55#
56# Abbreviations...
57StringIO = io.StringIO
58#@-<< imports >>
59in_bridge = False # True: leoApp object loads a null Gui.
60in_vs_code = False # #2098.
61minimum_python_version = '3.6' # #1215.
62isPython3 = sys.version_info >= (3, 0, 0)
63isMac = sys.platform.startswith('darwin')
64isWindows = sys.platform.startswith('win')
65#@+<< define g.globalDirectiveList >>
66#@+node:EKR.20040610094819: ** << define g.globalDirectiveList >>
67# Visible externally so plugins may add to the list of directives.
68# The atFile write logic uses this, but not the atFile read logic.
69globalDirectiveList = [
70 # Order does not matter.
71 'all',
72 'beautify',
73 'colorcache', 'code', 'color', 'comment', 'c',
74 'delims', 'doc',
75 'encoding',
76 # 'end_raw', # #2276.
77 'first', 'header', 'ignore',
78 'killbeautify', 'killcolor',
79 'language', 'last', 'lineending',
80 'markup',
81 'nobeautify',
82 'nocolor-node', 'nocolor', 'noheader', 'nowrap',
83 'nopyflakes', # Leo 6.1.
84 'nosearch', # Leo 5.3.
85 'others', 'pagewidth', 'path', 'quiet',
86 # 'raw', # #2276.
87 'section-delims', # Leo 6.6. #2276.
88 'silent',
89 'tabwidth', 'terse',
90 'unit', 'verbose', 'wrap',
91]
93directives_pat = None # Set below.
94#@-<< define g.globalDirectiveList >>
95#@+<< define global decorator dicts >>
96#@+node:ekr.20150510103918.1: ** << define global decorator dicts >> (leoGlobals.py)
97#@@nobeautify
98#@@language rest
99#@+at
100# The cmd_instance_dict supports per-class @cmd decorators. For example, the
101# following appears in leo.commands.
102#
103# def cmd(name: Any) -> Any:
104# """Command decorator for the abbrevCommands class."""
105# return g.new_cmd_decorator(name, ['c', 'abbrevCommands',])
106#
107# For commands based on functions, use the @g.command decorator.
108#@@c
109#@@language python
111global_commands_dict = {}
113cmd_instance_dict = {
114 # Keys are class names, values are attribute chains.
115 'AbbrevCommandsClass': ['c', 'abbrevCommands'],
116 'AtFile': ['c', 'atFileCommands'],
117 'AutoCompleterClass': ['c', 'k', 'autoCompleter'],
118 'ChapterController': ['c', 'chapterController'],
119 'Commands': ['c'],
120 'ControlCommandsClass': ['c', 'controlCommands'],
121 'DebugCommandsClass': ['c', 'debugCommands'],
122 'EditCommandsClass': ['c', 'editCommands'],
123 'EditFileCommandsClass': ['c', 'editFileCommands'],
124 'FileCommands': ['c', 'fileCommands'],
125 'HelpCommandsClass': ['c', 'helpCommands'],
126 'KeyHandlerClass': ['c', 'k'],
127 'KeyHandlerCommandsClass': ['c', 'keyHandlerCommands'],
128 'KillBufferCommandsClass': ['c', 'killBufferCommands'],
129 'LeoApp': ['g', 'app'],
130 'LeoFind': ['c', 'findCommands'],
131 'LeoImportCommands': ['c', 'importCommands'],
132 # 'MacroCommandsClass': ['c', 'macroCommands'],
133 'PrintingController': ['c', 'printingController'],
134 'RectangleCommandsClass': ['c', 'rectangleCommands'],
135 'RstCommands': ['c', 'rstCommands'],
136 'SpellCommandsClass': ['c', 'spellCommands'],
137 'Undoer': ['c', 'undoer'],
138 'VimCommands': ['c', 'vimCommands'],
139}
140#@-<< define global decorator dicts >>
141#@+<< define global error regexs >>
142#@+node:ekr.20220412193109.1: ** << define global error regexs >> (leoGlobals.py)
143# To do: error patterns for black and pyflakes.
145# Most code need only know about the *existence* of these patterns.
147# At table in LeoQtLog.put tells it how to extract filenames and line_numbers from each pattern.
148# For all *present* patterns, m.group(1) is the filename and m.group(2) is the line number.
150mypy_pat = re.compile(r'^(.+?):([0-9]+): (error|note): (.*)\s*$')
151pyflakes_pat = re.compile(r'^(.*):([0-9]+):[0-9]+ .*?$')
152pylint_pat = re.compile(r'^(.*):\s*([0-9]+)[,:]\s*[0-9]+:.*?\(.*\)\s*$')
153python_pat = re.compile(r'^\s*File\s+"(.*?)",\s*line\s*([0-9]+)\s*$')
154#@-<< define global error regexs >>
155#@+<< define g.decorators >>
156#@+node:ekr.20150508165324.1: ** << define g.Decorators >>
157#@+others
158#@+node:ekr.20150510104148.1: *3* g.check_cmd_instance_dict
159def check_cmd_instance_dict(c: Cmdr, g: Any) -> None:
160 """
161 Check g.check_cmd_instance_dict.
162 This is a permanent unit test, called from c.finishCreate.
163 """
164 d = cmd_instance_dict
165 for key in d:
166 ivars = d.get(key)
167 # Produces warnings.
168 obj = ivars2instance(c, g, ivars) # type:ignore
169 if obj:
170 name = obj.__class__.__name__
171 if name != key:
172 g.trace('class mismatch', key, name)
173#@+node:ville.20090521164644.5924: *3* g.command (decorator)
174class Command:
175 """
176 A global decorator for creating commands.
178 This is the recommended way of defining all new commands, including
179 commands that could befined inside a class. The typical usage is:
181 @g.command('command-name')
182 def A_Command(event):
183 c = event.get('c')
184 ...
186 g can *not* be used anywhere in this class!
187 """
189 def __init__(self, name: str, **kwargs: Any) -> None:
190 """Ctor for command decorator class."""
191 self.name = name
193 def __call__(self, func: Callable) -> Callable:
194 """Register command for all future commanders."""
195 global_commands_dict[self.name] = func
196 if app:
197 for c in app.commanders():
198 c.k.registerCommand(self.name, func)
199 # Inject ivars for plugins_menu.py.
200 func.__func_name__ = func.__name__ # For leoInteg.
201 func.is_command = True
202 func.command_name = self.name
203 return func
205command = Command
206#@+node:ekr.20171124070654.1: *3* g.command_alias
207def command_alias(alias: str, func: Callable) -> None:
208 """Create an alias for the *already defined* method in the Commands class."""
209 from leo.core import leoCommands
210 assert hasattr(leoCommands.Commands, func.__name__)
211 funcToMethod(func, leoCommands.Commands, alias)
212#@+node:ekr.20171123095526.1: *3* g.commander_command (decorator)
213class CommanderCommand:
214 """
215 A global decorator for creating commander commands, that is, commands
216 that were formerly methods of the Commands class in leoCommands.py.
218 Usage:
220 @g.command('command-name')
221 def command_name(self, *args, **kwargs):
222 ...
224 The decorator injects command_name into the Commander class and calls
225 funcToMethod so the ivar will be injected in all future commanders.
227 g can *not* be used anywhere in this class!
228 """
230 def __init__(self, name: str, **kwargs: Any) -> None:
231 """Ctor for command decorator class."""
232 self.name = name
234 def __call__(self, func: Callable) -> Callable:
235 """Register command for all future commanders."""
237 def commander_command_wrapper(event: Any) -> None:
238 c = event.get('c')
239 method = getattr(c, func.__name__, None)
240 method(event=event)
242 # Inject ivars for plugins_menu.py.
243 commander_command_wrapper.__func_name__ = func.__name__ # For leoInteg.
244 commander_command_wrapper.__name__ = self.name
245 commander_command_wrapper.__doc__ = func.__doc__
246 global_commands_dict[self.name] = commander_command_wrapper
247 if app:
248 from leo.core import leoCommands
249 funcToMethod(func, leoCommands.Commands)
250 for c in app.commanders():
251 c.k.registerCommand(self.name, func)
252 # Inject ivars for plugins_menu.py.
253 func.is_command = True
254 func.command_name = self.name
255 return func
257commander_command = CommanderCommand
258#@+node:ekr.20150508164812.1: *3* g.ivars2instance
259def ivars2instance(c: Cmdr, g: Any, ivars: List[str]) -> Any:
260 """
261 Return the instance of c given by ivars.
262 ivars is a list of strings.
263 A special case: ivars may be 'g', indicating the leoGlobals module.
264 """
265 if not ivars:
266 g.trace('can not happen: no ivars')
267 return None
268 ivar = ivars[0]
269 if ivar not in ('c', 'g'):
270 g.trace('can not happen: unknown base', ivar)
271 return None
272 obj = c if ivar == 'c' else g
273 for ivar in ivars[1:]:
274 obj = getattr(obj, ivar, None)
275 if not obj:
276 g.trace('can not happen: unknown attribute', obj, ivar, ivars)
277 break
278 return obj
279#@+node:ekr.20150508134046.1: *3* g.new_cmd_decorator (decorator)
280def new_cmd_decorator(name: str, ivars: List[str]) -> Callable:
281 """
282 Return a new decorator for a command with the given name.
283 Compute the class *instance* using the ivar string or list.
285 Don't even think about removing the @cmd decorators!
286 See https://github.com/leo-editor/leo-editor/issues/325
287 """
289 def _decorator(func: Callable) -> Callable:
291 def new_cmd_wrapper(event: Any) -> None:
292 if isinstance(event, dict):
293 c = event.get('c')
294 else:
295 c = event.c
296 self = g.ivars2instance(c, g, ivars)
297 try:
298 # Don't use a keyword for self.
299 # This allows the VimCommands class to use vc instead.
300 func(self, event=event)
301 except Exception:
302 g.es_exception()
304 new_cmd_wrapper.__func_name__ = func.__name__ # For leoInteg.
305 new_cmd_wrapper.__name__ = name
306 new_cmd_wrapper.__doc__ = func.__doc__
307 # Put the *wrapper* into the global dict.
308 global_commands_dict[name] = new_cmd_wrapper
309 return func # The decorator must return the func itself.
311 return _decorator
312#@-others
313#@-<< define g.decorators >>
314#@+<< define regex's >>
315#@+node:ekr.20200810093517.1: ** << define regex's >>
316# Regex used by this module, and in leoColorizer.py.
317g_language_pat = re.compile(r'^@language\s+(\w+)+', re.MULTILINE)
318#
319# Patterns used only in this module...
321# g_is_directive_pattern excludes @encoding.whatever and @encoding(whatever)
322# It must allow @language python, @nocolor-node, etc.
323g_is_directive_pattern = re.compile(r'^\s*@([\w-]+)\s*')
324g_noweb_root = re.compile('<' + '<' + '*' + '>' + '>' + '=', re.MULTILINE)
325g_tabwidth_pat = re.compile(r'(^@tabwidth)', re.MULTILINE)
326# #2267: Support for @section-delims.
327g_section_delims_pat = re.compile(r'^@section-delims[ \t]+([^ \w\n\t]+)[ \t]+([^ \w\n\t]+)[ \t]*$')
329# Regex to find GNX
330USERCHAR = r"""[^.,"'\s]""" # from LeoApp.cleanLeoID()
331USERID = f'{USERCHAR}{{2}}{USERCHAR}+' # At least three USERCHARs
332GNXre = re.compile(rf"""{USERID}\.
333 [0-9]+\. # timestamp
334 [0-9]+""", re.VERBOSE) # NodeIndices.lastIndex
335#@-<< define regex's >>
336tree_popup_handlers: List[Callable] = [] # Set later.
337user_dict: Dict[Any, Any] = {} # Non-persistent dictionary for scripts and plugins.
338app: Any = None # The singleton app object. Set by runLeo.py.
339# Global status vars.
340inScript = False # A synonym for app.inScript
341unitTesting = False # A synonym for app.unitTesting.
342#@+others
343#@+node:ekr.20201211182722.1: ** g.Backup
344#@+node:ekr.20201211182659.1: *3* g.standard_timestamp
345def standard_timestamp() -> str:
346 """Return a reasonable timestamp."""
347 return time.strftime("%Y%m%d-%H%M%S")
348#@+node:ekr.20201211183100.1: *3* g.get_backup_directory
349def get_backup_path(sub_directory: str) -> Optional[str]:
350 """
351 Return the full path to the subdirectory of the main backup directory.
353 The main backup directory is computed as follows:
355 1. os.environ['LEO_BACKUP']
356 2. ~/Backup
357 """
358 # Compute the main backup directory.
359 # First, try the LEO_BACKUP directory.
360 backup = None
361 try:
362 backup = os.environ['LEO_BACKUP']
363 if not os.path.exists(backup):
364 backup = None
365 except KeyError:
366 pass
367 except Exception:
368 g.es_exception()
369 # Second, try ~/Backup.
370 if not backup:
371 backup = os.path.join(str(Path.home()), 'Backup')
372 if not os.path.exists(backup):
373 backup = None
374 if not backup:
375 return None
376 # Compute the path to backup/sub_directory
377 directory = os.path.join(backup, sub_directory)
378 return directory if os.path.exists(directory) else None
379#@+node:ekr.20140711071454.17644: ** g.Classes & class accessors
380#@+node:ekr.20120123115816.10209: *3* class g.BindingInfo & isBindingInfo
381class BindingInfo:
382 """
383 A class representing any kind of key binding line.
385 This includes other information besides just the KeyStroke.
386 """
387 # Important: The startup code uses this class,
388 # so it is convenient to define it in leoGlobals.py.
389 #@+others
390 #@+node:ekr.20120129040823.10254: *4* bi.__init__
391 def __init__(
392 self,
393 kind: str,
394 commandName: str='',
395 func: Any=None,
396 nextMode: Any=None,
397 pane: Any=None,
398 stroke: Any=None,
399 ) -> None:
400 if not g.isStrokeOrNone(stroke):
401 g.trace('***** (BindingInfo) oops', repr(stroke))
402 self.kind = kind
403 self.commandName = commandName
404 self.func = func
405 self.nextMode = nextMode
406 self.pane = pane
407 self.stroke = stroke # The *caller* must canonicalize the shortcut.
408 #@+node:ekr.20120203153754.10031: *4* bi.__hash__
409 def __hash__(self) -> Any:
410 return self.stroke.__hash__() if self.stroke else 0
411 #@+node:ekr.20120125045244.10188: *4* bi.__repr__ & ___str_& dump
412 def __repr__(self) -> str:
413 return self.dump()
415 __str__ = __repr__
417 def dump(self) -> str:
418 result = [f"BindingInfo kind: {self.kind}"]
419 # Print all existing ivars.
420 table = ('pane', 'commandName', 'func', 'stroke') # 'nextMode',
421 for ivar in table:
422 if hasattr(self, ivar):
423 val = getattr(self, ivar)
424 if val not in (None, 'none', 'None', ''):
425 if ivar == 'func':
426 # pylint: disable=no-member
427 val = val.__name__
428 s = f"{ivar}: {val!r}"
429 result.append(s)
430 # Clearer w/o f-string.
431 return "<%s>" % ' '.join(result).strip()
432 #@+node:ekr.20120129040823.10226: *4* bi.isModeBinding
433 def isModeBinding(self) -> bool:
434 return self.kind.startswith('*mode')
435 #@-others
437def isBindingInfo(obj: Any) -> bool:
438 return isinstance(obj, BindingInfo)
439#@+node:ekr.20031218072017.3098: *3* class g.Bunch (Python Cookbook)
440class Bunch:
441 """
442 From The Python Cookbook:
444 Create a Bunch whenever you want to group a few variables:
446 point = Bunch(datum=y, squared=y*y, coord=x)
448 You can read/write the named attributes you just created, add others,
449 del some of them, etc::
451 if point.squared > threshold:
452 point.isok = True
453 """
455 def __init__(self, **keywords: Any) -> None:
456 self.__dict__.update(keywords)
458 def __repr__(self) -> str:
459 return self.toString()
461 def ivars(self) -> List:
462 return sorted(self.__dict__)
464 def keys(self) -> List:
465 return sorted(self.__dict__)
467 def toString(self) -> str:
468 tag = self.__dict__.get('tag')
469 entries = [
470 f"{key}: {str(self.__dict__.get(key)) or repr(self.__dict__.get(key))}"
471 for key in self.ivars() if key != 'tag'
472 ]
473 # Fail.
474 result = [f'g.Bunch({tag or ""})']
475 result.extend(entries)
476 return '\n '.join(result) + '\n'
478 # Used by new undo code.
480 def __setitem__(self, key: str, value: Any) -> Any:
481 """Support aBunch[key] = val"""
482 return operator.setitem(self.__dict__, key, value)
484 def __getitem__(self, key: str) -> Any:
485 """Support aBunch[key]"""
486 # g.pr('g.Bunch.__getitem__', key)
487 return operator.getitem(self.__dict__, key)
489 def get(self, key: str, theDefault: Any=None) -> Any:
490 return self.__dict__.get(key, theDefault)
492 def __contains__(self, key: str) -> bool: # New.
493 # g.pr('g.Bunch.__contains__', key in self.__dict__, key)
494 return key in self.__dict__
496bunch = Bunch
497#@+node:ekr.20120219154958.10492: *3* class g.EmergencyDialog
498class EmergencyDialog:
499 """
500 A class that creates an tkinter dialog with a single OK button.
502 If tkinter doesn't exist (#2512), this class just prints the message
503 passed to the ctor.
505 """
506 #@+others
507 #@+node:ekr.20120219154958.10493: *4* emergencyDialog.__init__
508 def __init__(self, title: str, message: str) -> None:
509 """Constructor for the leoTkinterDialog class."""
510 self.answer = None # Value returned from run()
511 self.title = title
512 self.message = message
513 self.buttonsFrame = None # Frame to hold typical dialog buttons.
514 # Command to call when user click's the window's close box.
515 self.defaultButtonCommand = None
516 self.frame = None # The outermost frame.
517 self.root = None # Created in createTopFrame.
518 self.top = None # The toplevel Tk widget.
519 if Tk: # #2512.
520 self.createTopFrame()
521 buttons = [{
522 "text": "OK",
523 "command": self.okButton,
524 "default": True,
525 }]
526 self.createButtons(buttons)
527 self.top.bind("<Key>", self.onKey)
528 else:
529 print(message.rstrip() + '\n')
530 #@+node:ekr.20120219154958.10494: *4* emergencyDialog.createButtons
531 def createButtons(self, buttons: List[Dict[str, Any]]) -> List[Any]:
532 """Create a row of buttons.
534 buttons is a list of dictionaries containing
535 the properties of each button.
536 """
537 assert self.frame
538 self.buttonsFrame = f = Tk.Frame(self.top)
539 f.pack(side="top", padx=30)
540 # Buttons is a list of dictionaries, with an empty dictionary
541 # at the end if there is only one entry.
542 buttonList = []
543 for d in buttons:
544 text = d.get("text", "<missing button name>")
545 isDefault = d.get("default", False)
546 underline = d.get("underline", 0)
547 command = d.get("command", None)
548 bd = 4 if isDefault else 2
549 b = Tk.Button(f, width=6, text=text, bd=bd,
550 underline=underline, command=command)
551 b.pack(side="left", padx=5, pady=10)
552 buttonList.append(b)
553 if isDefault and command:
554 self.defaultButtonCommand = command
555 return buttonList
556 #@+node:ekr.20120219154958.10495: *4* emergencyDialog.createTopFrame
557 def createTopFrame(self) -> None:
558 """Create the Tk.Toplevel widget for a leoTkinterDialog."""
559 self.root = Tk.Tk() # type:ignore
560 self.top = Tk.Toplevel(self.root) # type:ignore
561 self.top.title(self.title)
562 self.root.withdraw() # This root window should *never* be shown.
563 self.frame = Tk.Frame(self.top) # type:ignore
564 self.frame.pack(side="top", expand=1, fill="both")
565 label = Tk.Label(self.frame, text=self.message, bg='white')
566 label.pack(pady=10)
567 #@+node:ekr.20120219154958.10496: *4* emergencyDialog.okButton
568 def okButton(self) -> None:
569 """Do default click action in ok button."""
570 self.top.destroy()
571 self.top = None
572 #@+node:ekr.20120219154958.10497: *4* emergencyDialog.onKey
573 def onKey(self, event: Any) -> None:
574 """Handle Key events in askOk dialogs."""
575 self.okButton()
576 #@+node:ekr.20120219154958.10498: *4* emergencyDialog.run
577 def run(self) -> None:
578 """Run the modal emergency dialog."""
579 # Suppress f-stringify.
580 self.top.geometry("%dx%d%+d%+d" % (300, 200, 50, 50))
581 self.top.lift()
582 self.top.grab_set() # Make the dialog a modal dialog.
583 self.root.wait_window(self.top)
584 #@-others
585#@+node:ekr.20120123143207.10223: *3* class g.GeneralSetting
586# Important: The startup code uses this class,
587# so it is convenient to define it in leoGlobals.py.
590class GeneralSetting:
591 """A class representing any kind of setting except shortcuts."""
593 def __init__(
594 self,
595 kind: str,
596 encoding: str=None,
597 ivar: str=None,
598 setting: str=None,
599 val: Any=None,
600 path: str=None,
601 tag: str='setting',
602 unl: str=None,
603 ) -> None:
604 self.encoding = encoding
605 self.ivar = ivar
606 self.kind = kind
607 self.path = path
608 self.unl = unl
609 self.setting = setting
610 self.val = val
611 self.tag = tag
613 def __repr__(self) -> str:
614 # Better for g.printObj.
615 val = str(self.val).replace('\n', ' ')
616 return (
617 f"GS: {g.shortFileName(self.path):20} "
618 f"{self.kind:7} = {g.truncate(val, 50)}")
620 dump = __repr__
621 __str__ = __repr__
622#@+node:ekr.20120201164453.10090: *3* class g.KeyStroke & isStroke/OrNone
623class KeyStroke:
624 """
625 A class that represent any key stroke or binding.
627 stroke.s is the "canonicalized" stroke.
628 """
629 #@+others
630 #@+node:ekr.20180414195401.2: *4* ks.__init__
631 def __init__(self, binding: str) -> None:
633 if binding:
634 self.s = self.finalize_binding(binding)
635 else:
636 self.s = None # type:ignore
637 #@+node:ekr.20120203053243.10117: *4* ks.__eq__, etc
638 #@+at All these must be defined in order to say, for example:
639 # for key in sorted(d)
640 # where the keys of d are KeyStroke objects.
641 #@@c
643 def __eq__(self, other: Any) -> bool:
644 if not other:
645 return False
646 if hasattr(other, 's'):
647 return self.s == other.s
648 return self.s == other
650 def __lt__(self, other: Any) -> bool:
651 if not other:
652 return False
653 if hasattr(other, 's'):
654 return self.s < other.s
655 return self.s < other
657 def __le__(self, other: Any) -> bool:
658 return self.__lt__(other) or self.__eq__(other)
660 def __ne__(self, other: Any) -> bool:
661 return not self.__eq__(other)
663 def __gt__(self, other: Any) -> bool:
664 return not self.__lt__(other) and not self.__eq__(other)
666 def __ge__(self, other: Any) -> bool:
667 return not self.__lt__(other)
668 #@+node:ekr.20120203053243.10118: *4* ks.__hash__
669 # Allow KeyStroke objects to be keys in dictionaries.
671 def __hash__(self) -> Any:
672 return self.s.__hash__() if self.s else 0
673 #@+node:ekr.20120204061120.10067: *4* ks.__repr___ & __str__
674 def __repr__(self) -> str:
675 return f"<KeyStroke: {repr(self.s)}>"
677 def __str__(self) -> str:
678 return repr(self.s)
679 #@+node:ekr.20180417160703.1: *4* ks.dump
680 def dump(self) -> None:
681 """Show results of printable chars."""
682 for i in range(128):
683 s = chr(i)
684 stroke = g.KeyStroke(s)
685 if stroke.s != s:
686 print(f"{i:2} {s!r:10} {stroke.s!r}")
687 for ch in ('backspace', 'linefeed', 'return', 'tab'):
688 stroke = g.KeyStroke(ch)
689 print(f'{"":2} {ch!r:10} {stroke.s!r}')
690 #@+node:ekr.20180415082249.1: *4* ks.finalize_binding
691 def finalize_binding(self, binding: str) -> str:
693 # This trace is good for devs only.
694 trace = False and 'keys' in g.app.debug
695 self.mods = self.find_mods(binding)
696 s = self.strip_mods(binding)
697 s = self.finalize_char(s) # May change self.mods.
698 mods = ''.join([f"{z.capitalize()}+" for z in self.mods])
699 if trace and 'meta' in self.mods:
700 g.trace(f"{binding:20}:{self.mods:>20} ==> {mods+s}")
701 return mods + s
702 #@+node:ekr.20180415083926.1: *4* ks.finalize_char & helper
703 def finalize_char(self, s: str) -> str:
704 """Perform very-last-minute translations on bindings."""
705 #
706 # Retain "bigger" spelling for gang-of-four bindings with modifiers.
707 shift_d = {
708 'bksp': 'BackSpace',
709 'backspace': 'BackSpace',
710 'backtab': 'Tab', # The shift mod will convert to 'Shift+Tab',
711 'linefeed': 'Return',
712 '\r': 'Return',
713 'return': 'Return',
714 'tab': 'Tab',
715 }
716 if self.mods and s.lower() in shift_d:
717 # Returning '' breaks existing code.
718 return shift_d.get(s.lower()) # type:ignore
719 #
720 # Make all other translations...
721 #
722 # This dict ensures proper capitalization.
723 # It also translates legacy Tk binding names to ascii chars.
724 translate_d = {
725 #
726 # The gang of four...
727 'bksp': 'BackSpace',
728 'backspace': 'BackSpace',
729 'backtab': 'Tab', # The shift mod will convert to 'Shift+Tab',
730 'linefeed': '\n',
731 '\r': '\n',
732 'return': '\n',
733 'tab': 'Tab',
734 #
735 # Special chars...
736 'delete': 'Delete',
737 'down': 'Down',
738 'end': 'End',
739 'enter': 'Enter',
740 'escape': 'Escape',
741 'home': 'Home',
742 'insert': 'Insert',
743 'left': 'Left',
744 'next': 'Next',
745 'prior': 'Prior',
746 'right': 'Right',
747 'up': 'Up',
748 #
749 # Qt key names...
750 'del': 'Delete',
751 'dnarrow': 'Down',
752 'esc': 'Escape',
753 'ins': 'Insert',
754 'ltarrow': 'Left',
755 'pagedn': 'Next',
756 'pageup': 'Prior',
757 'pgdown': 'Next',
758 'pgup': 'Prior',
759 'rtarrow': 'Right',
760 'uparrow': 'Up',
761 #
762 # Legacy Tk binding names...
763 "ampersand": "&",
764 "asciicircum": "^",
765 "asciitilde": "~",
766 "asterisk": "*",
767 "at": "@",
768 "backslash": "\\",
769 "bar": "|",
770 "braceleft": "{",
771 "braceright": "}",
772 "bracketleft": "[",
773 "bracketright": "]",
774 "colon": ":",
775 "comma": ",",
776 "dollar": "$",
777 "equal": "=",
778 "exclam": "!",
779 "greater": ">",
780 "less": "<",
781 "minus": "-",
782 "numbersign": "#",
783 "quotedbl": '"',
784 "quoteright": "'",
785 "parenleft": "(",
786 "parenright": ")",
787 "percent": "%",
788 "period": ".",
789 "plus": "+",
790 "question": "?",
791 "quoteleft": "`",
792 "semicolon": ";",
793 "slash": "/",
794 "space": " ",
795 "underscore": "_",
796 }
797 #
798 # pylint: disable=undefined-loop-variable
799 # Looks like a pylint bug.
800 if s in (None, 'none', 'None'):
801 return 'None'
802 if s.lower() in translate_d:
803 s = translate_d.get(s.lower())
804 return self.strip_shift(s) # type:ignore
805 if len(s) > 1 and s.find(' ') > -1:
806 # #917: not a pure, but should be ignored.
807 return ''
808 if s.isalpha():
809 if len(s) == 1:
810 if 'shift' in self.mods:
811 if len(self.mods) == 1:
812 self.mods.remove('shift')
813 s = s.upper()
814 else:
815 s = s.lower()
816 elif self.mods:
817 s = s.lower()
818 else:
819 # 917: Ignore multi-byte alphas not in the table.
820 s = ''
821 if 0:
822 # Make sure all special chars are in translate_d.
823 if g.app.gui: # It may not exist yet.
824 if s.capitalize() in g.app.gui.specialChars:
825 s = s.capitalize()
826 return s
827 #
828 # Translate shifted keys to their appropriate alternatives.
829 return self.strip_shift(s)
830 #@+node:ekr.20180502104829.1: *5* ks.strip_shift
831 def strip_shift(self, s: str) -> str:
832 """
833 Handle supposedly shifted keys.
835 User settings might specify an already-shifted key, which is not an error.
837 The legacy Tk binding names have already been translated,
838 so we don't have to worry about Shift-ampersand, etc.
839 """
840 #
841 # The second entry in each line handles shifting an already-shifted character.
842 # That's ok in user settings: the Shift modifier is just removed.
843 shift_d = {
844 # Top row of keyboard.
845 "`": "~", "~": "~",
846 "1": "!", "!": "!",
847 "2": "@", "@": "@",
848 "3": "#", "#": "#",
849 "4": "$", "$": "$",
850 "5": "%", "%": "%",
851 "6": "^", "^": "^",
852 "7": "&", "&": "&",
853 "8": "*", "*": "*",
854 "9": "(", "(": "(",
855 "0": ")", ")": ")",
856 "-": "_", "_": "_",
857 "=": "+", "+": "+",
858 # Second row of keyboard.
859 "[": "{", "{": "{",
860 "]": "}", "}": "}",
861 "\\": '|', "|": "|",
862 # Third row of keyboard.
863 ";": ":", ":": ":",
864 "'": '"', '"': '"',
865 # Fourth row of keyboard.
866 ".": "<", "<": "<",
867 ",": ">", ">": ">",
868 "//": "?", "?": "?",
869 }
870 if 'shift' in self.mods and s in shift_d:
871 self.mods.remove('shift')
872 s = shift_d.get(s) # type:ignore
873 return s
874 #@+node:ekr.20120203053243.10124: *4* ks.find, lower & startswith
875 # These may go away later, but for now they make conversion of string strokes easier.
877 def find(self, pattern: str) -> int:
878 return self.s.find(pattern)
880 def lower(self) -> str:
881 return self.s.lower()
883 def startswith(self, s: str) -> bool:
884 return self.s.startswith(s)
885 #@+node:ekr.20180415081209.2: *4* ks.find_mods
886 def find_mods(self, s: str) -> List[str]:
887 """Return the list of all modifiers seen in s."""
888 s = s.lower()
889 table = (
890 ['alt',],
891 ['command', 'cmd',],
892 ['ctrl', 'control',], # Use ctrl, not control.
893 ['meta',],
894 ['shift', 'shft',],
895 # 868: Allow alternative spellings.
896 ['keypad', 'key_pad', 'numpad', 'num_pad'],
897 )
898 result = []
899 for aList in table:
900 kind = aList[0]
901 for mod in aList:
902 for suffix in '+-':
903 if s.find(mod + suffix) > -1:
904 s = s.replace(mod + suffix, '')
905 result.append(kind)
906 break
907 return result
908 #@+node:ekr.20180417101435.1: *4* ks.isAltCtl
909 def isAltCtrl(self) -> bool:
910 """Return True if this is an Alt-Ctrl character."""
911 mods = self.find_mods(self.s)
912 return 'alt' in mods and 'ctrl' in mods
913 #@+node:ekr.20120203053243.10121: *4* ks.isFKey
914 def isFKey(self) -> bool:
915 return self.s in g.app.gui.FKeys
916 #@+node:ekr.20180417102341.1: *4* ks.isPlainKey (does not handle alt-ctrl chars)
917 def isPlainKey(self) -> bool:
918 """
919 Return True if self.s represents a plain key.
921 A plain key is a key that can be inserted into text.
923 **Note**: The caller is responsible for handling Alt-Ctrl keys.
924 """
925 s = self.s
926 if s in g.app.gui.ignoreChars:
927 # For unit tests.
928 return False
929 # #868:
930 if s.find('Keypad+') > -1:
931 # Enable bindings.
932 return False
933 if self.find_mods(s) or self.isFKey():
934 return False
935 if s in g.app.gui.specialChars:
936 return False
937 if s == 'BackSpace':
938 return False
939 return True
940 #@+node:ekr.20180511092713.1: *4* ks.isNumPadKey, ks.isPlainNumPad & ks.removeNumPadModifier
941 def isNumPadKey(self) -> bool:
942 return self.s.find('Keypad+') > -1
944 def isPlainNumPad(self) -> bool:
945 return (
946 self.isNumPadKey() and
947 len(self.s.replace('Keypad+', '')) == 1
948 )
950 def removeNumPadModifier(self) -> None:
951 self.s = self.s.replace('Keypad+', '')
952 #@+node:ekr.20180419170934.1: *4* ks.prettyPrint
953 def prettyPrint(self) -> str:
955 s = self.s
956 if not s:
957 return '<None>'
958 d = {' ': 'Space', '\t': 'Tab', '\n': 'Return', '\r': 'LineFeed'}
959 ch = s[-1]
960 return s[:-1] + d.get(ch, ch)
961 #@+node:ekr.20180415124853.1: *4* ks.strip_mods
962 def strip_mods(self, s: str) -> str:
963 """Remove all modifiers from s, without changing the case of s."""
964 table = (
965 'alt',
966 'cmd', 'command',
967 'control', 'ctrl',
968 'keypad', 'key_pad', # 868:
969 'meta',
970 'shift', 'shft',
971 )
972 for mod in table:
973 for suffix in '+-':
974 target = mod + suffix
975 i = s.lower().find(target)
976 if i > -1:
977 s = s[:i] + s[i + len(target) :]
978 break
979 return s
980 #@+node:ekr.20120203053243.10125: *4* ks.toGuiChar
981 def toGuiChar(self) -> str:
982 """Replace special chars by the actual gui char."""
983 s = self.s.lower()
984 if s in ('\n', 'return'):
985 s = '\n'
986 elif s in ('\t', 'tab'):
987 s = '\t'
988 elif s in ('\b', 'backspace'):
989 s = '\b'
990 elif s in ('.', 'period'):
991 s = '.'
992 return s
993 #@+node:ekr.20180417100834.1: *4* ks.toInsertableChar
994 def toInsertableChar(self) -> str:
995 """Convert self to an (insertable) char."""
996 # pylint: disable=len-as-condition
997 s = self.s
998 if not s or self.find_mods(s):
999 return ''
1000 # Handle the "Gang of Four"
1001 d = {
1002 'BackSpace': '\b',
1003 'LineFeed': '\n',
1004 # 'Insert': '\n',
1005 'Return': '\n',
1006 'Tab': '\t',
1007 }
1008 if s in d:
1009 return d.get(s) # type:ignore
1010 return s if len(s) == 1 else ''
1011 #@-others
1013def isStroke(obj: Any) -> bool:
1014 return isinstance(obj, KeyStroke)
1016def isStrokeOrNone(obj: Any) -> bool:
1017 return obj is None or isinstance(obj, KeyStroke)
1018#@+node:ekr.20160119093947.1: *3* class g.MatchBrackets
1019class MatchBrackets:
1020 """
1021 A class implementing the match-brackets command.
1023 In the interest of speed, the code assumes that the user invokes the
1024 match-bracket command ouside of any string, comment or (for perl or
1025 javascript) regex.
1026 """
1027 #@+others
1028 #@+node:ekr.20160119104510.1: *4* mb.ctor
1029 def __init__(self, c: Cmdr, p: Pos, language: str) -> None:
1030 """Ctor for MatchBrackets class."""
1031 self.c = c
1032 self.p = p.copy()
1033 self.language = language
1034 # Constants.
1035 self.close_brackets = ")]}>"
1036 self.open_brackets = "([{<"
1037 self.brackets = self.open_brackets + self.close_brackets
1038 self.matching_brackets = self.close_brackets + self.open_brackets
1039 # Language dependent.
1040 d1, d2, d3 = g.set_delims_from_language(language)
1041 self.single_comment, self.start_comment, self.end_comment = d1, d2, d3
1042 # to track expanding selection
1043 c.user_dict.setdefault('_match_brackets', {'count': 0, 'range': (0, 0)})
1044 #@+node:ekr.20160121164723.1: *4* mb.bi-directional helpers
1045 #@+node:ekr.20160121112812.1: *5* mb.is_regex
1046 def is_regex(self, s: str, i: int) -> bool:
1047 """Return true if there is another slash on the line."""
1048 if self.language in ('javascript', 'perl',):
1049 assert s[i] == '/'
1050 offset = 1 if self.forward else -1
1051 i += offset
1052 while 0 <= i < len(s) and s[i] != '\n':
1053 if s[i] == '/':
1054 return True
1055 i += offset
1056 return False
1057 return False
1058 #@+node:ekr.20160121112536.1: *5* mb.scan_regex
1059 def scan_regex(self, s: str, i: int) -> int:
1060 """Scan a regex (or regex substitution for perl)."""
1061 assert s[i] == '/'
1062 offset = 1 if self.forward else -1
1063 i1 = i
1064 i += offset
1065 found: Union[int, bool] = False
1066 while 0 <= i < len(s) and s[i] != '\n':
1067 ch = s[i]
1068 i2 = i - 1 # in case we have to look behind.
1069 i += offset
1070 if ch == '/':
1071 # Count the preceding backslashes.
1072 n = 0
1073 while 0 <= i2 < len(s) and s[i2] == '\\':
1074 n += 1
1075 i2 -= 1
1076 if (n % 2) == 0:
1077 if self.language == 'perl' and found is None:
1078 found = i
1079 else:
1080 found = i
1081 break
1082 if found is None:
1083 self.oops('unmatched regex delim')
1084 return i1 + offset
1085 return found
1086 #@+node:ekr.20160121112303.1: *5* mb.scan_string
1087 def scan_string(self, s: str, i: int) -> int:
1088 """
1089 Scan the string starting at s[i] (forward or backward).
1090 Return the index of the next character.
1091 """
1092 # i1 = i if self.forward else i + 1
1093 delim = s[i]
1094 assert delim in "'\"", repr(delim)
1095 offset = 1 if self.forward else -1
1096 i += offset
1097 while 0 <= i < len(s):
1098 ch = s[i]
1099 i2 = i - 1 # in case we have to look behind.
1100 i += offset
1101 if ch == delim:
1102 # Count the preceding backslashes.
1103 n = 0
1104 while 0 <= i2 < len(s) and s[i2] == '\\':
1105 n += 1
1106 i2 -= 1
1107 if (n % 2) == 0:
1108 return i
1109 # Annoying when matching brackets on the fly.
1110 # self.oops('unmatched string')
1111 return i + offset
1112 #@+node:tbrown.20180226113621.1: *4* mb.expand_range
1113 def expand_range(
1114 self,
1115 s: str,
1116 left: int,
1117 right: int,
1118 max_right: int,
1119 expand: bool=False,
1120 ) -> Tuple[Any, Any, Any, Any]:
1121 """
1122 Find the bracket nearest the cursor searching outwards left and right.
1124 Expand the range (left, right) in string s until either s[left] or
1125 s[right] is a bracket. right can not exceed max_right, and if expand is
1126 True, the new range must encompass the old range, in addition to s[left]
1127 or s[right] being a bracket.
1129 Returns
1130 new_left, new_right, bracket_char, index_of_bracket_char
1131 if expansion succeeds, otherwise
1132 None, None, None, None
1134 Note that only one of new_left and new_right will necessarily be a
1135 bracket, but index_of_bracket_char will definitely be a bracket.
1136 """
1137 expanded: Union[bool, str] = False
1138 left = max(0, min(left, len(s))) # #2240
1139 right = max(0, min(right, len(s))) # #2240
1140 orig_left = left
1141 orig_right = right
1142 while (
1143 (s[left] not in self.brackets or expand and not expanded)
1144 and (s[right] not in self.brackets or expand and not expanded)
1145 and (left > 0 or right < max_right)
1146 ):
1147 expanded = False
1148 if left > 0:
1149 left -= 1
1150 if s[left] in self.brackets:
1151 other = self.find_matching_bracket(s[left], s, left)
1152 if other is not None and other >= orig_right:
1153 expanded = 'left'
1154 if right < max_right:
1155 right += 1
1156 if s[right] in self.brackets:
1157 other = self.find_matching_bracket(s[right], s, right)
1158 if other is not None and other <= orig_left:
1159 expanded = 'right'
1160 if s[left] in self.brackets and (not expand or expanded == 'left'):
1161 return left, right, s[left], left
1162 if s[right] in self.brackets and (not expand or expanded == 'right'):
1163 return left, right, s[right], right
1164 return None, None, None, None
1165 #@+node:ekr.20061113221414: *4* mb.find_matching_bracket
1166 def find_matching_bracket(self, ch1: str, s: str, i: int) -> Any:
1167 """Find the bracket matching s[i] for self.language."""
1168 self.forward = ch1 in self.open_brackets
1169 # Find the character matching the initial bracket.
1170 for n in range(len(self.brackets)): # pylint: disable=consider-using-enumerate
1171 if ch1 == self.brackets[n]:
1172 target = self.matching_brackets[n]
1173 break
1174 else:
1175 return None
1176 f = self.scan if self.forward else self.scan_back
1177 return f(ch1, target, s, i)
1178 #@+node:ekr.20160121164556.1: *4* mb.scan & helpers
1179 def scan(self, ch1: str, target: str, s: str, i: int) -> Optional[int]:
1180 """Scan forward for target."""
1181 level = 0
1182 while 0 <= i < len(s):
1183 progress = i
1184 ch = s[i]
1185 if ch in '"\'':
1186 # Scan to the end/beginning of the string.
1187 i = self.scan_string(s, i)
1188 elif self.starts_comment(s, i):
1189 i = self.scan_comment(s, i) # type:ignore
1190 elif ch == '/' and self.is_regex(s, i):
1191 i = self.scan_regex(s, i)
1192 elif ch == ch1:
1193 level += 1
1194 i += 1
1195 elif ch == target:
1196 level -= 1
1197 if level <= 0:
1198 return i
1199 i += 1
1200 else:
1201 i += 1
1202 assert i > progress
1203 # Not found
1204 return None
1205 #@+node:ekr.20160119090634.1: *5* mb.scan_comment
1206 def scan_comment(self, s: str, i: int) -> Optional[int]:
1207 """Return the index of the character after a comment."""
1208 i1 = i
1209 start = self.start_comment if self.forward else self.end_comment
1210 end = self.end_comment if self.forward else self.start_comment
1211 offset = 1 if self.forward else -1
1212 if g.match(s, i, start):
1213 if not self.forward:
1214 i1 += len(end)
1215 i += offset
1216 while 0 <= i < len(s):
1217 if g.match(s, i, end):
1218 i = i + len(end) if self.forward else i - 1
1219 return i
1220 i += offset
1221 self.oops('unmatched multiline comment')
1222 elif self.forward:
1223 # Scan to the newline.
1224 target = '\n'
1225 while 0 <= i < len(s):
1226 if s[i] == '\n':
1227 i += 1
1228 return i
1229 i += 1
1230 else:
1231 # Careful: scan to the *first* target on the line
1232 target = self.single_comment
1233 found = None
1234 i -= 1
1235 while 0 <= i < len(s) and s[i] != '\n':
1236 if g.match(s, i, target):
1237 found = i
1238 i -= 1
1239 if found is None:
1240 self.oops('can not happen: unterminated single-line comment')
1241 found = 0
1242 return found
1243 return i
1244 #@+node:ekr.20160119101851.1: *5* mb.starts_comment
1245 def starts_comment(self, s: str, i: int) -> bool:
1246 """Return True if s[i] starts a comment."""
1247 assert 0 <= i < len(s)
1248 if self.forward:
1249 if self.single_comment and g.match(s, i, self.single_comment):
1250 return True
1251 return (
1252 self.start_comment and self.end_comment and
1253 g.match(s, i, self.start_comment)
1254 )
1255 if s[i] == '\n':
1256 if self.single_comment:
1257 # Scan backward for any single-comment delim.
1258 i -= 1
1259 while i >= 0 and s[i] != '\n':
1260 if g.match(s, i, self.single_comment):
1261 return True
1262 i -= 1
1263 return False
1264 return (
1265 self.start_comment and self.end_comment and
1266 g.match(s, i, self.end_comment)
1267 )
1268 #@+node:ekr.20160119230141.1: *4* mb.scan_back & helpers
1269 def scan_back(self, ch1: str, target: str, s: str, i: int) -> Optional[int]:
1270 """Scan backwards for delim."""
1271 level = 0
1272 while i >= 0:
1273 progress = i
1274 ch = s[i]
1275 if self.ends_comment(s, i):
1276 i = self.back_scan_comment(s, i)
1277 elif ch in '"\'':
1278 # Scan to the beginning of the string.
1279 i = self.scan_string(s, i)
1280 elif ch == '/' and self.is_regex(s, i):
1281 i = self.scan_regex(s, i)
1282 elif ch == ch1:
1283 level += 1
1284 i -= 1
1285 elif ch == target:
1286 level -= 1
1287 if level <= 0:
1288 return i
1289 i -= 1
1290 else:
1291 i -= 1
1292 assert i < progress
1293 # Not found
1294 return None
1295 #@+node:ekr.20160119230141.2: *5* mb.back_scan_comment
1296 def back_scan_comment(self, s: str, i: int) -> int:
1297 """Return the index of the character after a comment."""
1298 i1 = i
1299 if g.match(s, i, self.end_comment):
1300 i1 += len(self.end_comment) # For traces.
1301 i -= 1
1302 while i >= 0:
1303 if g.match(s, i, self.start_comment):
1304 i -= 1
1305 return i
1306 i -= 1
1307 self.oops('unmatched multiline comment')
1308 return i
1309 # Careful: scan to the *first* target on the line
1310 found = None
1311 i -= 1
1312 while i >= 0 and s[i] != '\n':
1313 if g.match(s, i, self.single_comment):
1314 found = i - 1
1315 i -= 1
1316 if found is None:
1317 self.oops('can not happen: unterminated single-line comment')
1318 found = 0
1319 return found
1320 #@+node:ekr.20160119230141.4: *5* mb.ends_comment
1321 def ends_comment(self, s: str, i: int) -> bool:
1322 """
1323 Return True if s[i] ends a comment. This is called while scanning
1324 backward, so this is a bit of a guess.
1325 """
1326 if s[i] == '\n':
1327 # This is the hard (dubious) case.
1328 # Let w, x, y and z stand for any strings not containg // or quotes.
1329 # Case 1: w"x//y"z Assume // is inside a string.
1330 # Case 2: x//y"z Assume " is inside the comment.
1331 # Case 3: w//x"y"z Assume both quotes are inside the comment.
1332 #
1333 # That is, we assume (perhaps wrongly) that a quote terminates a
1334 # string if and *only* if the string starts *and* ends on the line.
1335 if self.single_comment:
1336 # Scan backward for single-line comment delims or quotes.
1337 quote = None
1338 i -= 1
1339 while i >= 0 and s[i] != '\n':
1340 progress = i
1341 if quote and s[i] == quote:
1342 quote = None
1343 i -= 1
1344 elif s[i] in '"\'':
1345 if not quote:
1346 quote = s[i]
1347 i -= 1
1348 elif g.match(s, i, self.single_comment):
1349 # Assume that there is a comment only if the comment delim
1350 # isn't inside a string that begins and ends on *this* line.
1351 if quote:
1352 while i >= 0 and s[i] != 'n':
1353 if s[i] == quote:
1354 return False
1355 i -= 1
1356 return True
1357 else:
1358 i -= 1
1359 assert progress > i
1360 return False
1361 return (
1362 self.start_comment and
1363 self.end_comment and
1364 g.match(s, i, self.end_comment))
1365 #@+node:ekr.20160119104148.1: *4* mb.oops
1366 def oops(self, s: str) -> None:
1367 """Report an error in the match-brackets command."""
1368 g.es(s, color='red')
1369 #@+node:ekr.20160119094053.1: *4* mb.run
1370 #@@nobeautify
1372 def run(self) -> None:
1373 """The driver for the MatchBrackets class.
1375 With no selected range: find the nearest bracket and select from
1376 it to it's match, moving cursor to match.
1378 With selected range: the first time, move cursor back to other end of
1379 range. The second time, select enclosing range.
1380 """
1381 #
1382 # A partial fix for bug 127: Bracket matching is buggy.
1383 w = self.c.frame.body.wrapper
1384 s = w.getAllText()
1385 _mb = self.c.user_dict['_match_brackets']
1386 sel_range = w.getSelectionRange()
1387 if not w.hasSelection():
1388 _mb['count'] = 1
1389 if _mb['range'] == sel_range and _mb['count'] == 1:
1390 # haven't been to other end yet
1391 _mb['count'] += 1
1392 # move insert point to other end of selection
1393 insert = 1 if w.getInsertPoint() == sel_range[0] else 0
1394 w.setSelectionRange(
1395 sel_range[0], sel_range[1], insert=sel_range[insert])
1396 return
1398 # Find the bracket nearest the cursor.
1399 max_right = len(s) - 1 # insert point can be past last char.
1400 left = right = min(max_right, w.getInsertPoint())
1401 left, right, ch, index = self.expand_range(s, left, right, max_right)
1402 if left is None:
1403 g.es("Bracket not found")
1404 return
1405 index2 = self.find_matching_bracket(ch, s, index)
1406 if index2 is None:
1407 g.es("No matching bracket.") # #1447.
1408 return
1410 # If this is the first time we've selected the range index-index2, do
1411 # nothing extra. The second time, move cursor to other end (requires
1412 # no special action here), and the third time, try to expand the range
1413 # to any enclosing brackets
1414 minmax = (min(index, index2), max(index, index2)+1)
1415 # the range, +1 to match w.getSelectionRange()
1416 if _mb['range'] == minmax: # count how many times this has been the answer
1417 _mb['count'] += 1
1418 else:
1419 _mb['count'] = 1
1420 _mb['range'] = minmax
1421 if _mb['count'] >= 3: # try to expand range
1422 left, right, ch, index3 = self.expand_range(
1423 s,
1424 max(minmax[0], 0),
1425 min(minmax[1], max_right),
1426 max_right, expand=True
1427 )
1428 if index3 is not None: # found nearest bracket outside range
1429 index4 = self.find_matching_bracket(ch, s, index3)
1430 if index4 is not None: # found matching bracket, expand range
1431 index, index2 = index3, index4
1432 _mb['count'] = 1
1433 _mb['range'] = (min(index3, index4), max(index3, index4)+1)
1435 if index2 is not None:
1436 if index2 < index:
1437 w.setSelectionRange(index2, index + 1, insert=index2)
1438 else:
1439 w.setSelectionRange(
1440 index, index2 + 1, insert=min(len(s), index2 + 1))
1441 w.see(index2)
1442 else:
1443 g.es("unmatched", repr(ch))
1444 #@-others
1445#@+node:ekr.20090128083459.82: *3* class g.PosList (deprecated)
1446class PosList(list):
1447 #@+<< docstring for PosList >>
1448 #@+node:ekr.20090130114732.2: *4* << docstring for PosList >>
1449 """A subclass of list for creating and selecting lists of positions.
1451 This is deprecated, use leoNodes.PosList instead!
1453 # Creates a PosList containing all positions in c.
1454 aList = g.PosList(c)
1456 # Creates a PosList from aList2.
1457 aList = g.PosList(c,aList2)
1459 # Creates a PosList containing all positions p in aList
1460 # such that p.h matches the pattern.
1461 # The pattern is a regular expression if regex is True.
1462 # if removeClones is True, all positions p2 are removed
1463 # if a position p is already in the list and p2.v == p.v.
1464 aList2 = aList.select(pattern,regex=False,removeClones=True)
1466 # Prints all positions in aList, sorted if sort is True.
1467 # Prints p.h, or repr(p) if verbose is True.
1468 aList.dump(sort=False,verbose=False)
1469 """
1470 #@-<< docstring for PosList >>
1471 #@+others
1472 #@+node:ekr.20140531104908.17611: *4* PosList.ctor
1473 def __init__(self, c: Cmdr, aList: List[Cmdr]=None) -> None:
1474 self.c = c
1475 super().__init__()
1476 if aList is None:
1477 for p in c.all_positions():
1478 self.append(p.copy())
1479 else:
1480 for p in aList:
1481 self.append(p.copy())
1482 #@+node:ekr.20140531104908.17612: *4* PosList.dump
1483 def dump(self, sort: bool=False, verbose: bool=False) -> str:
1484 if verbose:
1485 return g.listToString(self, sort=sort)
1486 return g.listToString([p.h for p in self], sort=sort)
1487 #@+node:ekr.20140531104908.17613: *4* PosList.select
1488 def select(self, pat: str, regex: bool=False, removeClones: bool=True) -> "PosList":
1489 """
1490 Return a new PosList containing all positions
1491 in self that match the given pattern.
1492 """
1493 c = self.c
1495 aList = []
1496 if regex:
1497 for p in self:
1498 if re.match(pat, p.h):
1499 aList.append(p.copy())
1500 else:
1501 for p in self:
1502 if p.h.find(pat) != -1:
1503 aList.append(p.copy())
1504 if removeClones:
1505 aList = self.removeClones(aList)
1506 return PosList(c, aList)
1507 #@+node:ekr.20140531104908.17614: *4* PosList.removeClones
1508 def removeClones(self, aList: List[Pos]) -> List[Pos]:
1509 seen = {}
1510 aList2: List[Pos] = []
1511 for p in aList:
1512 if p.v not in seen:
1513 seen[p.v] = p.v
1514 aList2.append(p)
1515 return aList2
1516 #@-others
1517#@+node:EKR.20040612114220.4: *3* class g.ReadLinesClass
1518class ReadLinesClass:
1519 """A class whose next method provides a readline method for Python's tokenize module."""
1521 def __init__(self, s: str) -> None:
1522 self.lines = g.splitLines(s)
1523 self.i = 0
1525 def next(self) -> str:
1526 if self.i < len(self.lines):
1527 line = self.lines[self.i]
1528 self.i += 1
1529 else:
1530 line = ''
1531 return line
1533 __next__ = next
1534#@+node:ekr.20031218072017.3121: *3* class g.RedirectClass & convenience functions
1535class RedirectClass:
1536 """A class to redirect stdout and stderr to Leo's log pane."""
1537 #@+<< RedirectClass methods >>
1538 #@+node:ekr.20031218072017.1656: *4* << RedirectClass methods >>
1539 #@+others
1540 #@+node:ekr.20041012082437: *5* RedirectClass.__init__
1541 def __init__(self) -> None:
1542 self.old = None
1543 self.encoding = 'utf-8' # 2019/03/29 For pdb.
1544 #@+node:ekr.20041012082437.1: *5* isRedirected
1545 def isRedirected(self) -> bool:
1546 return self.old is not None
1547 #@+node:ekr.20041012082437.2: *5* flush
1548 # For LeoN: just for compatibility.
1550 def flush(self, *args: Any) -> None:
1551 return
1552 #@+node:ekr.20041012091252: *5* rawPrint
1553 def rawPrint(self, s: str) -> None:
1554 if self.old:
1555 self.old.write(s + '\n')
1556 else:
1557 g.pr(s)
1558 #@+node:ekr.20041012082437.3: *5* redirect
1559 def redirect(self, stdout: bool=True) -> None:
1560 if g.app.batchMode:
1561 # Redirection is futile in batch mode.
1562 return
1563 if not self.old:
1564 if stdout:
1565 self.old, sys.stdout = sys.stdout, self # type:ignore
1566 else:
1567 self.old, sys.stderr = sys.stderr, self # type:ignore
1568 #@+node:ekr.20041012082437.4: *5* undirect
1569 def undirect(self, stdout: bool=True) -> None:
1570 if self.old:
1571 if stdout:
1572 sys.stdout, self.old = self.old, None
1573 else:
1574 sys.stderr, self.old = self.old, None
1575 #@+node:ekr.20041012082437.5: *5* write
1576 def write(self, s: str) -> None:
1578 if self.old:
1579 if app.log:
1580 app.log.put(s, from_redirect=True)
1581 else:
1582 self.old.write(s + '\n')
1583 else:
1584 # Can happen when g.batchMode is True.
1585 g.pr(s)
1586 #@-others
1587 #@-<< RedirectClass methods >>
1589# Create two redirection objects, one for each stream.
1591redirectStdErrObj = RedirectClass()
1592redirectStdOutObj = RedirectClass()
1593#@+<< define convenience methods for redirecting streams >>
1594#@+node:ekr.20031218072017.3122: *4* << define convenience methods for redirecting streams >>
1595#@+others
1596#@+node:ekr.20041012090942: *5* redirectStderr & redirectStdout
1597# Redirect streams to the current log window.
1599def redirectStderr() -> None:
1600 global redirectStdErrObj
1601 redirectStdErrObj.redirect(stdout=False)
1603def redirectStdout() -> None:
1604 global redirectStdOutObj
1605 redirectStdOutObj.redirect()
1606#@+node:ekr.20041012090942.1: *5* restoreStderr & restoreStdout
1607# Restore standard streams.
1609def restoreStderr() -> None:
1610 global redirectStdErrObj
1611 redirectStdErrObj.undirect(stdout=False)
1613def restoreStdout() -> None:
1614 global redirectStdOutObj
1615 redirectStdOutObj.undirect()
1616#@+node:ekr.20041012090942.2: *5* stdErrIsRedirected & stdOutIsRedirected
1617def stdErrIsRedirected() -> bool:
1618 global redirectStdErrObj
1619 return redirectStdErrObj.isRedirected()
1621def stdOutIsRedirected() -> bool:
1622 global redirectStdOutObj
1623 return redirectStdOutObj.isRedirected()
1624#@+node:ekr.20041012090942.3: *5* rawPrint
1625# Send output to original stdout.
1627def rawPrint(s: str) -> None:
1628 global redirectStdOutObj
1629 redirectStdOutObj.rawPrint(s)
1630#@-others
1631#@-<< define convenience methods for redirecting streams >>
1632#@+node:ekr.20121128031949.12605: *3* class g.SherlockTracer
1633class SherlockTracer:
1634 """
1635 A stand-alone tracer class with many of Sherlock's features.
1637 This class should work in any environment containing the re, os and sys modules.
1639 The arguments in the pattern lists determine which functions get traced
1640 or which stats get printed. Each pattern starts with "+", "-", "+:" or
1641 "-:", followed by a regular expression::
1643 "+x" Enables tracing (or stats) for all functions/methods whose name
1644 matches the regular expression x.
1645 "-x" Disables tracing for functions/methods.
1646 "+:x" Enables tracing for all functions in the **file** whose name matches x.
1647 "-:x" Disables tracing for an entire file.
1649 Enabling and disabling depends on the order of arguments in the pattern
1650 list. Consider the arguments for the Rope trace::
1652 patterns=['+.*','+:.*',
1653 '-:.*\\lib\\.*','+:.*rope.*','-:.*leoGlobals.py',
1654 '-:.*worder.py','-:.*prefs.py','-:.*resources.py',])
1656 This enables tracing for everything, then disables tracing for all
1657 library modules, except for all rope modules. Finally, it disables the
1658 tracing for Rope's worder, prefs and resources modules. Btw, this is
1659 one of the best uses for regular expressions that I know of.
1661 Being able to zero in on the code of interest can be a big help in
1662 studying other people's code. This is a non-invasive method: no tracing
1663 code needs to be inserted anywhere.
1665 Usage:
1667 g.SherlockTracer(patterns).run()
1668 """
1669 #@+others
1670 #@+node:ekr.20121128031949.12602: *4* __init__
1671 def __init__(
1672 self,
1673 patterns: List[Any],
1674 dots: bool=True,
1675 show_args: bool=True,
1676 show_return: bool=True,
1677 verbose: bool=True,
1678 ) -> None:
1679 """SherlockTracer ctor."""
1680 self.bad_patterns: List[str] = [] # List of bad patterns.
1681 self.dots = dots # True: print level dots.
1682 self.contents_d: Dict[str, List] = {} # Keys are file names, values are file lines.
1683 self.n = 0 # The frame level on entry to run.
1684 self.stats: Dict[str, Dict] = {} # Keys are full file names, values are dicts.
1685 self.patterns: List[Any] = None # A list of regex patterns to match.
1686 self.pattern_stack: List[str] = []
1687 self.show_args = show_args # True: show args for each function call.
1688 self.show_return = show_return # True: show returns from each function.
1689 self.trace_lines = True # True: trace lines in enabled functions.
1690 self.verbose = verbose # True: print filename:func
1691 self.set_patterns(patterns)
1692 from leo.core.leoQt import QtCore
1693 if QtCore:
1694 # pylint: disable=no-member
1695 QtCore.pyqtRemoveInputHook()
1696 #@+node:ekr.20140326100337.16844: *4* __call__
1697 def __call__(self, frame: Any, event: Any, arg: Any) -> Any:
1698 """Exists so that self.dispatch can return self."""
1699 return self.dispatch(frame, event, arg)
1700 #@+node:ekr.20140326100337.16846: *4* sherlock.bad_pattern
1701 def bad_pattern(self, pattern: Any) -> None:
1702 """Report a bad Sherlock pattern."""
1703 if pattern not in self.bad_patterns:
1704 self.bad_patterns.append(pattern)
1705 print(f"\nignoring bad pattern: {pattern}\n")
1706 #@+node:ekr.20140326100337.16847: *4* sherlock.check_pattern
1707 def check_pattern(self, pattern: str) -> bool:
1708 """Give an error and return False for an invalid pattern."""
1709 try:
1710 for prefix in ('+:', '-:', '+', '-'):
1711 if pattern.startswith(prefix):
1712 re.match(pattern[len(prefix) :], 'xyzzy')
1713 return True
1714 self.bad_pattern(pattern)
1715 return False
1716 except Exception:
1717 self.bad_pattern(pattern)
1718 return False
1719 #@+node:ekr.20121128031949.12609: *4* sherlock.dispatch
1720 def dispatch(self, frame: Any, event: Any, arg: Any) -> Any:
1721 """The dispatch method."""
1722 if event == 'call':
1723 self.do_call(frame, arg)
1724 elif event == 'return' and self.show_return:
1725 self.do_return(frame, arg)
1726 elif event == 'line' and self.trace_lines:
1727 self.do_line(frame, arg)
1728 # Queue the SherlockTracer instance again.
1729 return self
1730 #@+node:ekr.20121128031949.12603: *4* sherlock.do_call & helper
1731 def do_call(self, frame: Any, unused_arg: Any) -> None:
1732 """Trace through a function call."""
1733 frame1 = frame
1734 code = frame.f_code
1735 file_name = code.co_filename
1736 locals_ = frame.f_locals
1737 function_name = code.co_name
1738 try:
1739 full_name = self.get_full_name(locals_, function_name)
1740 except Exception:
1741 full_name = function_name
1742 if not self.is_enabled(file_name, full_name, self.patterns):
1743 # 2020/09/09: Don't touch, for example, __ methods.
1744 return
1745 n = 0 # The number of callers of this def.
1746 while frame:
1747 frame = frame.f_back
1748 n += 1
1749 dots = '.' * max(0, n - self.n) if self.dots else ''
1750 path = f"{os.path.basename(file_name):>20}" if self.verbose else ''
1751 leadin = '+' if self.show_return else ''
1752 args = "(%s)" % self.get_args(frame1) if self.show_args else ''
1753 print(f"{path}:{dots}{leadin}{full_name}{args}")
1754 # Always update stats.
1755 d = self.stats.get(file_name, {})
1756 d[full_name] = 1 + d.get(full_name, 0)
1757 self.stats[file_name] = d
1758 #@+node:ekr.20130111185820.10194: *5* sherlock.get_args
1759 def get_args(self, frame: Any) -> str:
1760 """Return name=val for each arg in the function call."""
1761 code = frame.f_code
1762 locals_ = frame.f_locals
1763 name = code.co_name
1764 n = code.co_argcount
1765 if code.co_flags & 4:
1766 n = n + 1
1767 if code.co_flags & 8:
1768 n = n + 1
1769 result = []
1770 for i in range(n):
1771 name = code.co_varnames[i]
1772 if name != 'self':
1773 arg = locals_.get(name, '*undefined*')
1774 if arg:
1775 if isinstance(arg, (list, tuple)):
1776 # Clearer w/o f-string
1777 val = "[%s]" % ','.join(
1778 [self.show(z) for z in arg if self.show(z)])
1779 else:
1780 val = self.show(arg)
1781 if val:
1782 result.append(f"{name}={val}")
1783 return ','.join(result)
1784 #@+node:ekr.20140402060647.16845: *4* sherlock.do_line (not used)
1785 bad_fns: List[str] = []
1787 def do_line(self, frame: Any, arg: Any) -> None:
1788 """print each line of enabled functions."""
1789 if 1:
1790 return
1791 code = frame.f_code
1792 file_name = code.co_filename
1793 locals_ = frame.f_locals
1794 name = code.co_name
1795 full_name = self.get_full_name(locals_, name)
1796 if not self.is_enabled(file_name, full_name, self.patterns):
1797 return
1798 n = frame.f_lineno - 1 # Apparently, the first line is line 1.
1799 d = self.contents_d
1800 lines = d.get(file_name)
1801 if not lines:
1802 print(file_name)
1803 try:
1804 with open(file_name) as f:
1805 s = f.read()
1806 except Exception:
1807 if file_name not in self.bad_fns:
1808 self.bad_fns.append(file_name)
1809 print(f"open({file_name}) failed")
1810 return
1811 lines = g.splitLines(s)
1812 d[file_name] = lines
1813 line = lines[n].rstrip() if n < len(lines) else '<EOF>'
1814 if 0:
1815 print(f"{name:3} {line}")
1816 else:
1817 print(f"{g.shortFileName(file_name)} {n} {full_name} {line}")
1818 #@+node:ekr.20130109154743.10172: *4* sherlock.do_return & helper
1819 def do_return(self, frame: Any, arg: Any) -> None: # Arg *is* used below.
1820 """Trace a return statement."""
1821 code = frame.f_code
1822 fn = code.co_filename
1823 locals_ = frame.f_locals
1824 name = code.co_name
1825 full_name = self.get_full_name(locals_, name)
1826 if self.is_enabled(fn, full_name, self.patterns):
1827 n = 0
1828 while frame:
1829 frame = frame.f_back
1830 n += 1
1831 dots = '.' * max(0, n - self.n) if self.dots else ''
1832 path = f"{os.path.basename(fn):>20}" if self.verbose else ''
1833 if name and name == '__init__':
1834 try:
1835 ret1 = locals_ and locals_.get('self', None)
1836 ret = self.format_ret(ret1)
1837 except NameError:
1838 ret = f"<{ret1.__class__.__name__}>"
1839 else:
1840 ret = self.format_ret(arg)
1841 print(f"{path}{dots}-{full_name}{ret}")
1842 #@+node:ekr.20130111120935.10192: *5* sherlock.format_ret
1843 def format_ret(self, arg: Any) -> str:
1844 """Format arg, the value returned by a "return" statement."""
1845 try:
1846 if isinstance(arg, types.GeneratorType):
1847 ret = '<generator>'
1848 elif isinstance(arg, (tuple, list)):
1849 # Clearer w/o f-string.
1850 ret = "[%s]" % ','.join([self.show(z) for z in arg])
1851 if len(ret) > 40:
1852 # Clearer w/o f-string.
1853 ret = "[\n%s]" % ('\n,'.join([self.show(z) for z in arg]))
1854 elif arg:
1855 ret = self.show(arg)
1856 if len(ret) > 40:
1857 ret = f"\n {ret}"
1858 else:
1859 ret = '' if arg is None else repr(arg)
1860 except Exception:
1861 exctype, value = sys.exc_info()[:2]
1862 s = f"<**exception: {exctype.__name__}, {value} arg: {arg !r}**>"
1863 ret = f" ->\n {s}" if len(s) > 40 else f" -> {s}"
1864 return f" -> {ret}"
1865 #@+node:ekr.20121128111829.12185: *4* sherlock.fn_is_enabled (not used)
1866 def fn_is_enabled(self, func: Any, patterns: List[str]) -> bool:
1867 """Return True if tracing for the given function is enabled."""
1868 if func in self.ignored_functions:
1869 return False
1871 def ignore_function() -> None:
1872 if func not in self.ignored_functions:
1873 self.ignored_functions.append(func)
1874 print(f"Ignore function: {func}")
1875 #
1876 # New in Leo 6.3. Never trace dangerous functions.
1877 table = (
1878 '_deepcopy.*',
1879 # Unicode primitives.
1880 'encode\b', 'decode\b',
1881 # System functions
1882 '.*__next\b',
1883 '<frozen>', '<genexpr>', '<listcomp>',
1884 # '<decorator-gen-.*>',
1885 'get\b',
1886 # String primitives.
1887 'append\b', 'split\b', 'join\b',
1888 # File primitives...
1889 'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
1890 'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
1891 )
1892 g.trace('=====', func)
1893 for z in table:
1894 if re.match(z, func):
1895 ignore_function()
1896 return False
1897 #
1898 # Legacy code.
1899 try:
1900 enabled, pattern = False, None
1901 for pattern in patterns:
1902 if pattern.startswith('+:'):
1903 if re.match(pattern[2:], func):
1904 enabled = True
1905 elif pattern.startswith('-:'):
1906 if re.match(pattern[2:], func):
1907 enabled = False
1908 return enabled
1909 except Exception:
1910 self.bad_pattern(pattern)
1911 return False
1912 #@+node:ekr.20130112093655.10195: *4* get_full_name
1913 def get_full_name(self, locals_: Any, name: str) -> str:
1914 """Return class_name::name if possible."""
1915 full_name = name
1916 try:
1917 user_self = locals_ and locals_.get('self', None)
1918 if user_self:
1919 full_name = user_self.__class__.__name__ + '::' + name
1920 except Exception:
1921 pass
1922 return full_name
1923 #@+node:ekr.20121128111829.12183: *4* sherlock.is_enabled
1924 ignored_files: List[str] = [] # List of files.
1925 ignored_functions: List[str] = [] # List of files.
1927 def is_enabled(
1928 self,
1929 file_name: str,
1930 function_name: str,
1931 patterns: List[str]=None,
1932 ) -> bool:
1933 """Return True if tracing for function_name in the given file is enabled."""
1934 #
1935 # New in Leo 6.3. Never trace through some files.
1936 if not os:
1937 return False # Shutting down.
1938 base_name = os.path.basename(file_name)
1939 if base_name in self.ignored_files:
1940 return False
1942 def ignore_file() -> None:
1943 if not base_name in self.ignored_files:
1944 self.ignored_files.append(base_name)
1946 def ignore_function() -> None:
1947 if function_name not in self.ignored_functions:
1948 self.ignored_functions.append(function_name)
1950 if f"{os.sep}lib{os.sep}" in file_name:
1951 ignore_file()
1952 return False
1953 if base_name.startswith('<') and base_name.endswith('>'):
1954 ignore_file()
1955 return False
1956 #
1957 # New in Leo 6.3. Never trace dangerous functions.
1958 table = (
1959 '_deepcopy.*',
1960 # Unicode primitives.
1961 'encode\b', 'decode\b',
1962 # System functions
1963 '.*__next\b',
1964 '<frozen>', '<genexpr>', '<listcomp>',
1965 # '<decorator-gen-.*>',
1966 'get\b',
1967 # String primitives.
1968 'append\b', 'split\b', 'join\b',
1969 # File primitives...
1970 'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
1971 'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
1972 )
1973 for z in table:
1974 if re.match(z, function_name):
1975 ignore_function()
1976 return False
1977 #
1978 # Legacy code.
1979 enabled = False
1980 if patterns is None:
1981 patterns = self.patterns
1982 for pattern in patterns:
1983 try:
1984 if pattern.startswith('+:'):
1985 if re.match(pattern[2:], file_name):
1986 enabled = True
1987 elif pattern.startswith('-:'):
1988 if re.match(pattern[2:], file_name):
1989 enabled = False
1990 elif pattern.startswith('+'):
1991 if re.match(pattern[1:], function_name):
1992 enabled = True
1993 elif pattern.startswith('-'):
1994 if re.match(pattern[1:], function_name):
1995 enabled = False
1996 else:
1997 self.bad_pattern(pattern)
1998 except Exception:
1999 self.bad_pattern(pattern)
2000 return enabled
2001 #@+node:ekr.20121128111829.12182: *4* print_stats
2002 def print_stats(self, patterns: List[str]=None) -> None:
2003 """Print all accumulated statisitics."""
2004 print('\nSherlock statistics...')
2005 if not patterns:
2006 patterns = ['+.*', '+:.*',]
2007 for fn in sorted(self.stats.keys()):
2008 d = self.stats.get(fn)
2009 if self.fn_is_enabled(fn, patterns):
2010 result = sorted(d.keys()) # type:ignore
2011 else:
2012 result = [key for key in sorted(d.keys()) # type:ignore
2013 if self.is_enabled(fn, key, patterns)]
2014 if result:
2015 print('')
2016 fn = fn.replace('\\', '/')
2017 parts = fn.split('/')
2018 print('/'.join(parts[-2:]))
2019 for key in result:
2020 print(f"{d.get(key):4} {key}")
2021 #@+node:ekr.20121128031949.12614: *4* run
2022 # Modified from pdb.Pdb.set_trace.
2024 def run(self, frame: Any=None) -> None:
2025 """Trace from the given frame or the caller's frame."""
2026 print("SherlockTracer.run:patterns:\n%s" % '\n'.join(self.patterns))
2027 if frame is None:
2028 frame = sys._getframe().f_back
2029 # Compute self.n, the number of frames to ignore.
2030 self.n = 0
2031 while frame:
2032 frame = frame.f_back
2033 self.n += 1
2034 # Pass self to sys.settrace to give easy access to all methods.
2035 sys.settrace(self)
2036 #@+node:ekr.20140322090829.16834: *4* push & pop
2037 def push(self, patterns: List[str]) -> None:
2038 """Push the old patterns and set the new."""
2039 self.pattern_stack.append(self.patterns) # type:ignore
2040 self.set_patterns(patterns)
2041 print(f"SherlockTracer.push: {self.patterns}")
2043 def pop(self) -> None:
2044 """Restore the pushed patterns."""
2045 if self.pattern_stack:
2046 self.patterns = self.pattern_stack.pop() # type:ignore
2047 print(f"SherlockTracer.pop: {self.patterns}")
2048 else:
2049 print('SherlockTracer.pop: pattern stack underflow')
2050 #@+node:ekr.20140326100337.16845: *4* set_patterns
2051 def set_patterns(self, patterns: List[str]) -> None:
2052 """Set the patterns in effect."""
2053 self.patterns = [z for z in patterns if self.check_pattern(z)]
2054 #@+node:ekr.20140322090829.16831: *4* show
2055 def show(self, item: Any) -> str:
2056 """return the best representation of item."""
2057 if not item:
2058 return repr(item)
2059 if isinstance(item, dict):
2060 return 'dict'
2061 if isinstance(item, str):
2062 s = repr(item)
2063 if len(s) <= 20:
2064 return s
2065 return s[:17] + '...'
2066 return repr(item)
2067 #@+node:ekr.20121128093229.12616: *4* stop
2068 def stop(self) -> None:
2069 """Stop all tracing."""
2070 sys.settrace(None)
2071 #@-others
2072#@+node:ekr.20191013145307.1: *3* class g.TkIDDialog (EmergencyDialog)
2073class TkIDDialog(EmergencyDialog):
2074 """A class that creates an tkinter dialog to get the Leo ID."""
2076 message = (
2077 "leoID.txt not found\n\n"
2078 "Please enter an id that identifies you uniquely.\n"
2079 "Your git/cvs/bzr login name is a good choice.\n\n"
2080 "Leo uses this id to uniquely identify nodes.\n\n"
2081 "Your id should contain only letters and numbers\n"
2082 "and must be at least 3 characters in length.")
2084 title = 'Enter Leo id'
2086 def __init__(self) -> None:
2087 super().__init__(self.title, self.message)
2088 self.val = ''
2090 #@+others
2091 #@+node:ekr.20191013145710.1: *4* leo_id_dialog.onKey
2092 def onKey(self, event: Any) -> None:
2093 """Handle Key events in askOk dialogs."""
2094 if event.char in '\n\r':
2095 self.okButton()
2096 #@+node:ekr.20191013145757.1: *4* leo_id_dialog.createTopFrame
2097 def createTopFrame(self) -> None:
2098 """Create the Tk.Toplevel widget for a leoTkinterDialog."""
2099 self.root = Tk.Tk() # type:ignore
2100 self.top = Tk.Toplevel(self.root) # type:ignore
2101 self.top.title(self.title)
2102 self.root.withdraw()
2103 self.frame = Tk.Frame(self.top) # type:ignore
2104 self.frame.pack(side="top", expand=1, fill="both")
2105 label = Tk.Label(self.frame, text=self.message, bg='white')
2106 label.pack(pady=10)
2107 self.entry = Tk.Entry(self.frame)
2108 self.entry.pack()
2109 self.entry.focus_set()
2110 #@+node:ekr.20191013150158.1: *4* leo_id_dialog.okButton
2111 def okButton(self) -> None:
2112 """Do default click action in ok button."""
2113 self.val = self.entry.get() # Return is not possible.
2114 self.top.destroy()
2115 self.top = None
2116 #@-others
2117#@+node:ekr.20080531075119.1: *3* class g.Tracer
2118class Tracer:
2119 """A "debugger" that computes a call graph.
2121 To trace a function and its callers, put the following at the function's start:
2123 g.startTracer()
2124 """
2125 #@+others
2126 #@+node:ekr.20080531075119.2: *4* __init__ (Tracer)
2127 def __init__(self, limit: int=0, trace: bool=False, verbose: bool=False) -> None:
2128 # Keys are function names.
2129 # Values are the number of times the function was called by the caller.
2130 self.callDict: Dict[str, Any] = {}
2131 # Keys are function names.
2132 # Values are the total number of times the function was called.
2133 self.calledDict: Dict[str, int] = {}
2134 self.count = 0
2135 self.inited = False
2136 self.limit = limit # 0: no limit, otherwise, limit trace to n entries deep.
2137 self.stack: List[str] = []
2138 self.trace = trace
2139 self.verbose = verbose # True: print returns as well as calls.
2140 #@+node:ekr.20080531075119.3: *4* computeName
2141 def computeName(self, frame: Any) -> str:
2142 if not frame:
2143 return ''
2144 code = frame.f_code
2145 result = []
2146 module = inspect.getmodule(code)
2147 if module:
2148 module_name = module.__name__
2149 if module_name == 'leo.core.leoGlobals':
2150 result.append('g')
2151 else:
2152 tag = 'leo.core.'
2153 if module_name.startswith(tag):
2154 module_name = module_name[len(tag) :]
2155 result.append(module_name)
2156 try:
2157 # This can fail during startup.
2158 self_obj = frame.f_locals.get('self')
2159 if self_obj:
2160 result.append(self_obj.__class__.__name__)
2161 except Exception:
2162 pass
2163 result.append(code.co_name)
2164 return '.'.join(result)
2165 #@+node:ekr.20080531075119.4: *4* report
2166 def report(self) -> None:
2167 if 0:
2168 g.pr('\nstack')
2169 for z in self.stack:
2170 g.pr(z)
2171 g.pr('\ncallDict...')
2172 for key in sorted(self.callDict):
2173 # Print the calling function.
2174 g.pr(f"{self.calledDict.get(key,0):d}", key)
2175 # Print the called functions.
2176 d = self.callDict.get(key)
2177 for key2 in sorted(d): # type:ignore
2178 g.pr(f"{d.get(key2):8d}", key2) # type:ignore
2179 #@+node:ekr.20080531075119.5: *4* stop
2180 def stop(self) -> None:
2181 sys.settrace(None)
2182 self.report()
2183 #@+node:ekr.20080531075119.6: *4* tracer
2184 def tracer(self, frame: Any, event: Any, arg: Any) -> Optional[Callable]:
2185 """A function to be passed to sys.settrace."""
2186 n = len(self.stack)
2187 if event == 'return':
2188 n = max(0, n - 1)
2189 pad = '.' * n
2190 if event == 'call':
2191 if not self.inited:
2192 # Add an extra stack element for the routine containing the call to startTracer.
2193 self.inited = True
2194 name = self.computeName(frame.f_back)
2195 self.updateStats(name)
2196 self.stack.append(name)
2197 name = self.computeName(frame)
2198 if self.trace and (self.limit == 0 or len(self.stack) < self.limit):
2199 g.trace(f"{pad}call", name)
2200 self.updateStats(name)
2201 self.stack.append(name)
2202 return self.tracer
2203 if event == 'return':
2204 if self.stack:
2205 name = self.stack.pop()
2206 if (
2207 self.trace and
2208 self.verbose and
2209 (self.limit == 0 or len(self.stack) < self.limit)
2210 ):
2211 g.trace(f"{pad}ret ", name)
2212 else:
2213 g.trace('return underflow')
2214 self.stop()
2215 return None
2216 if self.stack:
2217 return self.tracer
2218 self.stop()
2219 return None
2220 return self.tracer
2221 #@+node:ekr.20080531075119.7: *4* updateStats
2222 def updateStats(self, name: str) -> None:
2223 if not self.stack:
2224 return
2225 caller = self.stack[-1]
2226 # d is a dict reprenting the called functions.
2227 # Keys are called functions, values are counts.
2228 d: Dict[str, int] = self.callDict.get(caller, {})
2229 d[name] = 1 + d.get(name, 0)
2230 self.callDict[caller] = d
2231 # Update the total counts.
2232 self.calledDict[name] = 1 + self.calledDict.get(name, 0)
2233 #@-others
2235def startTracer(limit: int=0, trace: bool=False, verbose: bool=False) -> Callable:
2236 t = g.Tracer(limit=limit, trace=trace, verbose=verbose)
2237 sys.settrace(t.tracer)
2238 return t
2239#@+node:ekr.20031219074948.1: *3* class g.Tracing/NullObject & helpers
2240#@@nobeautify
2242tracing_tags: Dict[int, str] = {} # Keys are id's, values are tags.
2243tracing_vars: Dict[int, List] = {} # Keys are id's, values are names of ivars.
2244# Keys are signatures: '%s.%s:%s' % (tag, attr, callers). Values not important.
2245tracing_signatures: Dict[str, Any] = {}
2247class NullObject:
2248 """An object that does nothing, and does it very well."""
2249 def __init__(self, ivars: List[str]=None, *args: Any, **kwargs: Any) -> None:
2250 if isinstance(ivars, str):
2251 ivars = [ivars]
2252 tracing_vars [id(self)] = ivars or []
2253 def __call__(self, *args: Any, **keys: Any) -> "NullObject":
2254 return self
2255 def __repr__(self) -> str:
2256 return "NullObject"
2257 def __str__(self) -> str:
2258 return "NullObject"
2259 # Attribute access...
2260 def __delattr__(self, attr: str) -> None:
2261 return None
2262 def __getattr__(self, attr: str) -> Any:
2263 if attr in tracing_vars.get(id(self), []):
2264 return getattr(self, attr, None)
2265 return self # Required.
2266 def __setattr__(self, attr: str, val: Any) -> None:
2267 if attr in tracing_vars.get(id(self), []):
2268 object.__setattr__(self, attr, val)
2269 # Container methods..
2270 def __bool__(self) -> bool:
2271 return False
2272 def __contains__(self, item: Any) -> bool:
2273 return False
2274 def __getitem__(self, key: str) -> None:
2275 raise KeyError
2276 def __setitem__(self, key: str, val: Any) -> None:
2277 pass
2278 def __iter__(self) -> "NullObject":
2279 return self
2280 def __len__(self) -> int:
2281 return 0
2282 # Iteration methods:
2283 def __next__(self) -> None:
2284 raise StopIteration
2287class TracingNullObject:
2288 """Tracing NullObject."""
2289 def __init__(self, tag: str, ivars: List[Any]=None, *args: Any, **kwargs: Any) -> None:
2290 tracing_tags [id(self)] = tag
2291 if isinstance(ivars, str):
2292 ivars = [ivars]
2293 tracing_vars [id(self)] = ivars or []
2294 def __call__(self, *args: Any, **kwargs: Any) -> "TracingNullObject":
2295 return self
2296 def __repr__(self) -> str:
2297 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}'
2298 def __str__(self) -> str:
2299 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}'
2300 #
2301 # Attribute access...
2302 def __delattr__(self, attr: str) -> None:
2303 return None
2304 def __getattr__(self, attr: str) -> "TracingNullObject":
2305 null_object_print_attr(id(self), attr)
2306 if attr in tracing_vars.get(id(self), []):
2307 return getattr(self, attr, None)
2308 return self # Required.
2309 def __setattr__(self, attr: str, val: Any) -> None:
2310 g.null_object_print(id(self), '__setattr__', attr, val)
2311 if attr in tracing_vars.get(id(self), []):
2312 object.__setattr__(self, attr, val)
2313 #
2314 # All other methods...
2315 def __bool__(self) -> bool:
2316 if 0: # To do: print only once.
2317 suppress = ('getShortcut','on_idle', 'setItemText')
2318 callers = g.callers(2)
2319 if not callers.endswith(suppress):
2320 g.null_object_print(id(self), '__bool__')
2321 return False
2322 def __contains__(self, item: Any) -> bool:
2323 g.null_object_print(id(self), '__contains__')
2324 return False
2325 def __getitem__(self, key: str) -> None:
2326 g.null_object_print(id(self), '__getitem__')
2327 # pylint doesn't like trailing return None.
2328 def __iter__(self) -> "TracingNullObject":
2329 g.null_object_print(id(self), '__iter__')
2330 return self
2331 def __len__(self) -> int:
2332 # g.null_object_print(id(self), '__len__')
2333 return 0
2334 def __next__(self) -> None:
2335 g.null_object_print(id(self), '__next__')
2336 raise StopIteration
2337 def __setitem__(self, key: str, val: Any) -> None:
2338 g.null_object_print(id(self), '__setitem__')
2339 # pylint doesn't like trailing return None.
2340#@+node:ekr.20190330062625.1: *4* g.null_object_print_attr
2341def null_object_print_attr(id_: int, attr: str) -> None:
2342 suppress = True
2343 suppress_callers: List[str] = []
2344 suppress_attrs: List[str] = []
2345 if suppress:
2346 #@+<< define suppression lists >>
2347 #@+node:ekr.20190330072026.1: *5* << define suppression lists >>
2348 suppress_callers = [
2349 'drawNode', 'drawTopTree', 'drawTree',
2350 'contractItem', 'getCurrentItem',
2351 'declutter_node',
2352 'finishCreate',
2353 'initAfterLoad',
2354 'show_tips',
2355 'writeWaitingLog',
2356 # 'set_focus', 'show_tips',
2357 ]
2358 suppress_attrs = [
2359 # Leo...
2360 'c.frame.body.wrapper',
2361 'c.frame.getIconBar.add',
2362 'c.frame.log.createTab',
2363 'c.frame.log.enable',
2364 'c.frame.log.finishCreate',
2365 'c.frame.menu.createMenuBar',
2366 'c.frame.menu.finishCreate',
2367 # 'c.frame.menu.getMenu',
2368 'currentItem',
2369 'dw.leo_master.windowTitle',
2370 # Pyzo...
2371 'pyzo.keyMapper.connect',
2372 'pyzo.keyMapper.keyMappingChanged',
2373 'pyzo.keyMapper.setShortcut',
2374 ]
2375 #@-<< define suppression lists >>
2376 tag = tracing_tags.get(id_, "<NO TAG>")
2377 callers = g.callers(3).split(',')
2378 callers = ','.join(callers[:-1])
2379 in_callers = any(z in callers for z in suppress_callers)
2380 s = f"{tag}.{attr}"
2381 if suppress:
2382 # Filter traces.
2383 if not in_callers and s not in suppress_attrs:
2384 g.pr(f"{s:40} {callers}")
2385 else:
2386 # Print each signature once. No need to filter!
2387 signature = f"{tag}.{attr}:{callers}"
2388 if signature not in tracing_signatures:
2389 tracing_signatures[signature] = True
2390 g.pr(f"{s:40} {callers}")
2391#@+node:ekr.20190330072832.1: *4* g.null_object_print
2392def null_object_print(id_: int, kind: Any, *args: Any) -> None:
2393 tag = tracing_tags.get(id_, "<NO TAG>")
2394 callers = g.callers(3).split(',')
2395 callers = ','.join(callers[:-1])
2396 s = f"{kind}.{tag}"
2397 signature = f"{s}:{callers}"
2398 if 1:
2399 # Always print:
2400 if args:
2401 args_s = ', '.join([repr(z) for z in args])
2402 g.pr(f"{s:40} {callers}\n\t\t\targs: {args_s}")
2403 else:
2404 g.pr(f"{s:40} {callers}")
2405 elif signature not in tracing_signatures:
2406 # Print each signature once.
2407 tracing_signatures[signature] = True
2408 g.pr(f"{s:40} {callers}")
2409#@+node:ekr.20120129181245.10220: *3* class g.TypedDict
2410class TypedDict:
2411 """
2412 A class providing additional dictionary-related methods:
2414 __init__: Specifies types and the dict's name.
2415 __repr__: Compatible with g.printObj, based on g.objToString.
2416 __setitem__: Type checks its arguments.
2417 __str__: A concise summary of the inner dict.
2418 add_to_list: A convenience method that adds a value to its key's list.
2419 name: The dict's name.
2420 setName: Sets the dict's name, for use by __repr__.
2422 Overrides the following standard methods:
2424 copy: A thin wrapper for copy.deepcopy.
2425 get: Returns self.d.get
2426 items: Returns self.d.items
2427 keys: Returns self.d.keys
2428 update: Updates self.d from either a dict or a TypedDict.
2429 """
2431 def __init__(self, name: str, keyType: Any, valType: Any) -> None:
2432 self.d: Dict[str, Any] = {}
2433 self._name = name # For __repr__ only.
2434 self.keyType = keyType
2435 self.valType = valType
2436 #@+others
2437 #@+node:ekr.20120205022040.17770: *4* td.__repr__ & __str__
2438 def __str__(self) -> str:
2439 """Concise: used by repr."""
2440 return (
2441 f"<TypedDict name:{self._name} "
2442 f"keys:{self.keyType.__name__} "
2443 f"values:{self.valType.__name__} "
2444 f"len(keys): {len(list(self.keys()))}>"
2445 )
2447 def __repr__(self) -> str:
2448 """Suitable for g.printObj"""
2449 return f"{g.dictToString(self.d)}\n{str(self)}\n"
2450 #@+node:ekr.20120205022040.17774: *4* td.__setitem__
2451 def __setitem__(self, key: Any, val: Any) -> None:
2452 """Allow d[key] = val"""
2453 if key is None:
2454 g.trace('TypeDict: None is not a valid key', g.callers())
2455 return
2456 self._checkKeyType(key)
2457 try:
2458 for z in val:
2459 self._checkValType(z)
2460 except TypeError:
2461 self._checkValType(val) # val is not iterable.
2462 self.d[key] = val
2463 #@+node:ekr.20190904052828.1: *4* td.add_to_list
2464 def add_to_list(self, key: Any, val: Any) -> None:
2465 """Update the *list*, self.d [key]"""
2466 if key is None:
2467 g.trace('TypeDict: None is not a valid key', g.callers())
2468 return
2469 self._checkKeyType(key)
2470 self._checkValType(val)
2471 aList = self.d.get(key, [])
2472 if val not in aList:
2473 aList.append(val)
2474 self.d[key] = aList
2475 #@+node:ekr.20120206134955.10150: *4* td.checking
2476 def _checkKeyType(self, key: str) -> None:
2477 if key and key.__class__ != self.keyType:
2478 self._reportTypeError(key, self.keyType)
2480 def _checkValType(self, val: Any) -> None:
2481 if val.__class__ != self.valType:
2482 self._reportTypeError(val, self.valType)
2484 def _reportTypeError(self, obj: Any, objType: Any) -> str:
2485 return (
2486 f"{self._name}\n"
2487 f"expected: {obj.__class__.__name__}\n"
2488 f" got: {objType.__name__}")
2489 #@+node:ekr.20120223062418.10422: *4* td.copy
2490 def copy(self, name: str=None) -> Any:
2491 """Return a new dict with the same contents."""
2492 import copy
2493 return copy.deepcopy(self)
2494 #@+node:ekr.20120205022040.17771: *4* td.get & keys & values
2495 def get(self, key: Any, default: Any=None) -> Any:
2496 return self.d.get(key, default)
2498 def items(self) -> Any:
2499 return self.d.items()
2501 def keys(self) -> Any:
2502 return self.d.keys()
2504 def values(self) -> Any:
2505 return self.d.values()
2506 #@+node:ekr.20190903181030.1: *4* td.get_setting & get_string_setting
2507 def get_setting(self, key: str) -> Any:
2508 key = key.replace('-', '').replace('_', '')
2509 gs = self.get(key)
2510 val = gs and gs.val
2511 return val
2513 def get_string_setting(self, key: str) -> Optional[str]:
2514 val = self.get_setting(key)
2515 return val if val and isinstance(val, str) else None
2516 #@+node:ekr.20190904103552.1: *4* td.name & setName
2517 def name(self) -> str:
2518 return self._name
2520 def setName(self, name: str) -> None:
2521 self._name = name
2522 #@+node:ekr.20120205022040.17807: *4* td.update
2523 def update(self, d: Dict[Any, Any]) -> None:
2524 """Update self.d from a the appropriate dict."""
2525 if isinstance(d, TypedDict):
2526 self.d.update(d.d)
2527 else:
2528 self.d.update(d)
2529 #@-others
2530#@+node:ville.20090827174345.9963: *3* class g.UiTypeException & g.assertui
2531class UiTypeException(Exception):
2532 pass
2534def assertUi(uitype: Any) -> None:
2535 if not g.app.gui.guiName() == uitype:
2536 raise UiTypeException
2537#@+node:ekr.20200219071828.1: *3* class TestLeoGlobals (leoGlobals.py)
2538class TestLeoGlobals(unittest.TestCase):
2539 """Tests for leoGlobals.py."""
2540 #@+others
2541 #@+node:ekr.20200219071958.1: *4* test_comment_delims_from_extension
2542 def test_comment_delims_from_extension(self) -> None:
2544 # pylint: disable=import-self
2545 from leo.core import leoGlobals as leo_g
2546 from leo.core import leoApp
2547 leo_g.app = leoApp.LeoApp()
2548 assert leo_g.comment_delims_from_extension(".py") == ('#', '', '')
2549 assert leo_g.comment_delims_from_extension(".c") == ('//', '/*', '*/')
2550 assert leo_g.comment_delims_from_extension(".html") == ('', '<!--', '-->')
2551 #@+node:ekr.20200219072957.1: *4* test_is_sentinel
2552 def test_is_sentinel(self) -> None:
2554 # pylint: disable=import-self
2555 from leo.core import leoGlobals as leo_g
2556 # Python.
2557 py_delims = leo_g.comment_delims_from_extension('.py')
2558 assert leo_g.is_sentinel("#@+node", py_delims)
2559 assert not leo_g.is_sentinel("#comment", py_delims)
2560 # C.
2561 c_delims = leo_g.comment_delims_from_extension('.c')
2562 assert leo_g.is_sentinel("//@+node", c_delims)
2563 assert not g.is_sentinel("//comment", c_delims)
2564 # Html.
2565 html_delims = leo_g.comment_delims_from_extension('.html')
2566 assert leo_g.is_sentinel("<!--@+node-->", html_delims)
2567 assert not leo_g.is_sentinel("<!--comment-->", html_delims)
2568 #@-others
2569#@+node:ekr.20140904112935.18526: *3* g.isTextWrapper & isTextWidget
2570def isTextWidget(w: Any) -> bool:
2571 return g.app.gui.isTextWidget(w)
2573def isTextWrapper(w: Any) -> bool:
2574 return g.app.gui.isTextWrapper(w)
2575#@+node:ekr.20140711071454.17649: ** g.Debugging, GC, Stats & Timing
2576#@+node:ekr.20031218072017.3104: *3* g.Debugging
2577#@+node:ekr.20180415144534.1: *4* g.assert_is
2578def assert_is(obj: Any, list_or_class: Any, warn: bool=True) -> bool:
2580 if warn:
2581 ok = isinstance(obj, list_or_class)
2582 if not ok:
2583 g.es_print(
2584 f"can not happen. {obj !r}: "
2585 f"expected {list_or_class}, "
2586 f"got: {obj.__class__.__name__}")
2587 g.es_print(g.callers())
2588 return ok
2589 ok = isinstance(obj, list_or_class)
2590 assert ok, (obj, obj.__class__.__name__, g.callers())
2591 return ok
2592#@+node:ekr.20180420081530.1: *4* g._assert
2593def _assert(condition: Any, show_callers: bool=True) -> bool:
2594 """A safer alternative to a bare assert."""
2595 if g.unitTesting:
2596 assert condition
2597 return True
2598 ok = bool(condition)
2599 if ok:
2600 return True
2601 g.es_print('\n===== g._assert failed =====\n')
2602 if show_callers:
2603 g.es_print(g.callers())
2604 return False
2605#@+node:ekr.20051023083258: *4* g.callers & g.caller & _callerName
2606def callers(n: int=4, count: int=0, excludeCaller: bool=True, verbose: bool=False) -> str:
2607 """
2608 Return a string containing a comma-separated list of the callers
2609 of the function that called g.callerList.
2611 excludeCaller: True (the default), g.callers itself is not on the list.
2613 If the `verbose` keyword is True, return a list separated by newlines.
2614 """
2615 # Be careful to call g._callerName with smaller values of i first:
2616 # sys._getframe throws ValueError if there are less than i entries.
2617 result = []
2618 i = 3 if excludeCaller else 2
2619 while 1:
2620 s = _callerName(n=i, verbose=verbose)
2621 if s:
2622 result.append(s)
2623 if not s or len(result) >= n:
2624 break
2625 i += 1
2626 result.reverse()
2627 if count > 0:
2628 result = result[:count]
2629 if verbose:
2630 return ''.join([f"\n {z}" for z in result])
2631 return ','.join(result)
2632#@+node:ekr.20031218072017.3107: *5* g._callerName
2633def _callerName(n: int, verbose: bool=False) -> str:
2634 try:
2635 # get the function name from the call stack.
2636 f1 = sys._getframe(n) # The stack frame, n levels up.
2637 code1 = f1.f_code # The code object
2638 sfn = shortFilename(code1.co_filename) # The file name.
2639 locals_ = f1.f_locals # The local namespace.
2640 name = code1.co_name
2641 line = code1.co_firstlineno
2642 if verbose:
2643 obj = locals_.get('self')
2644 full_name = f"{obj.__class__.__name__}.{name}" if obj else name
2645 return f"line {line:4} {sfn:>30} {full_name}"
2646 return name
2647 except ValueError:
2648 # The stack is not deep enough OR
2649 # sys._getframe does not exist on this platform.
2650 return ''
2651 except Exception:
2652 es_exception()
2653 return '' # "<no caller name>"
2654#@+node:ekr.20180328170441.1: *5* g.caller
2655def caller(i: int=1) -> str:
2656 """Return the caller name i levels up the stack."""
2657 return g.callers(i + 1).split(',')[0]
2658#@+node:ekr.20031218072017.3109: *4* g.dump
2659def dump(s: str) -> str:
2660 out = ""
2661 for i in s:
2662 out += str(ord(i)) + ","
2663 return out
2665def oldDump(s: str) -> str:
2666 out = ""
2667 for i in s:
2668 if i == '\n':
2669 out += "["
2670 out += "n"
2671 out += "]"
2672 if i == '\t':
2673 out += "["
2674 out += "t"
2675 out += "]"
2676 elif i == ' ':
2677 out += "["
2678 out += " "
2679 out += "]"
2680 else:
2681 out += i
2682 return out
2683#@+node:ekr.20210904114446.1: *4* g.dump_tree & g.tree_to_string
2684def dump_tree(c: Cmdr, dump_body: bool=False, msg: str=None) -> None:
2685 if msg:
2686 print(msg.rstrip())
2687 else:
2688 print('')
2689 for p in c.all_positions():
2690 print(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}")
2691 if dump_body:
2692 for z in g.splitLines(p.b):
2693 print(z.rstrip())
2695def tree_to_string(c: Cmdr, dump_body: bool=False, msg: str=None) -> str:
2696 result = ['\n']
2697 if msg:
2698 result.append(msg)
2699 for p in c.all_positions():
2700 result.append(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}")
2701 if dump_body:
2702 for z in g.splitLines(p.b):
2703 result.append(z.rstrip())
2704 return '\n'.join(result)
2705#@+node:ekr.20150227102835.8: *4* g.dump_encoded_string
2706def dump_encoded_string(encoding: str, s: str) -> None:
2707 """Dump s, assumed to be an encoded string."""
2708 # Can't use g.trace here: it calls this function!
2709 print(f"dump_encoded_string: {g.callers()}")
2710 print(f"dump_encoded_string: encoding {encoding}\n")
2711 print(s)
2712 in_comment = False
2713 for ch in s:
2714 if ch == '#':
2715 in_comment = True
2716 elif not in_comment:
2717 print(f"{ord(ch):02x} {repr(ch)}")
2718 elif ch == '\n':
2719 in_comment = False
2720#@+node:ekr.20031218072017.1317: *4* g.file/module/plugin_date
2721def module_date(mod: Any, format: str=None) -> str:
2722 theFile = g.os_path_join(app.loadDir, mod.__file__)
2723 root, ext = g.os_path_splitext(theFile)
2724 return g.file_date(root + ".py", format=format)
2726def plugin_date(plugin_mod: Any, format: str=None) -> str:
2727 theFile = g.os_path_join(app.loadDir, "..", "plugins", plugin_mod.__file__)
2728 root, ext = g.os_path_splitext(theFile)
2729 return g.file_date(root + ".py", format=str)
2731def file_date(theFile: Any, format: str=None) -> str:
2732 if theFile and g.os_path_exists(theFile):
2733 try:
2734 n = g.os_path_getmtime(theFile)
2735 if format is None:
2736 format = "%m/%d/%y %H:%M:%S"
2737 return time.strftime(format, time.gmtime(n))
2738 except(ImportError, NameError):
2739 pass # Time module is platform dependent.
2740 return ""
2741#@+node:ekr.20031218072017.3127: *4* g.get_line & get_line__after
2742# Very useful for tracing.
2744def get_line(s: str, i: int) -> str:
2745 nl = ""
2746 if g.is_nl(s, i):
2747 i = g.skip_nl(s, i)
2748 nl = "[nl]"
2749 j = g.find_line_start(s, i)
2750 k = g.skip_to_end_of_line(s, i)
2751 return nl + s[j:k]
2753# Important: getLine is a completely different function.
2754# getLine = get_line
2756def get_line_after(s: str, i: int) -> str:
2757 nl = ""
2758 if g.is_nl(s, i):
2759 i = g.skip_nl(s, i)
2760 nl = "[nl]"
2761 k = g.skip_to_end_of_line(s, i)
2762 return nl + s[i:k]
2764getLineAfter = get_line_after
2765#@+node:ekr.20080729142651.1: *4* g.getIvarsDict and checkUnchangedIvars
2766def getIvarsDict(obj: Any) -> Dict[str, Any]:
2767 """Return a dictionary of ivars:values for non-methods of obj."""
2768 d: Dict[str, Any] = dict(
2769 [[key, getattr(obj, key)] for key in dir(obj) # type:ignore
2770 if not isinstance(getattr(obj, key), types.MethodType)])
2771 return d
2773def checkUnchangedIvars(
2774 obj: Any,
2775 d: Dict[str, Any],
2776 exceptions: Sequence[str]=None,
2777) -> bool:
2778 if not exceptions:
2779 exceptions = []
2780 ok = True
2781 for key in d:
2782 if key not in exceptions:
2783 if getattr(obj, key) != d.get(key):
2784 g.trace(
2785 f"changed ivar: {key} "
2786 f"old: {repr(d.get(key))} "
2787 f"new: {repr(getattr(obj, key))}")
2788 ok = False
2789 return ok
2790#@+node:ekr.20031218072017.3128: *4* g.pause
2791def pause(s: str) -> None:
2792 g.pr(s)
2793 i = 0
2794 while i < 1000 * 1000:
2795 i += 1
2796#@+node:ekr.20041105091148: *4* g.pdb
2797def pdb(message: str='') -> None:
2798 """Fall into pdb."""
2799 import pdb # Required: we have just defined pdb as a function!
2800 if app and not app.useIpython:
2801 try:
2802 from leo.core.leoQt import QtCore
2803 QtCore.pyqtRemoveInputHook()
2804 except Exception:
2805 pass
2806 if message:
2807 print(message)
2808 # pylint: disable=forgotten-debug-statement
2809 pdb.set_trace()
2810#@+node:ekr.20041224080039: *4* g.dictToString
2811def dictToString(d: Dict[str, str], indent: str='', tag: str=None) -> str:
2812 """Pretty print a Python dict to a string."""
2813 # pylint: disable=unnecessary-lambda
2814 if not d:
2815 return '{}'
2816 result = ['{\n']
2817 indent2 = indent + ' ' * 4
2818 n = 2 + len(indent) + max([len(repr(z)) for z in d.keys()])
2819 for i, key in enumerate(sorted(d, key=lambda z: repr(z))):
2820 pad = ' ' * max(0, (n - len(repr(key))))
2821 result.append(f"{pad}{key}:")
2822 result.append(objToString(d.get(key), indent=indent2))
2823 if i + 1 < len(d.keys()):
2824 result.append(',')
2825 result.append('\n')
2826 result.append(indent + '}')
2827 s = ''.join(result)
2828 return f"{tag}...\n{s}\n" if tag else s
2829#@+node:ekr.20041126060136: *4* g.listToString
2830def listToString(obj: Any, indent: str='', tag: str=None) -> str:
2831 """Pretty print a Python list to a string."""
2832 if not obj:
2833 return '[]'
2834 result = ['[']
2835 indent2 = indent + ' ' * 4
2836 # I prefer not to compress lists.
2837 for i, obj2 in enumerate(obj):
2838 result.append('\n' + indent2)
2839 result.append(objToString(obj2, indent=indent2))
2840 if i + 1 < len(obj) > 1:
2841 result.append(',')
2842 else:
2843 result.append('\n' + indent)
2844 result.append(']')
2845 s = ''.join(result)
2846 return f"{tag}...\n{s}\n" if tag else s
2847#@+node:ekr.20050819064157: *4* g.objToSTring & g.toString
2848def objToString(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> str:
2849 """Pretty print any Python object to a string."""
2850 # pylint: disable=undefined-loop-variable
2851 # Looks like a a pylint bug.
2852 #
2853 # Compute s.
2854 if isinstance(obj, dict):
2855 s = dictToString(obj, indent=indent)
2856 elif isinstance(obj, list):
2857 s = listToString(obj, indent=indent)
2858 elif isinstance(obj, tuple):
2859 s = tupleToString(obj, indent=indent)
2860 elif isinstance(obj, str):
2861 # Print multi-line strings as lists.
2862 s = obj
2863 lines = g.splitLines(s)
2864 if len(lines) > 1:
2865 s = listToString(lines, indent=indent)
2866 else:
2867 s = repr(s)
2868 else:
2869 s = repr(obj)
2870 #
2871 # Compute the return value.
2872 if printCaller and tag:
2873 prefix = f"{g.caller()}: {tag}"
2874 elif printCaller or tag:
2875 prefix = g.caller() if printCaller else tag
2876 else:
2877 prefix = ''
2878 if prefix:
2879 sep = '\n' if '\n' in s else ' '
2880 return f"{prefix}:{sep}{s}"
2881 return s
2883toString = objToString
2884#@+node:ekr.20120912153732.10597: *4* g.wait
2885def sleep(n: float) -> None:
2886 """Wait about n milliseconds."""
2887 from time import sleep # type:ignore
2888 sleep(n) # type:ignore
2889#@+node:ekr.20171023140544.1: *4* g.printObj & aliases
2890def printObj(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> None:
2891 """Pretty print any Python object using g.pr."""
2892 g.pr(objToString(obj, indent=indent, printCaller=printCaller, tag=tag))
2894printDict = printObj
2895printList = printObj
2896printTuple = printObj
2897#@+node:ekr.20171023110057.1: *4* g.tupleToString
2898def tupleToString(obj: Any, indent: str='', tag: str=None) -> str:
2899 """Pretty print a Python tuple to a string."""
2900 if not obj:
2901 return '(),'
2902 result = ['(']
2903 indent2 = indent + ' ' * 4
2904 for i, obj2 in enumerate(obj):
2905 if len(obj) > 1:
2906 result.append('\n' + indent2)
2907 result.append(objToString(obj2, indent=indent2))
2908 if len(obj) == 1 or i + 1 < len(obj):
2909 result.append(',')
2910 elif len(obj) > 1:
2911 result.append('\n' + indent)
2912 result.append(')')
2913 s = ''.join(result)
2914 return f"{tag}...\n{s}\n" if tag else s
2915#@+node:ekr.20031218072017.1588: *3* g.Garbage Collection
2916#@+node:ekr.20031218072017.1589: *4* g.clearAllIvars
2917def clearAllIvars(o: Any) -> None:
2918 """Clear all ivars of o, a member of some class."""
2919 if o:
2920 o.__dict__.clear()
2921#@+node:ekr.20060127162818: *4* g.enable_gc_debug
2922def enable_gc_debug() -> None:
2924 gc.set_debug(
2925 gc.DEBUG_STATS | # prints statistics.
2926 gc.DEBUG_LEAK | # Same as all below.
2927 gc.DEBUG_COLLECTABLE |
2928 gc.DEBUG_UNCOLLECTABLE |
2929 # gc.DEBUG_INSTANCES |
2930 # gc.DEBUG_OBJECTS |
2931 gc.DEBUG_SAVEALL)
2932#@+node:ekr.20031218072017.1592: *4* g.printGc
2933# Formerly called from unit tests.
2935def printGc() -> None:
2936 """Called from trace_gc_plugin."""
2937 g.printGcSummary()
2938 g.printGcObjects()
2939 g.printGcRefs()
2940#@+node:ekr.20060127164729.1: *4* g.printGcObjects
2941lastObjectCount = 0
2943def printGcObjects() -> int:
2944 """Print a summary of GC statistics."""
2945 global lastObjectCount
2946 n = len(gc.garbage)
2947 n2 = len(gc.get_objects())
2948 delta = n2 - lastObjectCount
2949 print('-' * 30)
2950 print(f"garbage: {n}")
2951 print(f"{delta:6d} = {n2:7d} totals")
2952 # print number of each type of object.
2953 d: Dict[str, int] = {}
2954 count = 0
2955 for obj in gc.get_objects():
2956 key = str(type(obj))
2957 n = d.get(key, 0)
2958 d[key] = n + 1
2959 count += 1
2960 print(f"{count:7} objects...")
2961 # Invert the dict.
2962 d2: Dict[int, str] = {v: k for k, v in d.items()}
2963 for key in reversed(sorted(d2.keys())): # type:ignore
2964 val = d2.get(key) # type:ignore
2965 print(f"{key:7} {val}")
2966 lastObjectCount = count
2967 return delta
2968#@+node:ekr.20031218072017.1593: *4* g.printGcRefs
2969def printGcRefs() -> None:
2971 refs = gc.get_referrers(app.windowList[0])
2972 print(f"{len(refs):d} referers")
2973#@+node:ekr.20060205043324.1: *4* g.printGcSummary
2974def printGcSummary() -> None:
2976 g.enable_gc_debug()
2977 try:
2978 n = len(gc.garbage)
2979 n2 = len(gc.get_objects())
2980 s = f"printGCSummary: garbage: {n}, objects: {n2}"
2981 print(s)
2982 except Exception:
2983 traceback.print_exc()
2984#@+node:ekr.20180528151850.1: *3* g.printTimes
2985def printTimes(times: List) -> None:
2986 """
2987 Print the differences in the times array.
2989 times: an array of times (calls to time.process_time()).
2990 """
2991 for n, junk in enumerate(times[:-1]):
2992 t = times[n + 1] - times[n]
2993 if t > 0.1:
2994 g.trace(f"*** {n} {t:5.4f} sec.")
2995#@+node:ekr.20031218072017.3133: *3* g.Statistics
2996#@+node:ekr.20031218072017.3134: *4* g.clearStats
2997def clearStats() -> None:
2999 g.app.statsDict = {}
3000#@+node:ekr.20031218072017.3135: *4* g.printStats
3001@command('show-stats')
3002def printStats(event: Any=None, name: str=None) -> None:
3003 """
3004 Print all gathered statistics.
3006 Here is the recommended code to gather stats for one method/function:
3008 if not g.app.statsLockout:
3009 g.app.statsLockout = True
3010 try:
3011 d = g.app.statsDict
3012 key = 'g.isUnicode:' + g.callers()
3013 d [key] = d.get(key, 0) + 1
3014 finally:
3015 g.app.statsLockout = False
3016 """
3017 if name:
3018 if not isinstance(name, str):
3019 name = repr(name)
3020 else:
3021 # Get caller name 2 levels back.
3022 name = g._callerName(n=2)
3023 # Print the stats, organized by number of calls.
3024 d = g.app.statsDict
3025 print('g.app.statsDict...')
3026 for key in reversed(sorted(d)):
3027 print(f"{key:7} {d.get(key)}")
3028#@+node:ekr.20031218072017.3136: *4* g.stat
3029def stat(name: str=None) -> None:
3030 """Increments the statistic for name in g.app.statsDict
3031 The caller's name is used by default.
3032 """
3033 d = g.app.statsDict
3034 if name:
3035 if not isinstance(name, str):
3036 name = repr(name)
3037 else:
3038 name = g._callerName(n=2) # Get caller name 2 levels back.
3039 d[name] = 1 + d.get(name, 0)
3040#@+node:ekr.20031218072017.3137: *3* g.Timing
3041def getTime() -> float:
3042 return time.time()
3044def esDiffTime(message: str, start: float) -> float:
3045 delta = time.time() - start
3046 g.es('', f"{message} {delta:5.2f} sec.")
3047 return time.time()
3049def printDiffTime(message: str, start: float) -> float:
3050 delta = time.time() - start
3051 g.pr(f"{message} {delta:5.2f} sec.")
3052 return time.time()
3054def timeSince(start: float) -> str:
3055 return f"{time.time()-start:5.2f} sec."
3056#@+node:ekr.20031218072017.1380: ** g.Directives
3057# Weird pylint bug, activated by TestLeoGlobals class.
3058# Disabling this will be safe, because pyflakes will still warn about true redefinitions
3059# pylint: disable=function-redefined
3060#@+node:EKR.20040504150046.4: *3* g.comment_delims_from_extension
3061def comment_delims_from_extension(filename: str) -> Tuple[str, str, str]:
3062 """
3063 Return the comment delims corresponding to the filename's extension.
3064 """
3065 if filename.startswith('.'):
3066 root, ext = None, filename
3067 else:
3068 root, ext = os.path.splitext(filename)
3069 if ext == '.tmp':
3070 root, ext = os.path.splitext(root)
3071 language = g.app.extension_dict.get(ext[1:])
3072 if ext:
3073 return g.set_delims_from_language(language)
3074 g.trace(
3075 f"unknown extension: {ext!r}, "
3076 f"filename: {filename!r}, "
3077 f"root: {root!r}")
3078 return '', '', ''
3079#@+node:ekr.20170201150505.1: *3* g.findAllValidLanguageDirectives
3080def findAllValidLanguageDirectives(s: str) -> List:
3081 """Return list of all valid @language directives in p.b"""
3082 if not s.strip():
3083 return []
3084 languages = set()
3085 for m in g.g_language_pat.finditer(s):
3086 language = m.group(1)
3087 if g.isValidLanguage(language):
3088 languages.add(language)
3089 return list(sorted(languages))
3090#@+node:ekr.20090214075058.8: *3* g.findAtTabWidthDirectives (must be fast)
3091def findTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[str]:
3092 """Return the language in effect at position p."""
3093 if c is None:
3094 return None # c may be None for testing.
3095 w = None
3096 # 2009/10/02: no need for copy arg to iter
3097 for p in p.self_and_parents(copy=False):
3098 if w:
3099 break
3100 for s in p.h, p.b:
3101 if w:
3102 break
3103 anIter = g_tabwidth_pat.finditer(s)
3104 for m in anIter:
3105 word = m.group(0)
3106 i = m.start(0)
3107 j = g.skip_ws(s, i + len(word))
3108 junk, w = g.skip_long(s, j)
3109 if w == 0:
3110 w = None
3111 return w
3112#@+node:ekr.20170127142001.5: *3* g.findFirstAtLanguageDirective
3113def findFirstValidAtLanguageDirective(s: str) -> Optional[str]:
3114 """Return the first *valid* @language directive ins."""
3115 if not s.strip():
3116 return None
3117 for m in g.g_language_pat.finditer(s):
3118 language = m.group(1)
3119 if g.isValidLanguage(language):
3120 return language
3121 return None
3122#@+node:ekr.20090214075058.6: *3* g.findLanguageDirectives (must be fast)
3123def findLanguageDirectives(c: Cmdr, p: Pos) -> Optional[str]:
3124 """Return the language in effect at position p."""
3125 if c is None or p is None:
3126 return None # c may be None for testing.
3128 v0 = p.v
3130 def find_language(p_or_v: Any) -> Optional[str]:
3131 for s in p_or_v.h, p_or_v.b:
3132 for m in g_language_pat.finditer(s):
3133 language = m.group(1)
3134 if g.isValidLanguage(language):
3135 return language
3136 return None
3138 # First, search up the tree.
3139 for p in p.self_and_parents(copy=False):
3140 language = find_language(p)
3141 if language:
3142 return language
3143 # #1625: Second, expand the search for cloned nodes.
3144 seen = [] # vnodes that have already been searched.
3145 parents = v0.parents[:] # vnodes whose ancestors are to be searched.
3146 while parents:
3147 parent_v = parents.pop()
3148 if parent_v in seen:
3149 continue
3150 seen.append(parent_v)
3151 language = find_language(parent_v)
3152 if language:
3153 return language
3154 for grand_parent_v in parent_v.parents:
3155 if grand_parent_v not in seen:
3156 parents.append(grand_parent_v)
3157 # Finally, fall back to the defaults.
3158 return c.target_language.lower() if c.target_language else 'python'
3159#@+node:ekr.20031218072017.1385: *3* g.findReference
3160# Called from the syntax coloring method that colorizes section references.
3161# Also called from write at.putRefAt.
3163def findReference(name: str, root: Pos) -> Optional[Pos]:
3164 """Return the position containing the section definition for name."""
3165 for p in root.subtree(copy=False):
3166 assert p != root
3167 if p.matchHeadline(name) and not p.isAtIgnoreNode():
3168 return p.copy()
3169 return None
3170#@+node:ekr.20090214075058.9: *3* g.get_directives_dict (must be fast)
3171# The caller passes [root_node] or None as the second arg.
3172# This allows us to distinguish between None and [None].
3174def get_directives_dict(p: Pos, root: Any=None) -> Dict[str, str]:
3175 """
3176 Scan p for Leo directives found in globalDirectiveList.
3178 Returns a dict containing the stripped remainder of the line
3179 following the first occurrence of each recognized directive
3180 """
3181 if root:
3182 root_node = root[0]
3183 d = {}
3184 #
3185 # #1688: legacy: Always compute the pattern.
3186 # g.directives_pat is updated whenever loading a plugin.
3187 #
3188 # The headline has higher precedence because it is more visible.
3189 for kind, s in (('head', p.h), ('body', p.b)):
3190 anIter = g.directives_pat.finditer(s)
3191 for m in anIter:
3192 word = m.group(1).strip()
3193 i = m.start(1)
3194 if word in d:
3195 continue
3196 j = i + len(word)
3197 if j < len(s) and s[j] not in ' \t\n':
3198 # Not a valid directive: just ignore it.
3199 # A unit test tests that @path:any is invalid.
3200 continue
3201 k = g.skip_line(s, j)
3202 val = s[j:k].strip()
3203 d[word] = val
3204 if root:
3205 anIter = g_noweb_root.finditer(p.b)
3206 for m in anIter:
3207 if root_node:
3208 d["root"] = 0 # value not immportant
3209 else:
3210 g.es(f'{g.angleBrackets("*")} may only occur in a topmost node (i.e., without a parent)')
3211 break
3212 return d
3213#@+node:ekr.20080827175609.1: *3* g.get_directives_dict_list (must be fast)
3214def get_directives_dict_list(p: Pos) -> List[Dict]:
3215 """Scans p and all its ancestors for directives.
3217 Returns a list of dicts containing pointers to
3218 the start of each directive"""
3219 result = []
3220 p1 = p.copy()
3221 for p in p1.self_and_parents(copy=False):
3222 # No copy necessary: g.get_directives_dict does not change p.
3223 root = None if p.hasParent() else [p]
3224 result.append(g.get_directives_dict(p, root=root))
3225 return result
3226#@+node:ekr.20111010082822.15545: *3* g.getLanguageFromAncestorAtFileNode
3227def getLanguageFromAncestorAtFileNode(p: Pos) -> Optional[str]:
3228 """
3229 Return the language in effect at node p.
3231 1. Use an unambiguous @language directive in p itself.
3232 2. Search p's "extended parents" for an @<file> node.
3233 3. Search p's "extended parents" for an unambiguous @language directive.
3234 """
3235 v0 = p.v
3236 seen: Set[VNode]
3238 # The same generator as in v.setAllAncestorAtFileNodesDirty.
3239 # Original idea by Виталије Милошевић (Vitalije Milosevic).
3240 # Modified by EKR.
3242 def v_and_parents(v: "VNode") -> Generator:
3243 if v in seen:
3244 return
3245 seen.add(v)
3246 yield v
3247 for parent_v in v.parents:
3248 if parent_v not in seen:
3249 yield from v_and_parents(parent_v)
3251 def find_language(v: "VNode", phase: int) -> Optional[str]:
3252 """
3253 A helper for all searches.
3254 Phase one searches only @<file> nodes.
3255 """
3256 if phase == 1 and not v.isAnyAtFileNode():
3257 return None
3258 # #1693: Scan v.b for an *unambiguous* @language directive.
3259 languages = g.findAllValidLanguageDirectives(v.b)
3260 if len(languages) == 1: # An unambiguous language
3261 return languages[0]
3262 if v.isAnyAtFileNode():
3263 # Use the file's extension.
3264 name = v.anyAtFileNodeName()
3265 junk, ext = g.os_path_splitext(name)
3266 ext = ext[1:] # strip the leading period.
3267 language = g.app.extension_dict.get(ext)
3268 if g.isValidLanguage(language):
3269 return language
3270 return None
3272 # First, see if p contains any @language directive.
3273 language = g.findFirstValidAtLanguageDirective(p.b)
3274 if language:
3275 return language
3276 #
3277 # Phase 1: search only @<file> nodes: #2308.
3278 # Phase 2: search all nodes.
3279 for phase in (1, 2):
3280 # Search direct parents.
3281 for p2 in p.self_and_parents(copy=False):
3282 language = find_language(p2.v, phase)
3283 if language:
3284 return language
3285 # Search all extended parents.
3286 seen = set([v0.context.hiddenRootNode])
3287 for v in v_and_parents(v0):
3288 language = find_language(v, phase)
3289 if language:
3290 return language
3291 return None
3292#@+node:ekr.20150325075144.1: *3* g.getLanguageFromPosition
3293def getLanguageAtPosition(c: Cmdr, p: Pos) -> str:
3294 """
3295 Return the language in effect at position p.
3296 This is always a lowercase language name, never None.
3297 """
3298 aList = g.get_directives_dict_list(p)
3299 d = g.scanAtCommentAndAtLanguageDirectives(aList)
3300 language = (
3301 d and d.get('language') or
3302 g.getLanguageFromAncestorAtFileNode(p) or
3303 c.config.getString('target-language') or
3304 'python'
3305 )
3306 return language.lower()
3307#@+node:ekr.20031218072017.1386: *3* g.getOutputNewline
3308def getOutputNewline(c: Cmdr=None, name: str=None) -> str:
3309 """Convert the name of a line ending to the line ending itself.
3311 Priority:
3312 - Use name if name given
3313 - Use c.config.output_newline if c given,
3314 - Otherwise use g.app.config.output_newline.
3315 """
3316 if name:
3317 s = name
3318 elif c:
3319 s = c.config.output_newline
3320 else:
3321 s = app.config.output_newline
3322 if not s:
3323 s = ''
3324 s = s.lower()
3325 if s in ("nl", "lf"):
3326 s = '\n'
3327 elif s == "cr":
3328 s = '\r'
3329 elif s == "platform":
3330 s = os.linesep # 12/2/03: emakital
3331 elif s == "crlf":
3332 s = "\r\n"
3333 else:
3334 s = '\n' # Default for erroneous values.
3335 assert isinstance(s, str), repr(s)
3336 return s
3337#@+node:ekr.20200521075143.1: *3* g.inAtNosearch
3338def inAtNosearch(p: Pos) -> bool:
3339 """Return True if p or p's ancestors contain an @nosearch directive."""
3340 if not p:
3341 return False # #2288.
3342 for p in p.self_and_parents():
3343 if p.is_at_ignore() or re.search(r'(^@|\n@)nosearch\b', p.b):
3344 return True
3345 return False
3346#@+node:ekr.20131230090121.16528: *3* g.isDirective
3347def isDirective(s: str) -> bool:
3348 """Return True if s starts with a directive."""
3349 m = g_is_directive_pattern.match(s)
3350 if m:
3351 s2 = s[m.end(1) :]
3352 if s2 and s2[0] in ".(":
3353 return False
3354 return bool(m.group(1) in g.globalDirectiveList)
3355 return False
3356#@+node:ekr.20200810074755.1: *3* g.isValidLanguage
3357def isValidLanguage(language: str) -> bool:
3358 """True if language exists in leo/modes."""
3359 # 2020/08/12: A hack for c++
3360 if language in ('c++', 'cpp'):
3361 language = 'cplusplus'
3362 fn = g.os_path_join(g.app.loadDir, '..', 'modes', f"{language}.py")
3363 return g.os_path_exists(fn)
3364#@+node:ekr.20080827175609.52: *3* g.scanAtCommentAndLanguageDirectives
3365def scanAtCommentAndAtLanguageDirectives(aList: List) -> Optional[Dict[str, str]]:
3366 """
3367 Scan aList for @comment and @language directives.
3369 @comment should follow @language if both appear in the same node.
3370 """
3371 lang = None
3372 for d in aList:
3373 comment = d.get('comment')
3374 language = d.get('language')
3375 # Important: assume @comment follows @language.
3376 if language:
3377 lang, delim1, delim2, delim3 = g.set_language(language, 0)
3378 if comment:
3379 delim1, delim2, delim3 = g.set_delims_from_string(comment)
3380 if comment or language:
3381 delims = delim1, delim2, delim3
3382 d = {'language': lang, 'comment': comment, 'delims': delims}
3383 return d
3384 return None
3385#@+node:ekr.20080827175609.32: *3* g.scanAtEncodingDirectives
3386def scanAtEncodingDirectives(aList: List) -> Optional[str]:
3387 """Scan aList for @encoding directives."""
3388 for d in aList:
3389 encoding = d.get('encoding')
3390 if encoding and g.isValidEncoding(encoding):
3391 return encoding
3392 if encoding and not g.unitTesting:
3393 g.error("invalid @encoding:", encoding)
3394 return None
3395#@+node:ekr.20080827175609.53: *3* g.scanAtHeaderDirectives
3396def scanAtHeaderDirectives(aList: List) -> None:
3397 """scan aList for @header and @noheader directives."""
3398 for d in aList:
3399 if d.get('header') and d.get('noheader'):
3400 g.error("conflicting @header and @noheader directives")
3401#@+node:ekr.20080827175609.33: *3* g.scanAtLineendingDirectives
3402def scanAtLineendingDirectives(aList: List) -> Optional[str]:
3403 """Scan aList for @lineending directives."""
3404 for d in aList:
3405 e = d.get('lineending')
3406 if e in ("cr", "crlf", "lf", "nl", "platform"):
3407 lineending = g.getOutputNewline(name=e)
3408 return lineending
3409 # else:
3410 # g.error("invalid @lineending directive:",e)
3411 return None
3412#@+node:ekr.20080827175609.34: *3* g.scanAtPagewidthDirectives
3413def scanAtPagewidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[int]:
3414 """Scan aList for @pagewidth directives. Return the page width or None"""
3415 for d in aList:
3416 s = d.get('pagewidth')
3417 if s is not None:
3418 i, val = g.skip_long(s, 0)
3419 if val is not None and val > 0:
3420 return val
3421 if issue_error_flag and not g.unitTesting:
3422 g.error("ignoring @pagewidth", s)
3423 return None
3424#@+node:ekr.20101022172109.6108: *3* g.scanAtPathDirectives
3425def scanAtPathDirectives(c: Cmdr, aList: List) -> str:
3426 path = c.scanAtPathDirectives(aList)
3427 return path
3429def scanAllAtPathDirectives(c: Cmdr, p: Pos) -> str:
3430 aList = g.get_directives_dict_list(p)
3431 path = c.scanAtPathDirectives(aList)
3432 return path
3433#@+node:ekr.20080827175609.37: *3* g.scanAtTabwidthDirectives
3434def scanAtTabwidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[int]:
3435 """Scan aList for @tabwidth directives."""
3436 for d in aList:
3437 s = d.get('tabwidth')
3438 if s is not None:
3439 junk, val = g.skip_long(s, 0)
3440 if val not in (None, 0):
3441 return val
3442 if issue_error_flag and not g.unitTesting:
3443 g.error("ignoring @tabwidth", s)
3444 return None
3446def scanAllAtTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[int]:
3447 """Scan p and all ancestors looking for @tabwidth directives."""
3448 if c and p:
3449 aList = g.get_directives_dict_list(p)
3450 val = g.scanAtTabwidthDirectives(aList)
3451 ret = c.tab_width if val is None else val
3452 else:
3453 ret = None
3454 return ret
3455#@+node:ekr.20080831084419.4: *3* g.scanAtWrapDirectives
3456def scanAtWrapDirectives(aList: List, issue_error_flag: bool=False) -> Optional[bool]:
3457 """Scan aList for @wrap and @nowrap directives."""
3458 for d in aList:
3459 if d.get('wrap') is not None:
3460 return True
3461 if d.get('nowrap') is not None:
3462 return False
3463 return None
3465def scanAllAtWrapDirectives(c: Cmdr, p: Pos) -> Optional[bool]:
3466 """Scan p and all ancestors looking for @wrap/@nowrap directives."""
3467 if c and p:
3468 default = bool(c and c.config.getBool("body-pane-wraps"))
3469 aList = g.get_directives_dict_list(p)
3470 val = g.scanAtWrapDirectives(aList)
3471 ret = default if val is None else val
3472 else:
3473 ret = None
3474 return ret
3475#@+node:ekr.20040715155607: *3* g.scanForAtIgnore
3476def scanForAtIgnore(c: Cmdr, p: Pos) -> bool:
3477 """Scan position p and its ancestors looking for @ignore directives."""
3478 if g.unitTesting:
3479 return False # For unit tests.
3480 for p in p.self_and_parents(copy=False):
3481 d = g.get_directives_dict(p)
3482 if 'ignore' in d:
3483 return True
3484 return False
3485#@+node:ekr.20040712084911.1: *3* g.scanForAtLanguage
3486def scanForAtLanguage(c: Cmdr, p: Pos) -> str:
3487 """Scan position p and p's ancestors looking only for @language and @ignore directives.
3489 Returns the language found, or c.target_language."""
3490 # Unlike the code in x.scanAllDirectives, this code ignores @comment directives.
3491 if c and p:
3492 for p in p.self_and_parents(copy=False):
3493 d = g.get_directives_dict(p)
3494 if 'language' in d:
3495 z = d["language"]
3496 language, delim1, delim2, delim3 = g.set_language(z, 0)
3497 return language
3498 return c.target_language
3499#@+node:ekr.20041123094807: *3* g.scanForAtSettings
3500def scanForAtSettings(p: Pos) -> bool:
3501 """Scan position p and its ancestors looking for @settings nodes."""
3502 for p in p.self_and_parents(copy=False):
3503 h = p.h
3504 h = g.app.config.canonicalizeSettingName(h)
3505 if h.startswith("@settings"):
3506 return True
3507 return False
3508#@+node:ekr.20031218072017.1382: *3* g.set_delims_from_language
3509def set_delims_from_language(language: str) -> Tuple[str, str, str]:
3510 """Return a tuple (single,start,end) of comment delims."""
3511 val = g.app.language_delims_dict.get(language)
3512 if val:
3513 delim1, delim2, delim3 = g.set_delims_from_string(val)
3514 if delim2 and not delim3:
3515 return '', delim1, delim2
3516 # 0,1 or 3 params.
3517 return delim1, delim2, delim3
3518 return '', '', '' # Indicate that no change should be made
3519#@+node:ekr.20031218072017.1383: *3* g.set_delims_from_string
3520def set_delims_from_string(s: str) -> Tuple[str, str, str]:
3521 """
3522 Return (delim1, delim2, delim2), the delims following the @comment
3523 directive.
3525 This code can be called from @language logic, in which case s can
3526 point at @comment
3527 """
3528 # Skip an optional @comment
3529 tag = "@comment"
3530 i = 0
3531 if g.match_word(s, i, tag):
3532 i += len(tag)
3533 count = 0
3534 delims = ['', '', '']
3535 while count < 3 and i < len(s):
3536 i = j = g.skip_ws(s, i)
3537 while i < len(s) and not g.is_ws(s[i]) and not g.is_nl(s, i):
3538 i += 1
3539 if j == i:
3540 break
3541 delims[count] = s[j:i] or ''
3542 count += 1
3543 # 'rr 09/25/02
3544 if count == 2: # delims[0] is always the single-line delim.
3545 delims[2] = delims[1]
3546 delims[1] = delims[0]
3547 delims[0] = ''
3548 for i in range(0, 3):
3549 if delims[i]:
3550 if delims[i].startswith("@0x"):
3551 # Allow delimiter definition as @0x + hexadecimal encoded delimiter
3552 # to avoid problems with duplicate delimiters on the @comment line.
3553 # If used, whole delimiter must be encoded.
3554 if len(delims[i]) == 3:
3555 g.warning(f"'{delims[i]}' delimiter is invalid")
3556 return None, None, None
3557 try:
3558 delims[i] = binascii.unhexlify(delims[i][3:]) # type:ignore
3559 delims[i] = g.toUnicode(delims[i])
3560 except Exception as e:
3561 g.warning(f"'{delims[i]}' delimiter is invalid: {e}")
3562 return None, None, None
3563 else:
3564 # 7/8/02: The "REM hack": replace underscores by blanks.
3565 # 9/25/02: The "perlpod hack": replace double underscores by newlines.
3566 delims[i] = delims[i].replace("__", '\n').replace('_', ' ')
3567 return delims[0], delims[1], delims[2]
3568#@+node:ekr.20031218072017.1384: *3* g.set_language
3569def set_language(s: str, i: int, issue_errors_flag: bool=False) -> Tuple:
3570 """Scan the @language directive that appears at s[i:].
3572 The @language may have been stripped away.
3574 Returns (language, delim1, delim2, delim3)
3575 """
3576 tag = "@language"
3577 assert i is not None
3578 if g.match_word(s, i, tag):
3579 i += len(tag)
3580 # Get the argument.
3581 i = g.skip_ws(s, i)
3582 j = i
3583 i = g.skip_c_id(s, i)
3584 # Allow tcl/tk.
3585 arg = s[j:i].lower()
3586 if app.language_delims_dict.get(arg):
3587 language = arg
3588 delim1, delim2, delim3 = g.set_delims_from_language(language)
3589 return language, delim1, delim2, delim3
3590 if issue_errors_flag:
3591 g.es("ignoring:", g.get_line(s, i))
3592 return None, None, None, None
3593#@+node:ekr.20071109165315: *3* g.stripPathCruft
3594def stripPathCruft(path: str) -> str:
3595 """Strip cruft from a path name."""
3596 if not path:
3597 return path # Retain empty paths for warnings.
3598 if len(path) > 2 and (
3599 (path[0] == '<' and path[-1] == '>') or
3600 (path[0] == '"' and path[-1] == '"') or
3601 (path[0] == "'" and path[-1] == "'")
3602 ):
3603 path = path[1:-1].strip()
3604 # We want a *relative* path, not an absolute path.
3605 return path
3606#@+node:ekr.20090214075058.10: *3* g.update_directives_pat
3607def update_directives_pat() -> None:
3608 """Init/update g.directives_pat"""
3609 global globalDirectiveList, directives_pat
3610 # Use a pattern that guarantees word matches.
3611 aList = [
3612 fr"\b{z}\b" for z in globalDirectiveList if z != 'others'
3613 ]
3614 pat = "^@(%s)" % "|".join(aList)
3615 directives_pat = re.compile(pat, re.MULTILINE)
3617# #1688: Initialize g.directives_pat
3618update_directives_pat()
3619#@+node:ekr.20031218072017.3116: ** g.Files & Directories
3620#@+node:ekr.20080606074139.2: *3* g.chdir
3621def chdir(path: str) -> None:
3622 if not g.os_path_isdir(path):
3623 path = g.os_path_dirname(path)
3624 if g.os_path_isdir(path) and g.os_path_exists(path):
3625 os.chdir(path)
3626#@+node:ekr.20120222084734.10287: *3* g.compute...Dir
3627# For compatibility with old code.
3629def computeGlobalConfigDir() -> str:
3630 return g.app.loadManager.computeGlobalConfigDir()
3632def computeHomeDir() -> str:
3633 return g.app.loadManager.computeHomeDir()
3635def computeLeoDir() -> str:
3636 return g.app.loadManager.computeLeoDir()
3638def computeLoadDir() -> str:
3639 return g.app.loadManager.computeLoadDir()
3641def computeMachineName() -> str:
3642 return g.app.loadManager.computeMachineName()
3644def computeStandardDirectories() -> str:
3645 return g.app.loadManager.computeStandardDirectories()
3646#@+node:ekr.20031218072017.3103: *3* g.computeWindowTitle
3647def computeWindowTitle(fileName: str) -> str:
3649 branch, commit = g.gitInfoForFile(fileName) # #1616
3650 if not fileName:
3651 return branch + ": untitled" if branch else 'untitled'
3652 path, fn = g.os_path_split(fileName)
3653 if path:
3654 title = fn + " in " + path
3655 else:
3656 title = fn
3657 # Yet another fix for bug 1194209: regularize slashes.
3658 if os.sep in '/\\':
3659 title = title.replace('/', os.sep).replace('\\', os.sep)
3660 if branch:
3661 title = branch + ": " + title
3662 return title
3663#@+node:ekr.20031218072017.3117: *3* g.create_temp_file
3664def create_temp_file(textMode: bool=False) -> Tuple[Any, str]:
3665 """
3666 Return a tuple (theFile,theFileName)
3668 theFile: a file object open for writing.
3669 theFileName: the name of the temporary file.
3670 """
3671 try:
3672 # fd is an handle to an open file as would be returned by os.open()
3673 fd, theFileName = tempfile.mkstemp(text=textMode)
3674 mode = 'w' if textMode else 'wb'
3675 theFile = os.fdopen(fd, mode)
3676 except Exception:
3677 g.error('unexpected exception in g.create_temp_file')
3678 g.es_exception()
3679 theFile, theFileName = None, ''
3680 return theFile, theFileName
3681#@+node:ekr.20210307060731.1: *3* g.createHiddenCommander
3682def createHiddenCommander(fn: str) -> Cmdr:
3683 """Read the file into a hidden commander (Similar to g.openWithFileName)."""
3684 from leo.core.leoCommands import Commands
3685 c = Commands(fn, gui=g.app.nullGui)
3686 theFile = g.app.loadManager.openAnyLeoFile(fn)
3687 if theFile:
3688 c.fileCommands.openLeoFile( # type:ignore
3689 theFile, fn, readAtFileNodesFlag=True, silent=True)
3690 return c
3691 return None
3692#@+node:vitalije.20170714085545.1: *3* g.defaultLeoFileExtension
3693def defaultLeoFileExtension(c: Cmdr=None) -> str:
3694 conf = c.config if c else g.app.config
3695 return conf.getString('default-leo-extension') or '.leo'
3696#@+node:ekr.20031218072017.3118: *3* g.ensure_extension
3697def ensure_extension(name: str, ext: str) -> str:
3699 theFile, old_ext = g.os_path_splitext(name)
3700 if not name:
3701 return name # don't add to an empty name.
3702 if old_ext in ('.db', '.leo'):
3703 return name
3704 if old_ext and old_ext == ext:
3705 return name
3706 return name + ext
3707#@+node:ekr.20150403150655.1: *3* g.fullPath
3708def fullPath(c: Cmdr, p: Pos, simulate: bool=False) -> str:
3709 """
3710 Return the full path (including fileName) in effect at p. Neither the
3711 path nor the fileName will be created if it does not exist.
3712 """
3713 # Search p and p's parents.
3714 for p in p.self_and_parents(copy=False):
3715 aList = g.get_directives_dict_list(p)
3716 path = c.scanAtPathDirectives(aList)
3717 fn = p.h if simulate else p.anyAtFileNodeName() # Use p.h for unit tests.
3718 if fn:
3719 # Fix #102: expand path expressions.
3720 fn = c.expand_path_expression(fn) # #1341.
3721 fn = os.path.expanduser(fn) # 1900.
3722 return g.os_path_finalize_join(path, fn) # #1341.
3723 return ''
3724#@+node:ekr.20190327192721.1: *3* g.get_files_in_directory
3725def get_files_in_directory(directory: str, kinds: List=None, recursive: bool=True) -> List[str]:
3726 """
3727 Return a list of all files of the given file extensions in the directory.
3728 Default kinds: ['*.py'].
3729 """
3730 files: List[str] = []
3731 sep = os.path.sep
3732 if not g.os.path.exists(directory):
3733 g.es_print('does not exist', directory)
3734 return files
3735 try:
3736 if kinds:
3737 kinds = [z if z.startswith('*') else '*' + z for z in kinds]
3738 else:
3739 kinds = ['*.py']
3740 if recursive:
3741 # Works for all versions of Python.
3742 for root, dirnames, filenames in os.walk(directory):
3743 for kind in kinds:
3744 for filename in fnmatch.filter(filenames, kind):
3745 files.append(os.path.join(root, filename))
3746 else:
3747 for kind in kinds:
3748 files.extend(glob.glob(directory + sep + kind))
3749 return list(set(sorted(files)))
3750 except Exception:
3751 g.es_exception()
3752 return []
3753#@+node:ekr.20031218072017.1264: *3* g.getBaseDirectory
3754# Handles the conventions applying to the "relative_path_base_directory" configuration option.
3756def getBaseDirectory(c: Cmdr) -> str:
3757 """Convert '!' or '.' to proper directory references."""
3758 base = app.config.relative_path_base_directory
3759 if base and base == "!":
3760 base = app.loadDir
3761 elif base and base == ".":
3762 base = c.openDirectory
3763 if base and g.os_path_isabs(base):
3764 # Set c.chdir_to_relative_path as needed.
3765 if not hasattr(c, 'chdir_to_relative_path'):
3766 c.chdir_to_relative_path = c.config.getBool('chdir-to-relative-path')
3767 # Call os.chdir if requested.
3768 if c.chdir_to_relative_path:
3769 os.chdir(base)
3770 return base # base need not exist yet.
3771 return "" # No relative base given.
3772#@+node:ekr.20170223093758.1: *3* g.getEncodingAt
3773def getEncodingAt(p: Pos, s: bytes=None) -> str:
3774 """
3775 Return the encoding in effect at p and/or for string s.
3777 Read logic: s is not None.
3778 Write logic: s is None.
3779 """
3780 # A BOM overrides everything.
3781 if s:
3782 e, junk_s = g.stripBOM(s)
3783 if e:
3784 return e
3785 aList = g.get_directives_dict_list(p)
3786 e = g.scanAtEncodingDirectives(aList)
3787 if s and s.strip() and not e:
3788 e = 'utf-8'
3789 return e
3790#@+node:ville.20090701144325.14942: *3* g.guessExternalEditor
3791def guessExternalEditor(c: Cmdr=None) -> Optional[str]:
3792 """ Return a 'sensible' external editor """
3793 editor = (
3794 os.environ.get("LEO_EDITOR") or
3795 os.environ.get("EDITOR") or
3796 g.app.db and g.app.db.get("LEO_EDITOR") or
3797 c and c.config.getString('external-editor'))
3798 if editor:
3799 return editor
3800 # fallbacks
3801 platform = sys.platform.lower()
3802 if platform.startswith('win'):
3803 return "notepad"
3804 if platform.startswith('linux'):
3805 return 'gedit'
3806 g.es(
3807 '''No editor set.
3808Please set LEO_EDITOR or EDITOR environment variable,
3809or do g.app.db['LEO_EDITOR'] = "gvim"''',
3810 )
3811 return None
3812#@+node:ekr.20160330204014.1: *3* g.init_dialog_folder
3813def init_dialog_folder(c: Cmdr, p: Pos, use_at_path: bool=True) -> str:
3814 """Return the most convenient folder to open or save a file."""
3815 if c and p and use_at_path:
3816 path = g.fullPath(c, p)
3817 if path:
3818 dir_ = g.os_path_dirname(path)
3819 if dir_ and g.os_path_exists(dir_):
3820 return dir_
3821 table = (
3822 ('c.last_dir', c and c.last_dir),
3823 ('os.curdir', g.os_path_abspath(os.curdir)),
3824 )
3825 for kind, dir_ in table:
3826 if dir_ and g.os_path_exists(dir_):
3827 return dir_
3828 return ''
3829#@+node:ekr.20100329071036.5744: *3* g.is_binary_file/external_file/string
3830def is_binary_file(f: Any) -> bool:
3831 return f and isinstance(f, io.BufferedIOBase)
3833def is_binary_external_file(fileName: str) -> bool:
3834 try:
3835 with open(fileName, 'rb') as f:
3836 s = f.read(1024) # bytes, in Python 3.
3837 return g.is_binary_string(s)
3838 except IOError:
3839 return False
3840 except Exception:
3841 g.es_exception()
3842 return False
3844def is_binary_string(s: str) -> bool:
3845 # http://stackoverflow.com/questions/898669
3846 # aList is a list of all non-binary characters.
3847 aList = [7, 8, 9, 10, 12, 13, 27] + list(range(0x20, 0x100))
3848 return bool(s.translate(None, bytes(aList))) # type:ignore
3849#@+node:EKR.20040504154039: *3* g.is_sentinel
3850def is_sentinel(line: str, delims: Sequence) -> bool:
3851 """Return True if line starts with a sentinel comment."""
3852 delim1, delim2, delim3 = delims
3853 line = line.lstrip()
3854 if delim1:
3855 return line.startswith(delim1 + '@')
3856 if delim2 and delim3:
3857 i = line.find(delim2 + '@')
3858 j = line.find(delim3)
3859 return 0 == i < j
3860 g.error(f"is_sentinel: can not happen. delims: {repr(delims)}")
3861 return False
3862#@+node:ekr.20031218072017.3119: *3* g.makeAllNonExistentDirectories
3863def makeAllNonExistentDirectories(theDir: str) -> Optional[str]:
3864 """
3865 A wrapper from os.makedirs.
3866 Attempt to make all non-existent directories.
3868 Return True if the directory exists or was created successfully.
3869 """
3870 # Return True if the directory already exists.
3871 theDir = g.os_path_normpath(theDir)
3872 ok = g.os_path_isdir(theDir) and g.os_path_exists(theDir)
3873 if ok:
3874 return theDir
3875 # #1450: Create the directory with os.makedirs.
3876 try:
3877 os.makedirs(theDir, mode=0o777, exist_ok=False)
3878 return theDir
3879 except Exception:
3880 return None
3881#@+node:ekr.20071114113736: *3* g.makePathRelativeTo
3882def makePathRelativeTo(fullPath: str, basePath: str) -> str:
3883 if fullPath.startswith(basePath):
3884 s = fullPath[len(basePath) :]
3885 if s.startswith(os.path.sep):
3886 s = s[len(os.path.sep) :]
3887 return s
3888 return fullPath
3889#@+node:ekr.20090520055433.5945: *3* g.openWithFileName
3890def openWithFileName(fileName: str, old_c: Cmdr=None, gui: str=None) -> Cmdr:
3891 """
3892 Create a Leo Frame for the indicated fileName if the file exists.
3894 Return the commander of the newly-opened outline.
3895 """
3896 return g.app.loadManager.loadLocalFile(fileName, gui, old_c)
3897#@+node:ekr.20150306035851.7: *3* g.readFileIntoEncodedString
3898def readFileIntoEncodedString(fn: str, silent: bool=False) -> bytes:
3899 """Return the raw contents of the file whose full path is fn."""
3900 try:
3901 with open(fn, 'rb') as f:
3902 return f.read()
3903 except IOError:
3904 if not silent:
3905 g.error('can not open', fn)
3906 except Exception:
3907 if not silent:
3908 g.error(f"readFileIntoEncodedString: exception reading {fn}")
3909 g.es_exception()
3910 return None
3911#@+node:ekr.20100125073206.8710: *3* g.readFileIntoString
3912def readFileIntoString(
3913 fileName: str,
3914 encoding: str='utf-8', # BOM may override this.
3915 kind: str=None, # @file, @edit, ...
3916 verbose: bool=True,
3917) -> Tuple[Any, Any]:
3918 """
3919 Return the contents of the file whose full path is fileName.
3921 Return (s,e)
3922 s is the string, converted to unicode, or None if there was an error.
3923 e is the encoding of s, computed in the following order:
3924 - The BOM encoding if the file starts with a BOM mark.
3925 - The encoding given in the # -*- coding: utf-8 -*- line for python files.
3926 - The encoding given by the 'encoding' keyword arg.
3927 - None, which typically means 'utf-8'.
3928 """
3929 if not fileName:
3930 if verbose:
3931 g.trace('no fileName arg given')
3932 return None, None
3933 if g.os_path_isdir(fileName):
3934 if verbose:
3935 g.trace('not a file:', fileName)
3936 return None, None
3937 if not g.os_path_exists(fileName):
3938 if verbose:
3939 g.error('file not found:', fileName)
3940 return None, None
3941 try:
3942 e = None
3943 with open(fileName, 'rb') as f:
3944 s = f.read()
3945 # Fix #391.
3946 if not s:
3947 return '', None
3948 # New in Leo 4.11: check for unicode BOM first.
3949 e, s = g.stripBOM(s)
3950 if not e:
3951 # Python's encoding comments override everything else.
3952 junk, ext = g.os_path_splitext(fileName)
3953 if ext == '.py':
3954 e = g.getPythonEncodingFromString(s)
3955 s = g.toUnicode(s, encoding=e or encoding)
3956 return s, e
3957 except IOError:
3958 # Translate 'can not open' and kind, but not fileName.
3959 if verbose:
3960 g.error('can not open', '', (kind or ''), fileName)
3961 except Exception:
3962 g.error(f"readFileIntoString: unexpected exception reading {fileName}")
3963 g.es_exception()
3964 return None, None
3965#@+node:ekr.20160504062833.1: *3* g.readFileToUnicodeString
3966def readFileIntoUnicodeString(fn: str, encoding: Optional[str]=None, silent: bool=False) -> Optional[str]:
3967 """Return the raw contents of the file whose full path is fn."""
3968 try:
3969 with open(fn, 'rb') as f:
3970 s = f.read()
3971 return g.toUnicode(s, encoding=encoding)
3972 except IOError:
3973 if not silent:
3974 g.error('can not open', fn)
3975 except Exception:
3976 g.error(f"readFileIntoUnicodeString: unexpected exception reading {fn}")
3977 g.es_exception()
3978 return None
3979#@+node:ekr.20031218072017.3120: *3* g.readlineForceUnixNewline
3980#@+at Stephen P. Schaefer 9/7/2002
3981#
3982# The Unix readline() routine delivers "\r\n" line end strings verbatim,
3983# while the windows versions force the string to use the Unix convention
3984# of using only "\n". This routine causes the Unix readline to do the
3985# same.
3986#@@c
3988def readlineForceUnixNewline(f: Any, fileName: Optional[str]=None) -> str:
3989 try:
3990 s = f.readline()
3991 except UnicodeDecodeError:
3992 g.trace(f"UnicodeDecodeError: {fileName}", f, g.callers())
3993 s = ''
3994 if len(s) >= 2 and s[-2] == "\r" and s[-1] == "\n":
3995 s = s[0:-2] + "\n"
3996 return s
3997#@+node:ekr.20031218072017.3124: *3* g.sanitize_filename
3998def sanitize_filename(s: str) -> str:
3999 """
4000 Prepares string s to be a valid file name:
4002 - substitute '_' for whitespace and special path characters.
4003 - eliminate all other non-alphabetic characters.
4004 - convert double quotes to single quotes.
4005 - strip leading and trailing whitespace.
4006 - return at most 128 characters.
4007 """
4008 result = []
4009 for ch in s:
4010 if ch in string.ascii_letters:
4011 result.append(ch)
4012 elif ch == '\t':
4013 result.append(' ')
4014 elif ch == '"':
4015 result.append("'")
4016 elif ch in '\\/:|<>*:._':
4017 result.append('_')
4018 s = ''.join(result).strip()
4019 while len(s) > 1:
4020 n = len(s)
4021 s = s.replace('__', '_')
4022 if len(s) == n:
4023 break
4024 return s[:128]
4025#@+node:ekr.20060328150113: *3* g.setGlobalOpenDir
4026def setGlobalOpenDir(fileName: str) -> None:
4027 if fileName:
4028 g.app.globalOpenDir = g.os_path_dirname(fileName)
4029 # g.es('current directory:',g.app.globalOpenDir)
4030#@+node:ekr.20031218072017.3125: *3* g.shortFileName & shortFilename
4031def shortFileName(fileName: str, n: int=None) -> str:
4032 """Return the base name of a path."""
4033 if n is not None:
4034 g.trace('"n" keyword argument is no longer used')
4035 return g.os_path_basename(fileName) if fileName else ''
4037shortFilename = shortFileName
4038#@+node:ekr.20150610125813.1: *3* g.splitLongFileName
4039def splitLongFileName(fn: str, limit: int=40) -> str:
4040 """Return fn, split into lines at slash characters."""
4041 aList = fn.replace('\\', '/').split('/')
4042 n, result = 0, []
4043 for i, s in enumerate(aList):
4044 n += len(s)
4045 result.append(s)
4046 if i + 1 < len(aList):
4047 result.append('/')
4048 n += 1
4049 if n > limit:
4050 result.append('\n')
4051 n = 0
4052 return ''.join(result)
4053#@+node:ekr.20190114061452.26: *3* g.writeFile
4054def writeFile(contents: Union[bytes, str], encoding: str, fileName: str) -> bool:
4055 """Create a file with the given contents."""
4056 try:
4057 if isinstance(contents, str):
4058 contents = g.toEncodedString(contents, encoding=encoding)
4059 # 'wb' preserves line endings.
4060 with open(fileName, 'wb') as f:
4061 f.write(contents) # type:ignore
4062 return True
4063 except Exception as e:
4064 print(f"exception writing: {fileName}:\n{e}")
4065 # g.trace(g.callers())
4066 # g.es_exception()
4067 return False
4068#@+node:ekr.20031218072017.3151: ** g.Finding & Scanning
4069#@+node:ekr.20140602083643.17659: *3* g.find_word
4070def find_word(s: str, word: str, i: int=0) -> int:
4071 """
4072 Return the index of the first occurance of word in s, or -1 if not found.
4074 g.find_word is *not* the same as s.find(i,word);
4075 g.find_word ensures that only word-matches are reported.
4076 """
4077 while i < len(s):
4078 progress = i
4079 i = s.find(word, i)
4080 if i == -1:
4081 return -1
4082 # Make sure we are at the start of a word.
4083 if i > 0:
4084 ch = s[i - 1]
4085 if ch == '_' or ch.isalnum():
4086 i += len(word)
4087 continue
4088 if g.match_word(s, i, word):
4089 return i
4090 i += len(word)
4091 assert progress < i
4092 return -1
4093#@+node:ekr.20211029090118.1: *3* g.findAncestorVnodeByPredicate
4094def findAncestorVnodeByPredicate(p: Pos, v_predicate: Any) -> Optional["VNode"]:
4095 """
4096 Return first ancestor vnode matching the predicate.
4098 The predicate must must be a function of a single vnode argument.
4099 """
4100 if not p:
4101 return None
4102 # First, look up the tree.
4103 for p2 in p.self_and_parents():
4104 if v_predicate(p2.v):
4105 return p2.v
4106 # Look at parents of all cloned nodes.
4107 if not p.isCloned():
4108 return None
4109 seen = [] # vnodes that have already been searched.
4110 parents = p.v.parents[:] # vnodes to be searched.
4111 while parents:
4112 parent_v = parents.pop()
4113 if parent_v in seen:
4114 continue
4115 seen.append(parent_v)
4116 if v_predicate(parent_v):
4117 return parent_v
4118 for grand_parent_v in parent_v.parents:
4119 if grand_parent_v not in seen:
4120 parents.append(grand_parent_v)
4121 return None
4122#@+node:ekr.20170220103251.1: *3* g.findRootsWithPredicate
4123def findRootsWithPredicate(c: Cmdr, root: Pos, predicate: Callable=None) -> List[Pos]:
4124 """
4125 Commands often want to find one or more **roots**, given a position p.
4126 A root is the position of any node matching a predicate.
4128 This function formalizes the search order used by the black,
4129 pylint, pyflakes and the rst3 commands, returning a list of zero
4130 or more found roots.
4131 """
4132 seen = []
4133 roots = []
4134 if predicate is None:
4136 # A useful default predicate for python.
4137 # pylint: disable=function-redefined
4139 def predicate(p: Pos) -> bool:
4140 return p.isAnyAtFileNode() and p.h.strip().endswith('.py')
4142 # 1. Search p's tree.
4143 for p in root.self_and_subtree(copy=False):
4144 if predicate(p) and p.v not in seen:
4145 seen.append(p.v)
4146 roots.append(p.copy())
4147 if roots:
4148 return roots
4149 # 2. Look up the tree.
4150 for p in root.parents():
4151 if predicate(p):
4152 return [p.copy()]
4153 # 3. Expand the search if root is a clone.
4154 clones = []
4155 for p in root.self_and_parents(copy=False):
4156 if p.isCloned():
4157 clones.append(p.v)
4158 if clones:
4159 for p in c.all_positions(copy=False):
4160 if predicate(p):
4161 # Match if any node in p's tree matches any clone.
4162 for p2 in p.self_and_subtree():
4163 if p2.v in clones:
4164 return [p.copy()]
4165 return []
4166#@+node:ekr.20031218072017.3156: *3* g.scanError
4167# It is dubious to bump the Tangle error count here, but it really doesn't hurt.
4169def scanError(s: str) -> None:
4170 """Bump the error count in the tangle command."""
4171 # New in Leo 4.4b1: just set this global.
4172 g.app.scanErrors += 1
4173 g.es('', s)
4174#@+node:ekr.20031218072017.3157: *3* g.scanf
4175# A quick and dirty sscanf. Understands only %s and %d.
4177def scanf(s: str, pat: str) -> List[str]:
4178 count = pat.count("%s") + pat.count("%d")
4179 pat = pat.replace("%s", r"(\S+)")
4180 pat = pat.replace("%d", r"(\d+)")
4181 parts = re.split(pat, s)
4182 result: List[str] = []
4183 for part in parts:
4184 if part and len(result) < count:
4185 result.append(part)
4186 return result
4187#@+node:ekr.20031218072017.3158: *3* g.Scanners: calling scanError
4188#@+at These scanners all call g.scanError() directly or indirectly, so they
4189# will call g.es if they find an error. g.scanError() also bumps
4190# c.tangleCommands.errors, which is harmless if we aren't tangling, and
4191# useful if we are.
4192#
4193# These routines are called by the Import routines and the Tangle routines.
4194#@+node:ekr.20031218072017.3159: *4* g.skip_block_comment
4195# Scans past a block comment (an old_style C comment).
4197def skip_block_comment(s: str, i: int) -> int:
4198 assert g.match(s, i, "/*")
4199 j = i
4200 i += 2
4201 n = len(s)
4202 k = s.find("*/", i)
4203 if k == -1:
4204 g.scanError("Run on block comment: " + s[j:i])
4205 return n
4206 return k + 2
4207#@+node:ekr.20031218072017.3160: *4* g.skip_braces
4208#@+at This code is called only from the import logic, so we are allowed to
4209# try some tricks. In particular, we assume all braces are matched in
4210# if blocks.
4211#@@c
4213def skip_braces(s: str, i: int) -> int:
4214 """
4215 Skips from the opening to the matching brace.
4217 If no matching is found i is set to len(s)
4218 """
4219 # start = g.get_line(s,i)
4220 assert g.match(s, i, '{')
4221 level = 0
4222 n = len(s)
4223 while i < n:
4224 c = s[i]
4225 if c == '{':
4226 level += 1
4227 i += 1
4228 elif c == '}':
4229 level -= 1
4230 if level <= 0:
4231 return i
4232 i += 1
4233 elif c == '\'' or c == '"':
4234 i = g.skip_string(s, i)
4235 elif g.match(s, i, '//'):
4236 i = g.skip_to_end_of_line(s, i)
4237 elif g.match(s, i, '/*'):
4238 i = g.skip_block_comment(s, i)
4239 # 7/29/02: be more careful handling conditional code.
4240 elif (
4241 g.match_word(s, i, "#if") or
4242 g.match_word(s, i, "#ifdef") or
4243 g.match_word(s, i, "#ifndef")
4244 ):
4245 i, delta = g.skip_pp_if(s, i)
4246 level += delta
4247 else: i += 1
4248 return i
4249#@+node:ekr.20031218072017.3162: *4* g.skip_parens
4250def skip_parens(s: str, i: int) -> int:
4251 """
4252 Skips from the opening ( to the matching ).
4254 If no matching is found i is set to len(s).
4255 """
4256 level = 0
4257 n = len(s)
4258 assert g.match(s, i, '('), repr(s[i])
4259 while i < n:
4260 c = s[i]
4261 if c == '(':
4262 level += 1
4263 i += 1
4264 elif c == ')':
4265 level -= 1
4266 if level <= 0:
4267 return i
4268 i += 1
4269 elif c == '\'' or c == '"':
4270 i = g.skip_string(s, i)
4271 elif g.match(s, i, "//"):
4272 i = g.skip_to_end_of_line(s, i)
4273 elif g.match(s, i, "/*"):
4274 i = g.skip_block_comment(s, i)
4275 else:
4276 i += 1
4277 return i
4278#@+node:ekr.20031218072017.3163: *4* g.skip_pascal_begin_end
4279def skip_pascal_begin_end(s: str, i: int) -> int:
4280 """
4281 Skips from begin to matching end.
4282 If found, i points to the end. Otherwise, i >= len(s)
4283 The end keyword matches begin, case, class, record, and try.
4284 """
4285 assert g.match_c_word(s, i, "begin")
4286 level = 1
4287 i = g.skip_c_id(s, i) # Skip the opening begin.
4288 while i < len(s):
4289 ch = s[i]
4290 if ch == '{':
4291 i = g.skip_pascal_braces(s, i)
4292 elif ch == '"' or ch == '\'':
4293 i = g.skip_pascal_string(s, i)
4294 elif g.match(s, i, "//"):
4295 i = g.skip_line(s, i)
4296 elif g.match(s, i, "(*"):
4297 i = g.skip_pascal_block_comment(s, i)
4298 elif g.match_c_word(s, i, "end"):
4299 level -= 1
4300 if level == 0:
4301 return i
4302 i = g.skip_c_id(s, i)
4303 elif g.is_c_id(ch):
4304 j = i
4305 i = g.skip_c_id(s, i)
4306 name = s[j:i]
4307 if name in ["begin", "case", "class", "record", "try"]:
4308 level += 1
4309 else:
4310 i += 1
4311 return i
4312#@+node:ekr.20031218072017.3164: *4* g.skip_pascal_block_comment
4313def skip_pascal_block_comment(s: str, i: int) -> int:
4314 """Scan past a pascal comment delimited by (* and *)."""
4315 j = i
4316 assert g.match(s, i, "(*")
4317 i = s.find("*)", i)
4318 if i > -1:
4319 return i + 2
4320 g.scanError("Run on comment" + s[j:i])
4321 return len(s)
4322#@+node:ekr.20031218072017.3165: *4* g.skip_pascal_string
4323def skip_pascal_string(s: str, i: int) -> int:
4324 j = i
4325 delim = s[i]
4326 i += 1
4327 assert delim == '"' or delim == '\''
4328 while i < len(s):
4329 if s[i] == delim:
4330 return i + 1
4331 i += 1
4332 g.scanError("Run on string: " + s[j:i])
4333 return i
4334#@+node:ekr.20031218072017.3166: *4* g.skip_heredoc_string
4335def skip_heredoc_string(s: str, i: int) -> int:
4336 """
4337 08-SEP-2002 DTHEIN.
4338 A heredoc string in PHP looks like:
4340 <<<EOS
4341 This is my string.
4342 It is mine. I own it.
4343 No one else has it.
4344 EOS
4346 It begins with <<< plus a token (naming same as PHP variable names).
4347 It ends with the token on a line by itself (must start in first position.
4348 """
4349 j = i
4350 assert g.match(s, i, "<<<")
4351 m = re.match(r"\<\<\<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)", s[i:])
4352 if m is None:
4353 i += 3
4354 return i
4355 # 14-SEP-2002 DTHEIN: needed to add \n to find word, not just string
4356 delim = m.group(1) + '\n'
4357 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: look after \n, not before
4358 n = len(s)
4359 while i < n and not g.match(s, i, delim):
4360 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: move past \n
4361 if i >= n:
4362 g.scanError("Run on string: " + s[j:i])
4363 elif g.match(s, i, delim):
4364 i += len(delim)
4365 return i
4366#@+node:ekr.20031218072017.3167: *4* g.skip_pp_directive
4367def skip_pp_directive(s: str, i: int) -> int:
4368 """Now handles continuation lines and block comments."""
4369 while i < len(s):
4370 if g.is_nl(s, i):
4371 if g.escaped(s, i):
4372 i = g.skip_nl(s, i)
4373 else:
4374 break
4375 elif g.match(s, i, "//"):
4376 i = g.skip_to_end_of_line(s, i)
4377 elif g.match(s, i, "/*"):
4378 i = g.skip_block_comment(s, i)
4379 else:
4380 i += 1
4381 return i
4382#@+node:ekr.20031218072017.3168: *4* g.skip_pp_if
4383# Skips an entire if or if def statement, including any nested statements.
4385def skip_pp_if(s: str, i: int) -> Tuple[int, int]:
4386 start_line = g.get_line(s, i) # used for error messages.
4387 assert(
4388 g.match_word(s, i, "#if") or
4389 g.match_word(s, i, "#ifdef") or
4390 g.match_word(s, i, "#ifndef"))
4391 i = g.skip_line(s, i)
4392 i, delta1 = g.skip_pp_part(s, i)
4393 i = g.skip_ws(s, i)
4394 if g.match_word(s, i, "#else"):
4395 i = g.skip_line(s, i)
4396 i = g.skip_ws(s, i)
4397 i, delta2 = g.skip_pp_part(s, i)
4398 if delta1 != delta2:
4399 g.es("#if and #else parts have different braces:", start_line)
4400 i = g.skip_ws(s, i)
4401 if g.match_word(s, i, "#endif"):
4402 i = g.skip_line(s, i)
4403 else:
4404 g.es("no matching #endif:", start_line)
4405 return i, delta1
4406#@+node:ekr.20031218072017.3169: *4* g.skip_pp_part
4407# Skip to an #else or #endif. The caller has eaten the #if, #ifdef, #ifndef or #else
4409def skip_pp_part(s: str, i: int) -> Tuple[int, int]:
4411 delta = 0
4412 while i < len(s):
4413 c = s[i]
4414 if (
4415 g.match_word(s, i, "#if") or
4416 g.match_word(s, i, "#ifdef") or
4417 g.match_word(s, i, "#ifndef")
4418 ):
4419 i, delta1 = g.skip_pp_if(s, i)
4420 delta += delta1
4421 elif g.match_word(s, i, "#else") or g.match_word(s, i, "#endif"):
4422 return i, delta
4423 elif c == '\'' or c == '"':
4424 i = g.skip_string(s, i)
4425 elif c == '{':
4426 delta += 1
4427 i += 1
4428 elif c == '}':
4429 delta -= 1
4430 i += 1
4431 elif g.match(s, i, "//"):
4432 i = g.skip_line(s, i)
4433 elif g.match(s, i, "/*"):
4434 i = g.skip_block_comment(s, i)
4435 else:
4436 i += 1
4437 return i, delta
4438#@+node:ekr.20031218072017.3171: *4* g.skip_to_semicolon
4439# Skips to the next semicolon that is not in a comment or a string.
4441def skip_to_semicolon(s: str, i: int) -> int:
4442 n = len(s)
4443 while i < n:
4444 c = s[i]
4445 if c == ';':
4446 return i
4447 if c == '\'' or c == '"':
4448 i = g.skip_string(s, i)
4449 elif g.match(s, i, "//"):
4450 i = g.skip_to_end_of_line(s, i)
4451 elif g.match(s, i, "/*"):
4452 i = g.skip_block_comment(s, i)
4453 else:
4454 i += 1
4455 return i
4456#@+node:ekr.20031218072017.3172: *4* g.skip_typedef
4457def skip_typedef(s: str, i: int) -> int:
4458 n = len(s)
4459 while i < n and g.is_c_id(s[i]):
4460 i = g.skip_c_id(s, i)
4461 i = g.skip_ws_and_nl(s, i)
4462 if g.match(s, i, '{'):
4463 i = g.skip_braces(s, i)
4464 i = g.skip_to_semicolon(s, i)
4465 return i
4466#@+node:ekr.20201127143342.1: *3* g.see_more_lines
4467def see_more_lines(s: str, ins: int, n: int=4) -> int:
4468 """
4469 Extend index i within string s to include n more lines.
4470 """
4471 # Show more lines, if they exist.
4472 if n > 0:
4473 for z in range(n):
4474 if ins >= len(s):
4475 break
4476 i, j = g.getLine(s, ins)
4477 ins = j
4478 return max(0, min(ins, len(s)))
4479#@+node:ekr.20031218072017.3195: *3* g.splitLines
4480def splitLines(s: str) -> List[str]:
4481 """
4482 Split s into lines, preserving the number of lines and
4483 the endings of all lines, including the last line.
4484 """
4485 return s.splitlines(True) if s else [] # This is a Python string function!
4487splitlines = splitLines
4488#@+node:ekr.20031218072017.3173: *3* Scanners: no error messages
4489#@+node:ekr.20031218072017.3174: *4* g.escaped
4490# Returns True if s[i] is preceded by an odd number of backslashes.
4492def escaped(s: str, i: int) -> bool:
4493 count = 0
4494 while i - 1 >= 0 and s[i - 1] == '\\':
4495 count += 1
4496 i -= 1
4497 return (count % 2) == 1
4498#@+node:ekr.20031218072017.3175: *4* g.find_line_start
4499def find_line_start(s: str, i: int) -> int:
4500 """Return the index in s of the start of the line containing s[i]."""
4501 if i < 0:
4502 return 0 # New in Leo 4.4.5: add this defensive code.
4503 # bug fix: 11/2/02: change i to i+1 in rfind
4504 i = s.rfind('\n', 0, i + 1) # Finds the highest index in the range.
4505 return 0 if i == -1 else i + 1
4506#@+node:ekr.20031218072017.3176: *4* g.find_on_line
4507def find_on_line(s: str, i: int, pattern: str) -> int:
4508 j = s.find('\n', i)
4509 if j == -1:
4510 j = len(s)
4511 k = s.find(pattern, i, j)
4512 return k
4513#@+node:ekr.20031218072017.3179: *4* g.is_special
4514def is_special(s: str, directive: str) -> Tuple[bool, int]:
4515 """Return True if the body text contains the @ directive."""
4516 assert(directive and directive[0] == '@')
4517 # Most directives must start the line.
4518 lws = directive in ("@others", "@all")
4519 pattern_s = r'^\s*(%s\b)' if lws else r'^(%s\b)'
4520 pattern = re.compile(pattern_s % directive, re.MULTILINE)
4521 m = re.search(pattern, s)
4522 if m:
4523 return True, m.start(1)
4524 return False, -1
4525#@+node:ekr.20031218072017.3177: *4* g.is_c_id
4526def is_c_id(ch: str) -> bool:
4527 return g.isWordChar(ch)
4528#@+node:ekr.20031218072017.3178: *4* g.is_nl
4529def is_nl(s: str, i: int) -> bool:
4530 return i < len(s) and (s[i] == '\n' or s[i] == '\r')
4531#@+node:ekr.20031218072017.3180: *4* g.is_ws & is_ws_or_nl
4532def is_ws(ch: str) -> bool:
4533 return ch == '\t' or ch == ' '
4535def is_ws_or_nl(s: str, i: int) -> bool:
4536 return g.is_nl(s, i) or (i < len(s) and g.is_ws(s[i]))
4537#@+node:ekr.20031218072017.3181: *4* g.match
4538# Warning: this code makes no assumptions about what follows pattern.
4540def match(s: str, i: int, pattern: str) -> bool:
4541 return bool(s and pattern and s.find(pattern, i, i + len(pattern)) == i)
4542#@+node:ekr.20031218072017.3182: *4* g.match_c_word
4543def match_c_word(s: str, i: int, name: str) -> bool:
4544 n = len(name)
4545 return bool(
4546 name and
4547 name == s[i : i + n] and
4548 (i + n == len(s) or not g.is_c_id(s[i + n]))
4549 )
4550#@+node:ekr.20031218072017.3183: *4* g.match_ignoring_case
4551def match_ignoring_case(s1: str, s2: str) -> bool:
4552 return bool(s1 and s2 and s1.lower() == s2.lower())
4553#@+node:ekr.20031218072017.3184: *4* g.match_word & g.match_words
4554def match_word(s: str, i: int, pattern: str) -> bool:
4556 # Using a regex is surprisingly tricky.
4557 if pattern is None:
4558 return False
4559 if i > 0 and g.isWordChar(s[i - 1]): # Bug fix: 2017/06/01.
4560 return False
4561 j = len(pattern)
4562 if j == 0:
4563 return False
4564 if s.find(pattern, i, i + j) != i:
4565 return False
4566 if i + j >= len(s):
4567 return True
4568 ch = s[i + j]
4569 return not g.isWordChar(ch)
4571def match_words(s: str, i: int, patterns: Sequence[str]) -> bool:
4572 return any(g.match_word(s, i, pattern) for pattern in patterns)
4573#@+node:ekr.20031218072017.3185: *4* g.skip_blank_lines
4574# This routine differs from skip_ws_and_nl in that
4575# it does not advance over whitespace at the start
4576# of a non-empty or non-nl terminated line
4578def skip_blank_lines(s: str, i: int) -> int:
4579 while i < len(s):
4580 if g.is_nl(s, i):
4581 i = g.skip_nl(s, i)
4582 elif g.is_ws(s[i]):
4583 j = g.skip_ws(s, i)
4584 if g.is_nl(s, j):
4585 i = j
4586 else: break
4587 else: break
4588 return i
4589#@+node:ekr.20031218072017.3186: *4* g.skip_c_id
4590def skip_c_id(s: str, i: int) -> int:
4591 n = len(s)
4592 while i < n and g.isWordChar(s[i]):
4593 i += 1
4594 return i
4595#@+node:ekr.20040705195048: *4* g.skip_id
4596def skip_id(s: str, i: int, chars: str=None) -> int:
4597 chars = g.toUnicode(chars) if chars else ''
4598 n = len(s)
4599 while i < n and (g.isWordChar(s[i]) or s[i] in chars):
4600 i += 1
4601 return i
4602#@+node:ekr.20031218072017.3187: *4* g.skip_line, skip_to_start/end_of_line
4603#@+at These methods skip to the next newline, regardless of whether the
4604# newline may be preceeded by a backslash. Consequently, they should be
4605# used only when we know that we are not in a preprocessor directive or
4606# string.
4607#@@c
4609def skip_line(s: str, i: int) -> int:
4610 if i >= len(s):
4611 return len(s)
4612 if i < 0:
4613 i = 0
4614 i = s.find('\n', i)
4615 if i == -1:
4616 return len(s)
4617 return i + 1
4619def skip_to_end_of_line(s: str, i: int) -> int:
4620 if i >= len(s):
4621 return len(s)
4622 if i < 0:
4623 i = 0
4624 i = s.find('\n', i)
4625 if i == -1:
4626 return len(s)
4627 return i
4629def skip_to_start_of_line(s: str, i: int) -> int:
4630 if i >= len(s):
4631 return len(s)
4632 if i <= 0:
4633 return 0
4634 # Don't find s[i], so it doesn't matter if s[i] is a newline.
4635 i = s.rfind('\n', 0, i)
4636 if i == -1:
4637 return 0
4638 return i + 1
4639#@+node:ekr.20031218072017.3188: *4* g.skip_long
4640def skip_long(s: str, i: int) -> Tuple[int, Optional[int]]:
4641 """
4642 Scan s[i:] for a valid int.
4643 Return (i, val) or (i, None) if s[i] does not point at a number.
4644 """
4645 val = 0
4646 i = g.skip_ws(s, i)
4647 n = len(s)
4648 if i >= n or (not s[i].isdigit() and s[i] not in '+-'):
4649 return i, None
4650 j = i
4651 if s[i] in '+-': # Allow sign before the first digit
4652 i += 1
4653 while i < n and s[i].isdigit():
4654 i += 1
4655 try: # There may be no digits.
4656 val = int(s[j:i])
4657 return i, val
4658 except Exception:
4659 return i, None
4660#@+node:ekr.20031218072017.3190: *4* g.skip_nl
4661# We need this function because different systems have different end-of-line conventions.
4663def skip_nl(s: str, i: int) -> int:
4664 """Skips a single "logical" end-of-line character."""
4665 if g.match(s, i, "\r\n"):
4666 return i + 2
4667 if g.match(s, i, '\n') or g.match(s, i, '\r'):
4668 return i + 1
4669 return i
4670#@+node:ekr.20031218072017.3191: *4* g.skip_non_ws
4671def skip_non_ws(s: str, i: int) -> int:
4672 n = len(s)
4673 while i < n and not g.is_ws(s[i]):
4674 i += 1
4675 return i
4676#@+node:ekr.20031218072017.3192: *4* g.skip_pascal_braces
4677# Skips from the opening { to the matching }.
4679def skip_pascal_braces(s: str, i: int) -> int:
4680 # No constructs are recognized inside Pascal block comments!
4681 if i == -1:
4682 return len(s)
4683 return s.find('}', i)
4684#@+node:ekr.20031218072017.3170: *4* g.skip_python_string
4685def skip_python_string(s: str, i: int) -> int:
4686 if g.match(s, i, "'''") or g.match(s, i, '"""'):
4687 delim = s[i] * 3
4688 i += 3
4689 k = s.find(delim, i)
4690 if k > -1:
4691 return k + 3
4692 return len(s)
4693 return g.skip_string(s, i)
4694#@+node:ekr.20031218072017.2369: *4* g.skip_string
4695def skip_string(s: str, i: int) -> int:
4696 """Scan forward to the end of a string."""
4697 delim = s[i]
4698 i += 1
4699 assert delim in '\'"', (repr(delim), repr(s))
4700 n = len(s)
4701 while i < n and s[i] != delim:
4702 if s[i] == '\\':
4703 i += 2
4704 else:
4705 i += 1
4706 if i >= n:
4707 pass
4708 elif s[i] == delim:
4709 i += 1
4710 return i
4711#@+node:ekr.20031218072017.3193: *4* g.skip_to_char
4712def skip_to_char(s: str, i: int, ch: str) -> Tuple[int, str]:
4713 j = s.find(ch, i)
4714 if j == -1:
4715 return len(s), s[i:]
4716 return j, s[i:j]
4717#@+node:ekr.20031218072017.3194: *4* g.skip_ws, skip_ws_and_nl
4718def skip_ws(s: str, i: int) -> int:
4719 n = len(s)
4720 while i < n and g.is_ws(s[i]):
4721 i += 1
4722 return i
4724def skip_ws_and_nl(s: str, i: int) -> int:
4725 n = len(s)
4726 while i < n and (g.is_ws(s[i]) or g.is_nl(s, i)):
4727 i += 1
4728 return i
4729#@+node:ekr.20170414034616.1: ** g.Git
4730#@+node:ekr.20180325025502.1: *3* g.backupGitIssues
4731def backupGitIssues(c: Cmdr, base_url: str=None) -> None:
4732 """Get a list of issues from Leo's GitHub site."""
4733 if base_url is None:
4734 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues'
4736 root = c.lastTopLevel().insertAfter()
4737 root.h = f'Backup of issues: {time.strftime("%Y/%m/%d")}'
4738 label_list: List[str] = []
4739 GitIssueController().backup_issues(base_url, c, label_list, root)
4740 root.expand()
4741 c.selectPosition(root)
4742 c.redraw()
4743 g.trace('done')
4744#@+node:ekr.20170616102324.1: *3* g.execGitCommand
4745def execGitCommand(command: str, directory: str) -> List[str]:
4746 """Execute the given git command in the given directory."""
4747 git_dir = g.os_path_finalize_join(directory, '.git')
4748 if not g.os_path_exists(git_dir):
4749 g.trace('not found:', git_dir, g.callers())
4750 return []
4751 if '\n' in command:
4752 g.trace('removing newline from', command)
4753 command = command.replace('\n', '')
4754 # #1777: Save/restore os.curdir
4755 old_dir = os.getcwd()
4756 if directory:
4757 os.chdir(directory)
4758 try:
4759 p = subprocess.Popen(
4760 shlex.split(command),
4761 stdout=subprocess.PIPE,
4762 stderr=None, # Shows error traces.
4763 shell=False,
4764 )
4765 out, err = p.communicate()
4766 lines = [g.toUnicode(z) for z in g.splitLines(out or [])]
4767 finally:
4768 os.chdir(old_dir)
4769 return lines
4770#@+node:ekr.20180126043905.1: *3* g.getGitIssues
4771def getGitIssues(c: Cmdr,
4772 base_url: str=None,
4773 label_list: List=None,
4774 milestone: str=None,
4775 state: Optional[str]=None, # in (None, 'closed', 'open')
4776) -> None:
4777 """Get a list of issues from Leo's GitHub site."""
4778 if base_url is None:
4779 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues'
4780 if isinstance(label_list, (list, tuple)):
4781 root = c.lastTopLevel().insertAfter()
4782 root.h = 'Issues for ' + milestone if milestone else 'Backup'
4783 GitIssueController().backup_issues(base_url, c, label_list, root)
4784 root.expand()
4785 c.selectPosition(root)
4786 c.redraw()
4787 g.trace('done')
4788 else:
4789 g.trace('label_list must be a list or tuple', repr(label_list))
4790#@+node:ekr.20180126044602.1: *4* class GitIssueController
4791class GitIssueController:
4792 """
4793 A class encapsulating the retrieval of GitHub issues.
4795 The GitHub api: https://developer.github.com/v3/issues/
4796 """
4797 #@+others
4798 #@+node:ekr.20180325023336.1: *5* git.backup_issues
4799 def backup_issues(self, base_url: str, c: Cmdr, label_list: List, root: Pos, state: Any=None) -> None:
4801 self.base_url = base_url
4802 self.root = root
4803 self.milestone = None
4804 if label_list:
4805 for state in ('closed', 'open'):
4806 for label in label_list:
4807 self.get_one_issue(label, state)
4808 elif state is None:
4809 for state in ('closed', 'open'):
4810 organizer = root.insertAsLastChild()
4811 organizer.h = f"{state} issues..."
4812 self.get_all_issues(label_list, organizer, state)
4813 elif state in ('closed', 'open'):
4814 self.get_all_issues(label_list, root, state)
4815 else:
4816 g.es_print('state must be in (None, "open", "closed")')
4817 #@+node:ekr.20180325024334.1: *5* git.get_all_issues
4818 def get_all_issues(self, label_list: List, root: Pos, state: Any, limit: int=100) -> None:
4819 """Get all issues for the base url."""
4820 try:
4821 import requests
4822 except Exception:
4823 g.trace('requests not found: `pip install requests`')
4824 return
4825 label = None
4826 assert state in ('open', 'closed')
4827 page_url = self.base_url + '?&state=%s&page=%s'
4828 page, total = 1, 0
4829 while True:
4830 url = page_url % (state, page)
4831 r = requests.get(url)
4832 try:
4833 done, n = self.get_one_page(label, page, r, root)
4834 # Do not remove this trace. It's reassuring.
4835 g.trace(f"done: {done:5} page: {page:3} found: {n} label: {label}")
4836 except AttributeError:
4837 g.trace('Possible rate limit')
4838 self.print_header(r)
4839 g.es_exception()
4840 break
4841 total += n
4842 if done:
4843 break
4844 page += 1
4845 if page > limit:
4846 g.trace('too many pages')
4847 break
4848 #@+node:ekr.20180126044850.1: *5* git.get_issues
4849 def get_issues(self, base_url: str, label_list: List, milestone: Any, root: Pos, state: Any) -> None:
4850 """Create a list of issues for each label in label_list."""
4851 self.base_url = base_url
4852 self.milestone = milestone
4853 self.root = root
4854 for label in label_list:
4855 self.get_one_issue(label, state)
4856 #@+node:ekr.20180126043719.3: *5* git.get_one_issue
4857 def get_one_issue(self, label: str, state: Any, limit: int=20) -> None:
4858 """Create a list of issues with the given label."""
4859 try:
4860 import requests
4861 except Exception:
4862 g.trace('requests not found: `pip install requests`')
4863 return
4864 root = self.root.insertAsLastChild()
4865 page, total = 1, 0
4866 page_url = self.base_url + '?labels=%s&state=%s&page=%s'
4867 while True:
4868 url = page_url % (label, state, page)
4869 r = requests.get(url)
4870 try:
4871 done, n = self.get_one_page(label, page, r, root)
4872 # Do not remove this trace. It's reassuring.
4873 g.trace(f"done: {done:5} page: {page:3} found: {n:3} label: {label}")
4874 except AttributeError:
4875 g.trace('Possible rate limit')
4876 self.print_header(r)
4877 g.es_exception()
4878 break
4879 total += n
4880 if done:
4881 break
4882 page += 1
4883 if page > limit:
4884 g.trace('too many pages')
4885 break
4886 state = state.capitalize()
4887 if self.milestone:
4888 root.h = f"{total} {state} {label} issues for milestone {self.milestone}"
4889 else:
4890 root.h = f"{total} {state} {label} issues"
4891 #@+node:ekr.20180126043719.4: *5* git.get_one_page
4892 def get_one_page(self, label: str, page: int, r: Any, root: Pos) -> Tuple[bool, int]:
4894 if self.milestone:
4895 aList = [
4896 z for z in r.json()
4897 if z.get('milestone') is not None and
4898 self.milestone == z.get('milestone').get('title')
4899 ]
4900 else:
4901 aList = [z for z in r.json()]
4902 for d in aList:
4903 n, title = d.get('number'), d.get('title')
4904 html_url = d.get('html_url') or self.base_url
4905 p = root.insertAsNthChild(0)
4906 p.h = f"#{n}: {title}"
4907 p.b = f"{html_url}\n\n"
4908 p.b += d.get('body').strip()
4909 link = r.headers.get('Link')
4910 done = not link or link.find('rel="next"') == -1
4911 return done, len(aList)
4912 #@+node:ekr.20180127092201.1: *5* git.print_header
4913 def print_header(self, r: Any) -> None:
4915 # r.headers is a CaseInsensitiveDict
4916 # so g.printObj(r.headers) is just repr(r.headers)
4917 if 0:
4918 print('Link', r.headers.get('Link'))
4919 else:
4920 for key in r.headers:
4921 print(f"{key:35}: {r.headers.get(key)}")
4922 #@-others
4923#@+node:ekr.20190428173354.1: *3* g.getGitVersion
4924def getGitVersion(directory: str=None) -> Tuple[str, str, str]:
4925 """Return a tuple (author, build, date) from the git log, or None."""
4926 #
4927 # -n: Get only the last log.
4928 trace = 'git' in g.app.debug
4929 try:
4930 s = subprocess.check_output(
4931 'git log -n 1 --date=iso',
4932 cwd=directory or g.app.loadDir,
4933 stderr=subprocess.DEVNULL,
4934 shell=True,
4935 )
4936 # #1209.
4937 except subprocess.CalledProcessError as e:
4938 s = e.output
4939 if trace:
4940 g.trace('return code', e.returncode)
4941 g.trace('value', repr(s))
4942 g.es_print('Exception in g.getGitVersion')
4943 g.es_exception()
4944 s = g.toUnicode(s)
4945 if not isinstance(s, str):
4946 return '', '', ''
4947 except Exception:
4948 if trace:
4949 g.es_print('Exception in g.getGitVersion')
4950 g.es_exception()
4951 return '', '', ''
4953 info = [g.toUnicode(z) for z in s.splitlines()]
4955 def find(kind: str) -> str:
4956 """Return the given type of log line."""
4957 for z in info:
4958 if z.startswith(kind):
4959 return z.lstrip(kind).lstrip(':').strip()
4960 return ''
4962 return find('Author'), find('commit')[:10], find('Date')
4963#@+node:ekr.20170414034616.2: *3* g.gitBranchName
4964def gitBranchName(path: str=None) -> str:
4965 """
4966 Return the git branch name associated with path/.git, or the empty
4967 string if path/.git does not exist. If path is None, use the leo-editor
4968 directory.
4969 """
4970 branch, commit = g.gitInfo(path)
4971 return branch
4972#@+node:ekr.20170414034616.4: *3* g.gitCommitNumber
4973def gitCommitNumber(path: str=None) -> str:
4974 """
4975 Return the git commit number associated with path/.git, or the empty
4976 string if path/.git does not exist. If path is None, use the leo-editor
4977 directory.
4978 """
4979 branch, commit = g.gitInfo(path)
4980 return commit
4981#@+node:ekr.20200724132432.1: *3* g.gitInfoForFile
4982def gitInfoForFile(filename: str) -> Tuple[str, str]:
4983 """
4984 Return the git (branch, commit) info associated for the given file.
4985 """
4986 # g.gitInfo and g.gitHeadPath now do all the work.
4987 return g.gitInfo(filename)
4988#@+node:ekr.20200724133754.1: *3* g.gitInfoForOutline
4989def gitInfoForOutline(c: Cmdr) -> Tuple[str, str]:
4990 """
4991 Return the git (branch, commit) info associated for commander c.
4992 """
4993 return g.gitInfoForFile(c.fileName())
4994#@+node:maphew.20171112205129.1: *3* g.gitDescribe
4995def gitDescribe(path: str=None) -> Tuple[str, str, str]:
4996 """
4997 Return the Git tag, distance-from-tag, and commit hash for the
4998 associated path. If path is None, use the leo-editor directory.
5000 Given `git describe` cmd line output: `x-leo-v5.6-55-ge1129da\n`
5001 This function returns ('x-leo-v5.6', '55', 'e1129da')
5002 """
5003 describe = g.execGitCommand('git describe --tags --long', path)
5004 # rsplit not split, as '-' might be in tag name.
5005 tag, distance, commit = describe[0].rsplit('-', 2)
5006 if 'g' in commit[0:]:
5007 # leading 'g' isn't part of the commit hash.
5008 commit = commit[1:]
5009 commit = commit.rstrip()
5010 return tag, distance, commit
5011#@+node:ekr.20170414034616.6: *3* g.gitHeadPath
5012def gitHeadPath(path_s: str) -> Optional[str]:
5013 """
5014 Compute the path to .git/HEAD given the path.
5015 """
5016 path = Path(path_s)
5017 # #1780: Look up the directory tree, looking the .git directory.
5018 while os.path.exists(path):
5019 head = os.path.join(path, '.git', 'HEAD')
5020 if os.path.exists(head):
5021 return head
5022 if path == path.parent:
5023 break
5024 path = path.parent
5025 return None
5026#@+node:ekr.20170414034616.3: *3* g.gitInfo
5027def gitInfo(path: str=None) -> Tuple[str, str]:
5028 """
5029 Path may be a directory or file.
5031 Return the branch and commit number or ('', '').
5032 """
5033 branch, commit = '', '' # Set defaults.
5034 if path is None:
5035 # Default to leo/core.
5036 path = os.path.dirname(__file__)
5037 if not os.path.isdir(path):
5038 path = os.path.dirname(path)
5039 # Does path/../ref exist?
5040 path = g.gitHeadPath(path)
5041 if not path:
5042 return branch, commit
5043 try:
5044 with open(path) as f:
5045 s = f.read()
5046 if not s.startswith('ref'):
5047 branch = 'None'
5048 commit = s[:7]
5049 return branch, commit
5050 # On a proper branch
5051 pointer = s.split()[1]
5052 dirs = pointer.split('/')
5053 branch = dirs[-1]
5054 except IOError:
5055 g.trace('can not open:', path)
5056 return branch, commit
5057 # Try to get a better commit number.
5058 git_dir = g.os_path_finalize_join(path, '..')
5059 try:
5060 path = g.os_path_finalize_join(git_dir, pointer)
5061 with open(path) as f: # type:ignore
5062 s = f.read()
5063 commit = s.strip()[0:12]
5064 # shorten the hash to a unique shortname
5065 except IOError:
5066 try:
5067 path = g.os_path_finalize_join(git_dir, 'packed-refs')
5068 with open(path) as f: # type:ignore
5069 for line in f:
5070 if line.strip().endswith(' ' + pointer):
5071 commit = line.split()[0][0:12]
5072 break
5073 except IOError:
5074 pass
5075 return branch, commit
5076#@+node:ekr.20031218072017.3139: ** g.Hooks & Plugins
5077#@+node:ekr.20101028131948.5860: *3* g.act_on_node
5078def dummy_act_on_node(c: Cmdr, p: Pos, event: Any) -> None:
5079 pass
5081# This dummy definition keeps pylint happy.
5082# Plugins can change this.
5084act_on_node = dummy_act_on_node
5085#@+node:ville.20120502221057.7500: *3* g.childrenModifiedSet, g.contentModifiedSet
5086childrenModifiedSet: Set["VNode"] = set()
5087contentModifiedSet: Set["VNode"] = set()
5088#@+node:ekr.20031218072017.1596: *3* g.doHook
5089def doHook(tag: str, *args: Any, **keywords: Any) -> Any:
5090 """
5091 This global function calls a hook routine. Hooks are identified by the
5092 tag param.
5094 Returns the value returned by the hook routine, or None if the there is
5095 an exception.
5097 We look for a hook routine in three places:
5098 1. c.hookFunction
5099 2. app.hookFunction
5100 3. leoPlugins.doPlugins()
5102 Set app.hookError on all exceptions.
5103 Scripts may reset app.hookError to try again.
5104 """
5105 if g.app.killed or g.app.hookError:
5106 return None
5107 if args:
5108 # A minor error in Leo's core.
5109 g.pr(f"***ignoring args param. tag = {tag}")
5110 if not g.app.config.use_plugins:
5111 if tag in ('open0', 'start1'):
5112 g.warning("Plugins disabled: use_plugins is 0 in a leoSettings.leo file.")
5113 return None
5114 # Get the hook handler function. Usually this is doPlugins.
5115 c = keywords.get("c")
5116 # pylint: disable=consider-using-ternary
5117 f = (c and c.hookFunction) or g.app.hookFunction
5118 if not f:
5119 g.app.hookFunction = f = g.app.pluginsController.doPlugins
5120 try:
5121 # Pass the hook to the hook handler.
5122 # g.pr('doHook',f.__name__,keywords.get('c'))
5123 return f(tag, keywords)
5124 except Exception:
5125 g.es_exception()
5126 g.app.hookError = True # Supress this function.
5127 g.app.idle_time_hooks_enabled = False
5128 return None
5129#@+node:ekr.20100910075900.5950: *3* g.Wrappers for g.app.pluginController methods
5130# Important: we can not define g.pc here!
5131#@+node:ekr.20100910075900.5951: *4* g.Loading & registration
5132def loadOnePlugin(pluginName: str, verbose: bool=False) -> Any:
5133 pc = g.app.pluginsController
5134 return pc.loadOnePlugin(pluginName, verbose=verbose)
5136def registerExclusiveHandler(tags: List[str], fn: str) -> Any:
5137 pc = g.app.pluginsController
5138 return pc.registerExclusiveHandler(tags, fn)
5140def registerHandler(tags: Any, fn: Any) -> Any:
5141 pc = g.app.pluginsController
5142 return pc.registerHandler(tags, fn)
5144def plugin_signon(module_name: str, verbose: bool=False) -> Any:
5145 pc = g.app.pluginsController
5146 return pc.plugin_signon(module_name, verbose)
5148def unloadOnePlugin(moduleOrFileName: str, verbose: bool=False) -> Any:
5149 pc = g.app.pluginsController
5150 return pc.unloadOnePlugin(moduleOrFileName, verbose)
5152def unregisterHandler(tags: Any, fn: Any) -> Any:
5153 pc = g.app.pluginsController
5154 return pc.unregisterHandler(tags, fn)
5155#@+node:ekr.20100910075900.5952: *4* g.Information
5156def getHandlersForTag(tags: List[str]) -> List:
5157 pc = g.app.pluginsController
5158 return pc.getHandlersForTag(tags)
5160def getLoadedPlugins() -> List:
5161 pc = g.app.pluginsController
5162 return pc.getLoadedPlugins()
5164def getPluginModule(moduleName: str) -> Any:
5165 pc = g.app.pluginsController
5166 return pc.getPluginModule(moduleName)
5168def pluginIsLoaded(fn: str) -> bool:
5169 pc = g.app.pluginsController
5170 return pc.isLoaded(fn)
5171#@+node:ekr.20031218072017.1315: ** g.Idle time functions
5172#@+node:EKR.20040602125018.1: *3* g.disableIdleTimeHook
5173def disableIdleTimeHook() -> None:
5174 """Disable the global idle-time hook."""
5175 g.app.idle_time_hooks_enabled = False
5176#@+node:EKR.20040602125018: *3* g.enableIdleTimeHook
5177def enableIdleTimeHook(*args: Any, **keys: Any) -> None:
5178 """Enable idle-time processing."""
5179 g.app.idle_time_hooks_enabled = True
5180#@+node:ekr.20140825042850.18410: *3* g.IdleTime
5181def IdleTime(handler: Any, delay: int=500, tag: str=None) -> Any:
5182 """
5183 A thin wrapper for the LeoQtGui.IdleTime class.
5185 The IdleTime class executes a handler with a given delay at idle time.
5186 The handler takes a single argument, the IdleTime instance::
5188 def handler(timer):
5189 '''IdleTime handler. timer is an IdleTime instance.'''
5190 delta_t = timer.time-timer.starting_time
5191 g.trace(timer.count, '%2.4f' % (delta_t))
5192 if timer.count >= 5:
5193 g.trace('done')
5194 timer.stop()
5196 # Execute handler every 500 msec. at idle time.
5197 timer = g.IdleTime(handler,delay=500)
5198 if timer: timer.start()
5200 Timer instances are completely independent::
5202 def handler1(timer):
5203 delta_t = timer.time-timer.starting_time
5204 g.trace('%2s %2.4f' % (timer.count,delta_t))
5205 if timer.count >= 5:
5206 g.trace('done')
5207 timer.stop()
5209 def handler2(timer):
5210 delta_t = timer.time-timer.starting_time
5211 g.trace('%2s %2.4f' % (timer.count,delta_t))
5212 if timer.count >= 10:
5213 g.trace('done')
5214 timer.stop()
5216 timer1 = g.IdleTime(handler1, delay=500)
5217 timer2 = g.IdleTime(handler2, delay=1000)
5218 if timer1 and timer2:
5219 timer1.start()
5220 timer2.start()
5221 """
5222 try:
5223 return g.app.gui.idleTimeClass(handler, delay, tag)
5224 except Exception:
5225 return None
5226#@+node:ekr.20161027205025.1: *3* g.idleTimeHookHandler (stub)
5227def idleTimeHookHandler(timer: Any) -> None:
5228 """This function exists for compatibility."""
5229 g.es_print('Replaced by IdleTimeManager.on_idle')
5230 g.trace(g.callers())
5231#@+node:ekr.20041219095213: ** g.Importing
5232#@+node:ekr.20040917061619: *3* g.cantImport
5233def cantImport(moduleName: str, pluginName: str=None, verbose: bool=True) -> None:
5234 """Print a "Can't Import" message and return None."""
5235 s = f"Can not import {moduleName}"
5236 if pluginName:
5237 s = s + f" from {pluginName}"
5238 if not g.app or not g.app.gui:
5239 print(s)
5240 elif g.unitTesting:
5241 return
5242 else:
5243 g.warning('', s)
5244#@+node:ekr.20191220044128.1: *3* g.import_module
5245def import_module(name: str, package: str=None) -> Any:
5246 """
5247 A thin wrapper over importlib.import_module.
5248 """
5249 trace = 'plugins' in g.app.debug and not g.unitTesting
5250 exceptions = []
5251 try:
5252 m = importlib.import_module(name, package=package)
5253 except Exception as e:
5254 m = None
5255 if trace:
5256 t, v, tb = sys.exc_info()
5257 del tb # don't need the traceback
5258 # In case v is empty, we'll at least have the execption type
5259 v = v or str(t) # type:ignore
5260 if v not in exceptions:
5261 exceptions.append(v)
5262 g.trace(f"Can not import {name}: {e}")
5263 return m
5264#@+node:ekr.20140711071454.17650: ** g.Indices, Strings, Unicode & Whitespace
5265#@+node:ekr.20140711071454.17647: *3* g.Indices
5266#@+node:ekr.20050314140957: *4* g.convertPythonIndexToRowCol
5267def convertPythonIndexToRowCol(s: str, i: int) -> Tuple[int, int]:
5268 """Convert index i into string s into zero-based row/col indices."""
5269 if not s or i <= 0:
5270 return 0, 0
5271 i = min(i, len(s))
5272 # works regardless of what s[i] is
5273 row = s.count('\n', 0, i) # Don't include i
5274 if row == 0:
5275 return row, i
5276 prevNL = s.rfind('\n', 0, i) # Don't include i
5277 return row, i - prevNL - 1
5278#@+node:ekr.20050315071727: *4* g.convertRowColToPythonIndex
5279def convertRowColToPythonIndex(s: str, row: int, col: int, lines: List[str]=None) -> int:
5280 """Convert zero-based row/col indices into a python index into string s."""
5281 if row < 0:
5282 return 0
5283 if lines is None:
5284 lines = g.splitLines(s)
5285 if row >= len(lines):
5286 return len(s)
5287 col = min(col, len(lines[row]))
5288 # A big bottleneck
5289 prev = 0
5290 for line in lines[:row]:
5291 prev += len(line)
5292 return prev + col
5293#@+node:ekr.20061031102333.2: *4* g.getWord & getLine
5294def getWord(s: str, i: int) -> Tuple[int, int]:
5295 """Return i,j such that s[i:j] is the word surrounding s[i]."""
5296 if i >= len(s):
5297 i = len(s) - 1
5298 if i < 0:
5299 i = 0
5300 # Scan backwards.
5301 while 0 <= i < len(s) and g.isWordChar(s[i]):
5302 i -= 1
5303 i += 1
5304 # Scan forwards.
5305 j = i
5306 while 0 <= j < len(s) and g.isWordChar(s[j]):
5307 j += 1
5308 return i, j
5310def getLine(s: str, i: int) -> Tuple[int, int]:
5311 """
5312 Return i,j such that s[i:j] is the line surrounding s[i].
5313 s[i] is a newline only if the line is empty.
5314 s[j] is a newline unless there is no trailing newline.
5315 """
5316 if i > len(s):
5317 i = len(s) - 1
5318 if i < 0:
5319 i = 0
5320 # A newline *ends* the line, so look to the left of a newline.
5321 j = s.rfind('\n', 0, i)
5322 if j == -1:
5323 j = 0
5324 else:
5325 j += 1
5326 k = s.find('\n', i)
5327 if k == -1:
5328 k = len(s)
5329 else:
5330 k = k + 1
5331 return j, k
5332#@+node:ekr.20111114151846.9847: *4* g.toPythonIndex
5333def toPythonIndex(s: str, index: Union[int, str]) -> int:
5334 """
5335 Convert index to a Python int.
5337 index may be a Tk index (x.y) or 'end'.
5338 """
5339 if index is None:
5340 return 0
5341 if isinstance(index, int):
5342 return index
5343 if index == '1.0':
5344 return 0
5345 if index == 'end':
5346 return len(s)
5347 data = index.split('.')
5348 if len(data) == 2:
5349 row1, col1 = data
5350 row, col = int(row1), int(col1)
5351 i = g.convertRowColToPythonIndex(s, row - 1, col)
5352 return i
5353 g.trace(f"bad string index: {index}")
5354 return 0
5355#@+node:ekr.20140526144610.17601: *3* g.Strings
5356#@+node:ekr.20190503145501.1: *4* g.isascii
5357def isascii(s: str) -> bool:
5358 # s.isascii() is defined in Python 3.7.
5359 return all(ord(ch) < 128 for ch in s)
5360#@+node:ekr.20031218072017.3106: *4* g.angleBrackets & virtual_event_name
5361def angleBrackets(s: str) -> str:
5362 """Returns < < s > >"""
5363 lt = "<<"
5364 rt = ">>"
5365 return lt + s + rt
5367virtual_event_name = angleBrackets
5368#@+node:ekr.20090516135452.5777: *4* g.ensureLeading/TrailingNewlines
5369def ensureLeadingNewlines(s: str, n: int) -> str:
5370 s = g.removeLeading(s, '\t\n\r ')
5371 return ('\n' * n) + s
5373def ensureTrailingNewlines(s: str, n: int) -> str:
5374 s = g.removeTrailing(s, '\t\n\r ')
5375 return s + '\n' * n
5376#@+node:ekr.20050920084036.4: *4* g.longestCommonPrefix & g.itemsMatchingPrefixInList
5377def longestCommonPrefix(s1: str, s2: str) -> str:
5378 """Find the longest prefix common to strings s1 and s2."""
5379 prefix = ''
5380 for ch in s1:
5381 if s2.startswith(prefix + ch):
5382 prefix = prefix + ch
5383 else:
5384 return prefix
5385 return prefix
5387def itemsMatchingPrefixInList(s: str, aList: List[str], matchEmptyPrefix: bool=False) -> Tuple[List, str]:
5388 """This method returns a sorted list items of aList whose prefix is s.
5390 It also returns the longest common prefix of all the matches.
5391 """
5392 if s:
5393 pmatches = [a for a in aList if a.startswith(s)]
5394 elif matchEmptyPrefix:
5395 pmatches = aList[:]
5396 else: pmatches = []
5397 if pmatches:
5398 pmatches.sort()
5399 common_prefix = reduce(g.longestCommonPrefix, pmatches)
5400 else:
5401 common_prefix = ''
5402 return pmatches, common_prefix
5403#@+node:ekr.20090516135452.5776: *4* g.removeLeading/Trailing
5404# Warning: g.removeTrailingWs already exists.
5405# Do not change it!
5407def removeLeading(s: str, chars: str) -> str:
5408 """Remove all characters in chars from the front of s."""
5409 i = 0
5410 while i < len(s) and s[i] in chars:
5411 i += 1
5412 return s[i:]
5414def removeTrailing(s: str, chars: str) -> str:
5415 """Remove all characters in chars from the end of s."""
5416 i = len(s) - 1
5417 while i >= 0 and s[i] in chars:
5418 i -= 1
5419 i += 1
5420 return s[:i]
5421#@+node:ekr.20060410112600: *4* g.stripBrackets
5422def stripBrackets(s: str) -> str:
5423 """Strip leading and trailing angle brackets."""
5424 if s.startswith('<'):
5425 s = s[1:]
5426 if s.endswith('>'):
5427 s = s[:-1]
5428 return s
5429#@+node:ekr.20170317101100.1: *4* g.unCamel
5430def unCamel(s: str) -> List[str]:
5431 """Return a list of sub-words in camelCased string s."""
5432 result: List[str] = []
5433 word: List[str] = []
5434 for ch in s:
5435 if ch.isalpha() and ch.isupper():
5436 if word:
5437 result.append(''.join(word))
5438 word = [ch]
5439 elif ch.isalpha():
5440 word.append(ch)
5441 elif word:
5442 result.append(''.join(word))
5443 word = []
5444 if word:
5445 result.append(''.join(word))
5446 return result
5447#@+node:ekr.20031218072017.1498: *3* g.Unicode
5448#@+node:ekr.20190505052756.1: *4* g.checkUnicode
5449checkUnicode_dict: Dict[str, bool] = {}
5451def checkUnicode(s: str, encoding: str=None) -> str:
5452 """
5453 Warn when converting bytes. Report *all* errors.
5455 This method is meant to document defensive programming. We don't expect
5456 these errors, but they might arise as the result of problems in
5457 user-defined plugins or scripts.
5458 """
5459 tag = 'g.checkUnicode'
5460 if s is None and g.unitTesting:
5461 return ''
5462 if isinstance(s, str):
5463 return s
5464 if not isinstance(s, bytes):
5465 g.error(f"{tag}: unexpected argument: {s!r}")
5466 return ''
5467 #
5468 # Report the unexpected conversion.
5469 callers = g.callers(1)
5470 if callers not in checkUnicode_dict:
5471 g.trace(g.callers())
5472 g.error(f"\n{tag}: expected unicode. got: {s!r}\n")
5473 checkUnicode_dict[callers] = True
5474 #
5475 # Convert to unicode, reporting all errors.
5476 if not encoding:
5477 encoding = 'utf-8'
5478 try:
5479 s = s.decode(encoding, 'strict')
5480 except(UnicodeDecodeError, UnicodeError):
5481 # https://wiki.python.org/moin/UnicodeDecodeError
5482 s = s.decode(encoding, 'replace')
5483 g.trace(g.callers())
5484 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}")
5485 except Exception:
5486 g.trace(g.callers())
5487 g.es_excption()
5488 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}")
5489 return s
5490#@+node:ekr.20100125073206.8709: *4* g.getPythonEncodingFromString
5491def getPythonEncodingFromString(s: str) -> str:
5492 """Return the encoding given by Python's encoding line.
5493 s is the entire file.
5494 """
5495 encoding = None
5496 tag, tag2 = '# -*- coding:', '-*-'
5497 n1, n2 = len(tag), len(tag2)
5498 if s:
5499 # For Python 3.x we must convert to unicode before calling startswith.
5500 # The encoding doesn't matter: we only look at the first line, and if
5501 # the first line is an encoding line, it will contain only ascii characters.
5502 s = g.toUnicode(s, encoding='ascii', reportErrors=False)
5503 lines = g.splitLines(s)
5504 line1 = lines[0].strip()
5505 if line1.startswith(tag) and line1.endswith(tag2):
5506 e = line1[n1 : -n2].strip()
5507 if e and g.isValidEncoding(e):
5508 encoding = e
5509 elif g.match_word(line1, 0, '@first'): # 2011/10/21.
5510 line1 = line1[len('@first') :].strip()
5511 if line1.startswith(tag) and line1.endswith(tag2):
5512 e = line1[n1 : -n2].strip()
5513 if e and g.isValidEncoding(e):
5514 encoding = e
5515 return encoding
5516#@+node:ekr.20031218072017.1500: *4* g.isValidEncoding
5517def isValidEncoding(encoding: str) -> bool:
5518 """Return True if the encooding is valid."""
5519 if not encoding:
5520 return False
5521 if sys.platform == 'cli':
5522 return True
5523 try:
5524 codecs.lookup(encoding)
5525 return True
5526 except LookupError: # Windows
5527 return False
5528 except AttributeError: # Linux
5529 return False
5530 except Exception:
5531 # UnicodeEncodeError
5532 g.es_print('Please report the following error')
5533 g.es_exception()
5534 return False
5535#@+node:ekr.20061006152327: *4* g.isWordChar & g.isWordChar1
5536def isWordChar(ch: str) -> bool:
5537 """Return True if ch should be considered a letter."""
5538 return bool(ch and (ch.isalnum() or ch == '_'))
5540def isWordChar1(ch: str) -> bool:
5541 return bool(ch and (ch.isalpha() or ch == '_'))
5542#@+node:ekr.20130910044521.11304: *4* g.stripBOM
5543def stripBOM(s: bytes) -> Tuple[str, bytes]:
5544 """
5545 If there is a BOM, return (e,s2) where e is the encoding
5546 implied by the BOM and s2 is the s stripped of the BOM.
5548 If there is no BOM, return (None,s)
5550 s must be the contents of a file (a string) read in binary mode.
5551 """
5552 table = (
5553 # Important: test longer bom's first.
5554 (4, 'utf-32', codecs.BOM_UTF32_BE),
5555 (4, 'utf-32', codecs.BOM_UTF32_LE),
5556 (3, 'utf-8', codecs.BOM_UTF8),
5557 (2, 'utf-16', codecs.BOM_UTF16_BE),
5558 (2, 'utf-16', codecs.BOM_UTF16_LE),
5559 )
5560 if s:
5561 for n, e, bom in table:
5562 assert len(bom) == n
5563 if bom == s[: len(bom)]:
5564 return e, s[len(bom) :]
5565 return None, s
5566#@+node:ekr.20050208093800: *4* g.toEncodedString
5567def toEncodedString(s: str, encoding: str='utf-8', reportErrors: bool=False) -> bytes:
5568 """Convert unicode string to an encoded string."""
5569 if not isinstance(s, str):
5570 return s
5571 if not encoding:
5572 encoding = 'utf-8'
5573 # These are the only significant calls to s.encode in Leo.
5574 try:
5575 s = s.encode(encoding, "strict") # type:ignore
5576 except UnicodeError:
5577 s = s.encode(encoding, "replace") # type:ignore
5578 if reportErrors:
5579 g.error(f"Error converting {s} from unicode to {encoding} encoding")
5580 # Tracing these calls directly yields thousands of calls.
5581 return s # type:ignore
5582#@+node:ekr.20050208093800.1: *4* g.toUnicode
5583unicode_warnings: Dict[str, bool] = {} # Keys are g.callers.
5585def toUnicode(s: Any, encoding: str=None, reportErrors: bool=False) -> str:
5586 """Convert bytes to unicode if necessary."""
5587 if isinstance(s, str):
5588 return s
5589 tag = 'g.toUnicode'
5590 if not isinstance(s, bytes):
5591 if not isinstance(s, (NullObject, TracingNullObject)):
5592 callers = g.callers()
5593 if callers not in unicode_warnings:
5594 unicode_warnings[callers] = True
5595 g.error(f"{tag}: unexpected argument of type {s.__class__.__name__}")
5596 g.trace(callers)
5597 return ''
5598 if not encoding:
5599 encoding = 'utf-8'
5600 try:
5601 s = s.decode(encoding, 'strict')
5602 except(UnicodeDecodeError, UnicodeError):
5603 # https://wiki.python.org/moin/UnicodeDecodeError
5604 s = s.decode(encoding, 'replace')
5605 if reportErrors:
5606 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}")
5607 g.trace(g.callers())
5608 except Exception:
5609 g.es_exception()
5610 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}")
5611 g.trace(g.callers())
5612 return s
5613#@+node:ekr.20031218072017.3197: *3* g.Whitespace
5614#@+node:ekr.20031218072017.3198: *4* g.computeLeadingWhitespace
5615# Returns optimized whitespace corresponding to width with the indicated tab_width.
5617def computeLeadingWhitespace(width: int, tab_width: int) -> str:
5618 if width <= 0:
5619 return ""
5620 if tab_width > 1:
5621 tabs = int(width / tab_width)
5622 blanks = int(width % tab_width)
5623 return ('\t' * tabs) + (' ' * blanks)
5624 # Negative tab width always gets converted to blanks.
5625 return ' ' * width
5626#@+node:ekr.20120605172139.10263: *4* g.computeLeadingWhitespaceWidth
5627# Returns optimized whitespace corresponding to width with the indicated tab_width.
5629def computeLeadingWhitespaceWidth(s: str, tab_width: int) -> int:
5630 w = 0
5631 for ch in s:
5632 if ch == ' ':
5633 w += 1
5634 elif ch == '\t':
5635 w += (abs(tab_width) - (w % abs(tab_width)))
5636 else:
5637 break
5638 return w
5639#@+node:ekr.20031218072017.3199: *4* g.computeWidth
5640# Returns the width of s, assuming s starts a line, with indicated tab_width.
5642def computeWidth(s: str, tab_width: int) -> int:
5643 w = 0
5644 for ch in s:
5645 if ch == '\t':
5646 w += (abs(tab_width) - (w % abs(tab_width)))
5647 elif ch == '\n': # Bug fix: 2012/06/05.
5648 break
5649 else:
5650 w += 1
5651 return w
5652#@+node:ekr.20110727091744.15083: *4* g.wrap_lines (newer)
5653#@@language rest
5654#@+at
5655# Important note: this routine need not deal with leading whitespace.
5656#
5657# Instead, the caller should simply reduce pageWidth by the width of
5658# leading whitespace wanted, then add that whitespace to the lines
5659# returned here.
5660#
5661# The key to this code is the invarient that line never ends in whitespace.
5662#@@c
5663#@@language python
5665def wrap_lines(lines: List[str], pageWidth: int, firstLineWidth: int=None) -> List[str]:
5666 """Returns a list of lines, consisting of the input lines wrapped to the given pageWidth."""
5667 if pageWidth < 10:
5668 pageWidth = 10
5669 # First line is special
5670 if not firstLineWidth:
5671 firstLineWidth = pageWidth
5672 if firstLineWidth < 10:
5673 firstLineWidth = 10
5674 outputLineWidth = firstLineWidth
5675 # Sentence spacing
5676 # This should be determined by some setting, and can only be either 1 or 2
5677 sentenceSpacingWidth = 1
5678 assert 0 < sentenceSpacingWidth < 3
5679 result = [] # The lines of the result.
5680 line = "" # The line being formed. It never ends in whitespace.
5681 for s in lines:
5682 i = 0
5683 while i < len(s):
5684 assert len(line) <= outputLineWidth # DTHEIN 18-JAN-2004
5685 j = g.skip_ws(s, i)
5686 k = g.skip_non_ws(s, j)
5687 word = s[j:k]
5688 assert k > i
5689 i = k
5690 # DTHEIN 18-JAN-2004: wrap at exactly the text width,
5691 # not one character less
5692 #
5693 wordLen = len(word)
5694 if line.endswith('.') or line.endswith('?') or line.endswith('!'):
5695 space = ' ' * sentenceSpacingWidth
5696 else:
5697 space = ' '
5698 if line and wordLen > 0:
5699 wordLen += len(space)
5700 if wordLen + len(line) <= outputLineWidth:
5701 if wordLen > 0:
5702 #@+<< place blank and word on the present line >>
5703 #@+node:ekr.20110727091744.15084: *5* << place blank and word on the present line >>
5704 if line:
5705 # Add the word, preceeded by a blank.
5706 line = space.join((line, word))
5707 else:
5708 # Just add the word to the start of the line.
5709 line = word
5710 #@-<< place blank and word on the present line >>
5711 else: pass # discard the trailing whitespace.
5712 else:
5713 #@+<< place word on a new line >>
5714 #@+node:ekr.20110727091744.15085: *5* << place word on a new line >>
5715 # End the previous line.
5716 if line:
5717 result.append(line)
5718 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines
5719 # Discard the whitespace and put the word on a new line.
5720 line = word
5721 # Careful: the word may be longer than pageWidth.
5722 if len(line) > pageWidth: # DTHEIN 18-JAN-2004: line can equal pagewidth
5723 result.append(line)
5724 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines
5725 line = ""
5726 #@-<< place word on a new line >>
5727 if line:
5728 result.append(line)
5729 return result
5730#@+node:ekr.20031218072017.3200: *4* g.get_leading_ws
5731def get_leading_ws(s: str) -> str:
5732 """Returns the leading whitespace of 's'."""
5733 i = 0
5734 n = len(s)
5735 while i < n and s[i] in (' ', '\t'):
5736 i += 1
5737 return s[0:i]
5738#@+node:ekr.20031218072017.3201: *4* g.optimizeLeadingWhitespace
5739# Optimize leading whitespace in s with the given tab_width.
5741def optimizeLeadingWhitespace(line: str, tab_width: int) -> str:
5742 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width)
5743 s = g.computeLeadingWhitespace(width, tab_width) + line[i:]
5744 return s
5745#@+node:ekr.20040723093558: *4* g.regularizeTrailingNewlines
5746#@+at The caller should call g.stripBlankLines before calling this routine
5747# if desired.
5748#
5749# This routine does _not_ simply call rstrip(): that would delete all
5750# trailing whitespace-only lines, and in some cases that would change
5751# the meaning of program or data.
5752#@@c
5754def regularizeTrailingNewlines(s: str, kind: str) -> None:
5755 """Kind is 'asis', 'zero' or 'one'."""
5756 pass
5757#@+node:ekr.20091229090857.11698: *4* g.removeBlankLines
5758def removeBlankLines(s: str) -> str:
5759 lines = g.splitLines(s)
5760 lines = [z for z in lines if z.strip()]
5761 return ''.join(lines)
5762#@+node:ekr.20091229075924.6235: *4* g.removeLeadingBlankLines
5763def removeLeadingBlankLines(s: str) -> str:
5764 lines = g.splitLines(s)
5765 result = []
5766 remove = True
5767 for line in lines:
5768 if remove and not line.strip():
5769 pass
5770 else:
5771 remove = False
5772 result.append(line)
5773 return ''.join(result)
5774#@+node:ekr.20031218072017.3202: *4* g.removeLeadingWhitespace
5775# Remove whitespace up to first_ws wide in s, given tab_width, the width of a tab.
5777def removeLeadingWhitespace(s: str, first_ws: int, tab_width: int) -> str:
5778 j = 0
5779 ws = 0
5780 first_ws = abs(first_ws)
5781 for ch in s:
5782 if ws >= first_ws:
5783 break
5784 elif ch == ' ':
5785 j += 1
5786 ws += 1
5787 elif ch == '\t':
5788 j += 1
5789 ws += (abs(tab_width) - (ws % abs(tab_width)))
5790 else:
5791 break
5792 if j > 0:
5793 s = s[j:]
5794 return s
5795#@+node:ekr.20031218072017.3203: *4* g.removeTrailingWs
5796# Warning: string.rstrip also removes newlines!
5798def removeTrailingWs(s: str) -> str:
5799 j = len(s) - 1
5800 while j >= 0 and (s[j] == ' ' or s[j] == '\t'):
5801 j -= 1
5802 return s[: j + 1]
5803#@+node:ekr.20031218072017.3204: *4* g.skip_leading_ws
5804# Skips leading up to width leading whitespace.
5806def skip_leading_ws(s: str, i: int, ws: int, tab_width: int) -> int:
5807 count = 0
5808 while count < ws and i < len(s):
5809 ch = s[i]
5810 if ch == ' ':
5811 count += 1
5812 i += 1
5813 elif ch == '\t':
5814 count += (abs(tab_width) - (count % abs(tab_width)))
5815 i += 1
5816 else: break
5817 return i
5818#@+node:ekr.20031218072017.3205: *4* g.skip_leading_ws_with_indent
5819def skip_leading_ws_with_indent(s: str, i: int, tab_width: int) -> Tuple[int, int]:
5820 """Skips leading whitespace and returns (i, indent),
5822 - i points after the whitespace
5823 - indent is the width of the whitespace, assuming tab_width wide tabs."""
5824 count = 0
5825 n = len(s)
5826 while i < n:
5827 ch = s[i]
5828 if ch == ' ':
5829 count += 1
5830 i += 1
5831 elif ch == '\t':
5832 count += (abs(tab_width) - (count % abs(tab_width)))
5833 i += 1
5834 else: break
5835 return i, count
5836#@+node:ekr.20040723093558.1: *4* g.stripBlankLines
5837def stripBlankLines(s: str) -> str:
5838 lines = g.splitLines(s)
5839 for i, line in enumerate(lines):
5840 j = g.skip_ws(line, 0)
5841 if j >= len(line):
5842 lines[i] = ''
5843 elif line[j] == '\n':
5844 lines[i] = '\n'
5845 return ''.join(lines)
5846#@+node:ekr.20031218072017.3108: ** g.Logging & Printing
5847# g.es and related print to the Log window.
5848# g.pr prints to the console.
5849# g.es_print and related print to both the Log window and the console.
5850#@+node:ekr.20080821073134.2: *3* g.doKeywordArgs
5851def doKeywordArgs(keys: Dict, d: Dict=None) -> Dict:
5852 """
5853 Return a result dict that is a copy of the keys dict
5854 with missing items replaced by defaults in d dict.
5855 """
5856 if d is None:
5857 d = {}
5858 result = {}
5859 for key, default_val in d.items():
5860 isBool = default_val in (True, False)
5861 val = keys.get(key)
5862 if isBool and val in (True, 'True', 'true'):
5863 result[key] = True
5864 elif isBool and val in (False, 'False', 'false'):
5865 result[key] = False
5866 elif val is None:
5867 result[key] = default_val
5868 else:
5869 result[key] = val
5870 return result
5871#@+node:ekr.20031218072017.1474: *3* g.enl, ecnl & ecnls
5872def ecnl(tabName: str='Log') -> None:
5873 g.ecnls(1, tabName)
5875def ecnls(n: int, tabName: str='Log') -> None:
5876 log = app.log
5877 if log and not log.isNull:
5878 while log.newlines < n:
5879 g.enl(tabName)
5881def enl(tabName: str='Log') -> None:
5882 log = app.log
5883 if log and not log.isNull:
5884 log.newlines += 1
5885 log.putnl(tabName)
5886#@+node:ekr.20100914094836.5892: *3* g.error, g.note, g.warning, g.red, g.blue
5887def blue(*args: Any, **keys: Any) -> None:
5888 g.es_print(color='blue', *args, **keys)
5890def error(*args: Any, **keys: Any) -> None:
5891 g.es_print(color='error', *args, **keys)
5893def note(*args: Any, **keys: Any) -> None:
5894 g.es_print(color='note', *args, **keys)
5896def red(*args: Any, **keys: Any) -> None:
5897 g.es_print(color='red', *args, **keys)
5899def warning(*args: Any, **keys: Any) -> None:
5900 g.es_print(color='warning', *args, **keys)
5901#@+node:ekr.20070626132332: *3* g.es
5902def es(*args: Any, **keys: Any) -> None:
5903 """Put all non-keyword args to the log pane.
5904 The first, third, fifth, etc. arg translated by g.translateString.
5905 Supports color, comma, newline, spaces and tabName keyword arguments.
5906 """
5907 if not app or app.killed:
5908 return
5909 if app.gui and app.gui.consoleOnly:
5910 return
5911 log = app.log
5912 # Compute the effective args.
5913 d = {
5914 'color': None,
5915 'commas': False,
5916 'newline': True,
5917 'spaces': True,
5918 'tabName': 'Log',
5919 'nodeLink': None,
5920 }
5921 d = g.doKeywordArgs(keys, d)
5922 color = d.get('color')
5923 if color == 'suppress':
5924 return # New in 4.3.
5925 color = g.actualColor(color)
5926 tabName = d.get('tabName') or 'Log'
5927 newline = d.get('newline')
5928 s = g.translateArgs(args, d)
5929 # Do not call g.es, g.es_print, g.pr or g.trace here!
5930 # sys.__stdout__.write('\n===== g.es: %r\n' % s)
5931 if app.batchMode:
5932 if app.log:
5933 app.log.put(s)
5934 elif g.unitTesting:
5935 if log and not log.isNull:
5936 # This makes the output of unit tests match the output of scripts.
5937 g.pr(s, newline=newline)
5938 elif log and app.logInited:
5939 if newline:
5940 s += '\n'
5941 log.put(s, color=color, tabName=tabName, nodeLink=d['nodeLink'])
5942 # Count the number of *trailing* newlines.
5943 for ch in s:
5944 if ch == '\n':
5945 log.newlines += 1
5946 else:
5947 log.newlines = 0
5948 else:
5949 app.logWaiting.append((s, color, newline, d),)
5951log = es
5952#@+node:ekr.20060917120951: *3* g.es_dump
5953def es_dump(s: str, n: int=30, title: str=None) -> None:
5954 if title:
5955 g.es_print('', title)
5956 i = 0
5957 while i < len(s):
5958 aList = ''.join([f"{ord(ch):2x} " for ch in s[i : i + n]])
5959 g.es_print('', aList)
5960 i += n
5961#@+node:ekr.20031218072017.3110: *3* g.es_error & es_print_error
5962def es_error(*args: Any, **keys: Any) -> None:
5963 color = keys.get('color')
5964 if color is None and g.app.config:
5965 keys['color'] = g.app.config.getColor("log-error-color") or 'red'
5966 g.es(*args, **keys)
5968def es_print_error(*args: Any, **keys: Any) -> None:
5969 color = keys.get('color')
5970 if color is None and g.app.config:
5971 keys['color'] = g.app.config.getColor("log-error-color") or 'red'
5972 g.es_print(*args, **keys)
5973#@+node:ekr.20031218072017.3111: *3* g.es_event_exception
5974def es_event_exception(eventName: str, full: bool=False) -> None:
5975 g.es("exception handling ", eventName, "event")
5976 typ, val, tb = sys.exc_info()
5977 if full:
5978 errList = traceback.format_exception(typ, val, tb)
5979 else:
5980 errList = traceback.format_exception_only(typ, val)
5981 for i in errList:
5982 g.es('', i)
5983 if not g.stdErrIsRedirected(): # 2/16/04
5984 traceback.print_exc()
5985#@+node:ekr.20031218072017.3112: *3* g.es_exception
5986def es_exception(full: bool=True, c: Cmdr=None, color: str="red") -> Tuple[str, int]:
5987 typ, val, tb = sys.exc_info()
5988 # val is the second argument to the raise statement.
5989 if full:
5990 lines = traceback.format_exception(typ, val, tb)
5991 else:
5992 lines = traceback.format_exception_only(typ, val)
5993 for line in lines:
5994 g.es_print_error(line, color=color)
5995 fileName, n = g.getLastTracebackFileAndLineNumber()
5996 return fileName, n
5997#@+node:ekr.20061015090538: *3* g.es_exception_type
5998def es_exception_type(c: Cmdr=None, color: str="red") -> None:
5999 # exctype is a Exception class object; value is the error message.
6000 exctype, value = sys.exc_info()[:2]
6001 g.es_print('', f"{exctype.__name__}, {value}", color=color) # type:ignore
6002#@+node:ekr.20050707064040: *3* g.es_print
6003# see: http://www.diveintopython.org/xml_processing/unicode.html
6005def es_print(*args: Any, **keys: Any) -> None:
6006 """
6007 Print all non-keyword args, and put them to the log pane.
6009 The first, third, fifth, etc. arg translated by g.translateString.
6010 Supports color, comma, newline, spaces and tabName keyword arguments.
6011 """
6012 g.pr(*args, **keys)
6013 if g.app and not g.unitTesting:
6014 g.es(*args, **keys)
6015#@+node:ekr.20111107181638.9741: *3* g.print_exception
6016def print_exception(full: bool=True, c: Cmdr=None, flush: bool=False, color: str="red") -> Tuple[str, int]:
6017 """Print exception info about the last exception."""
6018 # val is the second argument to the raise statement.
6019 typ, val, tb = sys.exc_info()
6020 if full:
6021 lines = traceback.format_exception(typ, val, tb)
6022 else:
6023 lines = traceback.format_exception_only(typ, val)
6024 print(''.join(lines), flush=flush)
6025 try:
6026 fileName, n = g.getLastTracebackFileAndLineNumber()
6027 return fileName, n
6028 except Exception:
6029 return "<no file>", 0
6030#@+node:ekr.20050707065530: *3* g.es_trace
6031def es_trace(*args: Any, **keys: Any) -> None:
6032 if args:
6033 try:
6034 s = args[0]
6035 g.trace(g.toEncodedString(s, 'ascii'))
6036 except Exception:
6037 pass
6038 g.es(*args, **keys)
6039#@+node:ekr.20040731204831: *3* g.getLastTracebackFileAndLineNumber
6040def getLastTracebackFileAndLineNumber() -> Tuple[str, int]:
6041 typ, val, tb = sys.exc_info()
6042 if typ == SyntaxError:
6043 # IndentationError is a subclass of SyntaxError.
6044 return val.filename, val.lineno
6045 #
6046 # Data is a list of tuples, one per stack entry.
6047 # Tupls have the form (filename,lineNumber,functionName,text).
6048 data = traceback.extract_tb(tb)
6049 if data:
6050 item = data[-1] # Get the item at the top of the stack.
6051 filename, n, functionName, text = item
6052 return filename, n
6053 # Should never happen.
6054 return '<string>', 0
6055#@+node:ekr.20150621095017.1: *3* g.goto_last_exception
6056def goto_last_exception(c: Cmdr) -> None:
6057 """Go to the line given by sys.last_traceback."""
6058 typ, val, tb = sys.exc_info()
6059 if tb:
6060 file_name, line_number = g.getLastTracebackFileAndLineNumber()
6061 line_number = max(0, line_number - 1) # Convert to zero-based.
6062 if file_name.endswith('scriptFile.py'):
6063 # A script.
6064 c.goToScriptLineNumber(line_number, c.p)
6065 else:
6066 for p in c.all_nodes():
6067 if p.isAnyAtFileNode() and p.h.endswith(file_name):
6068 c.goToLineNumber(line_number)
6069 return
6070 else:
6071 g.trace('No previous exception')
6072#@+node:ekr.20100126062623.6240: *3* g.internalError
6073def internalError(*args: Any) -> None:
6074 """Report a serious interal error in Leo."""
6075 callers = g.callers(20).split(',')
6076 caller = callers[-1]
6077 g.error('\nInternal Leo error in', caller)
6078 g.es_print(*args)
6079 g.es_print('Called from', ', '.join(callers[:-1]))
6080 g.es_print('Please report this error to Leo\'s developers', color='red')
6081#@+node:ekr.20150127060254.5: *3* g.log_to_file
6082def log_to_file(s: str, fn: str=None) -> None:
6083 """Write a message to ~/test/leo_log.txt."""
6084 if fn is None:
6085 fn = g.os_path_expanduser('~/test/leo_log.txt')
6086 if not s.endswith('\n'):
6087 s = s + '\n'
6088 try:
6089 with open(fn, 'a') as f:
6090 f.write(s)
6091 except Exception:
6092 g.es_exception()
6093#@+node:ekr.20080710101653.1: *3* g.pr
6094# see: http://www.diveintopython.org/xml_processing/unicode.html
6096def pr(*args: Any, **keys: Any) -> None:
6097 """
6098 Print all non-keyword args. This is a wrapper for the print statement.
6100 The first, third, fifth, etc. arg translated by g.translateString.
6101 Supports color, comma, newline, spaces and tabName keyword arguments.
6102 """
6103 # Compute the effective args.
6104 d = {'commas': False, 'newline': True, 'spaces': True}
6105 d = doKeywordArgs(keys, d)
6106 newline = d.get('newline')
6107 # Unit tests require sys.stdout.
6108 stdout = sys.stdout if sys.stdout and g.unitTesting else sys.__stdout__
6109 if not stdout:
6110 # #541.
6111 return
6112 if sys.platform.lower().startswith('win'):
6113 encoding = 'ascii' # 2011/11/9.
6114 elif getattr(stdout, 'encoding', None):
6115 # sys.stdout is a TextIOWrapper with a particular encoding.
6116 encoding = stdout.encoding
6117 else:
6118 encoding = 'utf-8'
6119 s = translateArgs(args, d) # Translates everything to unicode.
6120 s = g.toUnicode(s, encoding=encoding, reportErrors=False)
6121 if newline:
6122 s += '\n'
6123 # Python's print statement *can* handle unicode, but
6124 # sitecustomize.py must have sys.setdefaultencoding('utf-8')
6125 try:
6126 # #783: print-* commands fail under pythonw.
6127 stdout.write(s)
6128 except Exception:
6129 pass
6130#@+node:ekr.20060221083356: *3* g.prettyPrintType
6131def prettyPrintType(obj: Any) -> str:
6132 if isinstance(obj, str): # type:ignore
6133 return 'string'
6134 t: Any = type(obj)
6135 if t in (types.BuiltinFunctionType, types.FunctionType):
6136 return 'function'
6137 if t == types.ModuleType:
6138 return 'module'
6139 if t in [types.MethodType, types.BuiltinMethodType]:
6140 return 'method'
6141 # Fall back to a hack.
6142 t = str(type(obj)) # type:ignore
6143 if t.startswith("<type '"):
6144 t = t[7:]
6145 if t.endswith("'>"):
6146 t = t[:-2]
6147 return t
6148#@+node:ekr.20031218072017.3113: *3* g.printBindings
6149def print_bindings(name: str, window: Any) -> None:
6150 bindings = window.bind()
6151 g.pr("\nBindings for", name)
6152 for b in bindings:
6153 g.pr(b)
6154#@+node:ekr.20070510074941: *3* g.printEntireTree
6155def printEntireTree(c: Cmdr, tag: str='') -> None:
6156 g.pr('printEntireTree', '=' * 50)
6157 g.pr('printEntireTree', tag, 'root', c.rootPosition())
6158 for p in c.all_positions():
6159 g.pr('..' * p.level(), p.v)
6160#@+node:ekr.20031218072017.3114: *3* g.printGlobals
6161def printGlobals(message: str=None) -> None:
6162 # Get the list of globals.
6163 globs = list(globals())
6164 globs.sort()
6165 # Print the list.
6166 if message:
6167 leader = "-" * 10
6168 g.pr(leader, ' ', message, ' ', leader)
6169 for name in globs:
6170 g.pr(name)
6171#@+node:ekr.20031218072017.3115: *3* g.printLeoModules
6172def printLeoModules(message: str=None) -> None:
6173 # Create the list.
6174 mods = []
6175 for name in sys.modules:
6176 if name and name[0:3] == "leo":
6177 mods.append(name)
6178 # Print the list.
6179 if message:
6180 leader = "-" * 10
6181 g.pr(leader, ' ', message, ' ', leader)
6182 mods.sort()
6183 for m in mods:
6184 g.pr(m, newline=False)
6185 g.pr('')
6186#@+node:ekr.20041122153823: *3* g.printStack
6187def printStack() -> None:
6188 traceback.print_stack()
6189#@+node:ekr.20031218072017.2317: *3* g.trace
6190def trace(*args: Any, **keys: Any) -> None:
6191 """Print a tracing message."""
6192 # Don't use g here: in standalone mode g is a NullObject!
6193 # Compute the effective args.
6194 d: Dict[str, Any] = {'align': 0, 'before': '', 'newline': True, 'caller_level': 1, 'noname': False}
6195 d = doKeywordArgs(keys, d)
6196 newline = d.get('newline')
6197 align = d.get('align', 0)
6198 caller_level = d.get('caller_level', 1)
6199 noname = d.get('noname')
6200 # Compute the caller name.
6201 if noname:
6202 name = ''
6203 else:
6204 try: # get the function name from the call stack.
6205 f1 = sys._getframe(caller_level) # The stack frame, one level up.
6206 code1 = f1.f_code # The code object
6207 name = code1.co_name # The code name
6208 except Exception:
6209 name = g.shortFileName(__file__)
6210 if name == '<module>':
6211 name = g.shortFileName(__file__)
6212 if name.endswith('.pyc'):
6213 name = name[:-1]
6214 # Pad the caller name.
6215 if align != 0 and len(name) < abs(align):
6216 pad = ' ' * (abs(align) - len(name))
6217 if align > 0:
6218 name = name + pad
6219 else:
6220 name = pad + name
6221 # Munge *args into s.
6222 result = [name] if name else []
6223 #
6224 # Put leading newlines into the prefix.
6225 if isinstance(args, tuple):
6226 args = list(args) # type:ignore
6227 if args and isinstance(args[0], str):
6228 prefix = ''
6229 while args[0].startswith('\n'):
6230 prefix += '\n'
6231 args[0] = args[0][1:] # type:ignore
6232 else:
6233 prefix = ''
6234 for arg in args:
6235 if isinstance(arg, str):
6236 pass
6237 elif isinstance(arg, bytes):
6238 arg = toUnicode(arg)
6239 else:
6240 arg = repr(arg)
6241 if result:
6242 result.append(" " + arg)
6243 else:
6244 result.append(arg)
6245 s = d.get('before') + ''.join(result)
6246 if prefix:
6247 prefix = prefix[1:] # One less newline.
6248 pr(prefix)
6249 pr(s, newline=newline)
6250#@+node:ekr.20080220111323: *3* g.translateArgs
6251console_encoding = None
6253def translateArgs(args: Iterable[Any], d: Dict[str, Any]) -> str:
6254 """
6255 Return the concatenation of s and all args, with odd args translated.
6256 """
6257 global console_encoding
6258 if not console_encoding:
6259 e = sys.getdefaultencoding()
6260 console_encoding = e if isValidEncoding(e) else 'utf-8'
6261 # print 'translateArgs',console_encoding
6262 result: List[str] = []
6263 n, spaces = 0, d.get('spaces')
6264 for arg in args:
6265 n += 1
6266 # First, convert to unicode.
6267 if isinstance(arg, str):
6268 arg = toUnicode(arg, console_encoding)
6269 # Now translate.
6270 if not isinstance(arg, str):
6271 arg = repr(arg)
6272 elif (n % 2) == 1:
6273 arg = translateString(arg)
6274 else:
6275 pass # The arg is an untranslated string.
6276 if arg:
6277 if result and spaces:
6278 result.append(' ')
6279 result.append(arg)
6280 return ''.join(result)
6281#@+node:ekr.20060810095921: *3* g.translateString & tr
6282def translateString(s: str) -> str:
6283 """Return the translated text of s."""
6284 # pylint: disable=undefined-loop-variable
6285 # looks like a pylint bug
6286 upper = app and getattr(app, 'translateToUpperCase', None)
6287 if not isinstance(s, str):
6288 s = str(s, 'utf-8')
6289 if upper:
6290 s = s.upper()
6291 else:
6292 s = gettext.gettext(s)
6293 return s
6295tr = translateString
6296#@+node:EKR.20040612114220: ** g.Miscellaneous
6297#@+node:ekr.20120928142052.10116: *3* g.actualColor
6298def actualColor(color: str) -> str:
6299 """Return the actual color corresponding to the requested color."""
6300 c = g.app.log and g.app.log.c
6301 # Careful: c.config may not yet exist.
6302 if not c or not c.config:
6303 return color
6304 # Don't change absolute colors.
6305 if color and color.startswith('#'):
6306 return color
6307 # #788: Translate colors to theme-defined colors.
6308 if color is None:
6309 # Prefer text_foreground_color'
6310 color2 = c.config.getColor('log-text-foreground-color')
6311 if color2:
6312 return color2
6313 # Fall back to log_black_color.
6314 color2 = c.config.getColor('log-black-color')
6315 return color2 or 'black'
6316 if color == 'black':
6317 # Prefer log_black_color.
6318 color2 = c.config.getColor('log-black-color')
6319 if color2:
6320 return color2
6321 # Fall back to log_text_foreground_color.
6322 color2 = c.config.getColor('log-text-foreground-color')
6323 return color2 or 'black'
6324 color2 = c.config.getColor(f"log_{color}_color")
6325 return color2 or color
6326#@+node:ekr.20060921100435: *3* g.CheckVersion & helpers
6327# Simplified version by EKR: stringCompare not used.
6329def CheckVersion(
6330 s1: str,
6331 s2: str,
6332 condition: str=">=",
6333 stringCompare: bool=None,
6334 delimiter: str='.',
6335 trace: bool=False,
6336) -> bool:
6337 # CheckVersion is called early in the startup process.
6338 vals1 = [g.CheckVersionToInt(s) for s in s1.split(delimiter)]
6339 n1 = len(vals1)
6340 vals2 = [g.CheckVersionToInt(s) for s in s2.split(delimiter)]
6341 n2 = len(vals2)
6342 n = max(n1, n2)
6343 if n1 < n:
6344 vals1.extend([0 for i in range(n - n1)])
6345 if n2 < n:
6346 vals2.extend([0 for i in range(n - n2)])
6347 for cond, val in (
6348 ('==', vals1 == vals2), ('!=', vals1 != vals2),
6349 ('<', vals1 < vals2), ('<=', vals1 <= vals2),
6350 ('>', vals1 > vals2), ('>=', vals1 >= vals2),
6351 ):
6352 if condition == cond:
6353 result = val
6354 break
6355 else:
6356 raise EnvironmentError(
6357 "condition must be one of '>=', '>', '==', '!=', '<', or '<='.")
6358 return result
6359#@+node:ekr.20070120123930: *4* g.CheckVersionToInt
6360def CheckVersionToInt(s: str) -> int:
6361 try:
6362 return int(s)
6363 except ValueError:
6364 aList = []
6365 for ch in s:
6366 if ch.isdigit():
6367 aList.append(ch)
6368 else:
6369 break
6370 if aList:
6371 s = ''.join(aList)
6372 return int(s)
6373 return 0
6374#@+node:ekr.20111103205308.9657: *3* g.cls
6375@command('cls')
6376def cls(event: Any=None) -> None:
6377 """Clear the screen."""
6378 if sys.platform.lower().startswith('win'):
6379 os.system('cls')
6380#@+node:ekr.20131114124839.16665: *3* g.createScratchCommander
6381def createScratchCommander(fileName: str=None) -> None:
6382 c = g.app.newCommander(fileName)
6383 frame = c.frame
6384 frame.createFirstTreeNode()
6385 assert c.rootPosition()
6386 frame.setInitialWindowGeometry()
6387 frame.resizePanesToRatio(frame.ratio, frame.secondary_ratio)
6388#@+node:ekr.20031218072017.3126: *3* g.funcToMethod (Python Cookbook)
6389def funcToMethod(f: Any, theClass: Any, name: str=None) -> None:
6390 """
6391 From the Python Cookbook...
6393 The following method allows you to add a function as a method of
6394 any class. That is, it converts the function to a method of the
6395 class. The method just added is available instantly to all
6396 existing instances of the class, and to all instances created in
6397 the future.
6399 The function's first argument should be self.
6401 The newly created method has the same name as the function unless
6402 the optional name argument is supplied, in which case that name is
6403 used as the method name.
6404 """
6405 setattr(theClass, name or f.__name__, f)
6406#@+node:ekr.20060913090832.1: *3* g.init_zodb
6407init_zodb_import_failed = False
6408init_zodb_failed: Dict[str, bool] = {} # Keys are paths, values are True.
6409init_zodb_db: Dict[str, Any] = {} # Keys are paths, values are ZODB.DB instances.
6411def init_zodb(pathToZodbStorage: str, verbose: bool=True) -> Any:
6412 """
6413 Return an ZODB.DB instance from the given path.
6414 return None on any error.
6415 """
6416 global init_zodb_db, init_zodb_failed, init_zodb_import_failed
6417 db = init_zodb_db.get(pathToZodbStorage)
6418 if db:
6419 return db
6420 if init_zodb_import_failed:
6421 return None
6422 failed = init_zodb_failed.get(pathToZodbStorage)
6423 if failed:
6424 return None
6425 try:
6426 import ZODB # type:ignore
6427 except ImportError:
6428 if verbose:
6429 g.es('g.init_zodb: can not import ZODB')
6430 g.es_exception()
6431 init_zodb_import_failed = True
6432 return None
6433 try:
6434 storage = ZODB.FileStorage.FileStorage(pathToZodbStorage)
6435 init_zodb_db[pathToZodbStorage] = db = ZODB.DB(storage)
6436 return db
6437 except Exception:
6438 if verbose:
6439 g.es('g.init_zodb: exception creating ZODB.DB instance')
6440 g.es_exception()
6441 init_zodb_failed[pathToZodbStorage] = True
6442 return None
6443#@+node:ekr.20170206080908.1: *3* g.input_
6444def input_(message: str='', c: Cmdr=None) -> str:
6445 """
6446 Safely execute python's input statement.
6448 c.executeScriptHelper binds 'input' to be a wrapper that calls g.input_
6449 with c and handler bound properly.
6450 """
6451 if app.gui.isNullGui:
6452 return ''
6453 # Prompt for input from the console, assuming there is one.
6454 # pylint: disable=no-member
6455 from leo.core.leoQt import QtCore
6456 QtCore.pyqtRemoveInputHook()
6457 return input(message)
6458#@+node:ekr.20110609125359.16493: *3* g.isMacOS
6459def isMacOS() -> bool:
6460 return sys.platform == 'darwin'
6461#@+node:ekr.20181027133311.1: *3* g.issueSecurityWarning
6462def issueSecurityWarning(setting: str) -> None:
6463 g.es('Security warning! Ignoring...', color='red')
6464 g.es(setting, color='red')
6465 g.es('This setting can be set only in')
6466 g.es('leoSettings.leo or myLeoSettings.leo')
6467#@+node:ekr.20031218072017.3144: *3* g.makeDict (Python Cookbook)
6468# From the Python cookbook.
6470def makeDict(**keys: Any) -> Dict:
6471 """Returns a Python dictionary from using the optional keyword arguments."""
6472 return keys
6473#@+node:ekr.20140528065727.17963: *3* g.pep8_class_name
6474def pep8_class_name(s: str) -> str:
6475 """Return the proper class name for s."""
6476 # Warning: s.capitalize() does not work.
6477 # It lower cases all but the first letter!
6478 return ''.join([z[0].upper() + z[1:] for z in s.split('_') if z])
6480if 0: # Testing:
6481 cls()
6482 aList = (
6483 '_',
6484 '__',
6485 '_abc',
6486 'abc_',
6487 'abc',
6488 'abc_xyz',
6489 'AbcPdQ',
6490 )
6491 for s in aList:
6492 print(pep8_class_name(s))
6493#@+node:ekr.20160417174224.1: *3* g.plural
6494def plural(obj: Any) -> str:
6495 """Return "s" or "" depending on n."""
6496 if isinstance(obj, (list, tuple, str)):
6497 n = len(obj)
6498 else:
6499 n = obj
6500 return '' if n == 1 else 's'
6501#@+node:ekr.20160331194701.1: *3* g.truncate
6502def truncate(s: str, n: int) -> str:
6503 """Return s truncated to n characters."""
6504 if len(s) <= n:
6505 return s
6506 # Fail: weird ws.
6507 s2 = s[: n - 3] + f"...({len(s)})"
6508 if s.endswith('\n'):
6509 return s2 + '\n'
6510 return s2
6511#@+node:ekr.20031218072017.3150: *3* g.windows
6512def windows() -> Optional[List]:
6513 return app and app.windowList
6514#@+node:ekr.20031218072017.2145: ** g.os_path_ Wrappers
6515#@+at Note: all these methods return Unicode strings. It is up to the user to
6516# convert to an encoded string as needed, say when opening a file.
6517#@+node:ekr.20180314120442.1: *3* g.glob_glob
6518def glob_glob(pattern: str) -> List:
6519 """Return the regularized glob.glob(pattern)"""
6520 aList = glob.glob(pattern)
6521 # os.path.normpath does the *reverse* of what we want.
6522 if g.isWindows:
6523 aList = [z.replace('\\', '/') for z in aList]
6524 return aList
6525#@+node:ekr.20031218072017.2146: *3* g.os_path_abspath
6526def os_path_abspath(path: str) -> str:
6527 """Convert a path to an absolute path."""
6528 if not path:
6529 return ''
6530 if '\x00' in path:
6531 g.trace('NULL in', repr(path), g.callers())
6532 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6533 path = os.path.abspath(path)
6534 # os.path.normpath does the *reverse* of what we want.
6535 if g.isWindows:
6536 path = path.replace('\\', '/')
6537 return path
6538#@+node:ekr.20031218072017.2147: *3* g.os_path_basename
6539def os_path_basename(path: str) -> str:
6540 """Return the second half of the pair returned by split(path)."""
6541 if not path:
6542 return ''
6543 path = os.path.basename(path)
6544 # os.path.normpath does the *reverse* of what we want.
6545 if g.isWindows:
6546 path = path.replace('\\', '/')
6547 return path
6548#@+node:ekr.20031218072017.2148: *3* g.os_path_dirname
6549def os_path_dirname(path: str) -> str:
6550 """Return the first half of the pair returned by split(path)."""
6551 if not path:
6552 return ''
6553 path = os.path.dirname(path)
6554 # os.path.normpath does the *reverse* of what we want.
6555 if g.isWindows:
6556 path = path.replace('\\', '/')
6557 return path
6558#@+node:ekr.20031218072017.2149: *3* g.os_path_exists
6559def os_path_exists(path: str) -> bool:
6560 """Return True if path exists."""
6561 if not path:
6562 return False
6563 if '\x00' in path:
6564 g.trace('NULL in', repr(path), g.callers())
6565 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6566 return os.path.exists(path)
6567#@+node:ekr.20080921060401.13: *3* g.os_path_expanduser
6568def os_path_expanduser(path: str) -> str:
6569 """wrap os.path.expanduser"""
6570 if not path:
6571 return ''
6572 result = os.path.normpath(os.path.expanduser(path))
6573 # os.path.normpath does the *reverse* of what we want.
6574 if g.isWindows:
6575 path = path.replace('\\', '/')
6576 return result
6577#@+node:ekr.20080921060401.14: *3* g.os_path_finalize
6578def os_path_finalize(path: str) -> str:
6579 """
6580 Expand '~', then return os.path.normpath, os.path.abspath of the path.
6581 There is no corresponding os.path method
6582 """
6583 if '\x00' in path:
6584 g.trace('NULL in', repr(path), g.callers())
6585 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6586 path = os.path.expanduser(path) # #1383.
6587 path = os.path.abspath(path)
6588 path = os.path.normpath(path)
6589 # os.path.normpath does the *reverse* of what we want.
6590 if g.isWindows:
6591 path = path.replace('\\', '/')
6592 # calling os.path.realpath here would cause problems in some situations.
6593 return path
6594#@+node:ekr.20140917154740.19483: *3* g.os_path_finalize_join
6595def os_path_finalize_join(*args: Any, **keys: Any) -> str:
6596 """
6597 Join and finalize.
6599 **keys may contain a 'c' kwarg, used by g.os_path_join.
6600 """
6601 path = g.os_path_join(*args, **keys)
6602 path = g.os_path_finalize(path)
6603 return path
6604#@+node:ekr.20031218072017.2150: *3* g.os_path_getmtime
6605def os_path_getmtime(path: str) -> float:
6606 """Return the modification time of path."""
6607 if not path:
6608 return 0
6609 try:
6610 return os.path.getmtime(path)
6611 except Exception:
6612 return 0
6613#@+node:ekr.20080729142651.2: *3* g.os_path_getsize
6614def os_path_getsize(path: str) -> int:
6615 """Return the size of path."""
6616 return os.path.getsize(path) if path else 0
6617#@+node:ekr.20031218072017.2151: *3* g.os_path_isabs
6618def os_path_isabs(path: str) -> bool:
6619 """Return True if path is an absolute path."""
6620 return os.path.isabs(path) if path else False
6621#@+node:ekr.20031218072017.2152: *3* g.os_path_isdir
6622def os_path_isdir(path: str) -> bool:
6623 """Return True if the path is a directory."""
6624 return os.path.isdir(path) if path else False
6625#@+node:ekr.20031218072017.2153: *3* g.os_path_isfile
6626def os_path_isfile(path: str) -> bool:
6627 """Return True if path is a file."""
6628 return os.path.isfile(path) if path else False
6629#@+node:ekr.20031218072017.2154: *3* g.os_path_join
6630def os_path_join(*args: Any, **keys: Any) -> str:
6631 """
6632 Join paths, like os.path.join, with enhancements:
6634 A '!!' arg prepends g.app.loadDir to the list of paths.
6635 A '.' arg prepends c.openDirectory to the list of paths,
6636 provided there is a 'c' kwarg.
6637 """
6638 c = keys.get('c')
6639 uargs = [z for z in args if z]
6640 if not uargs:
6641 return ''
6642 # Note: This is exactly the same convention as used by getBaseDirectory.
6643 if uargs[0] == '!!':
6644 uargs[0] = g.app.loadDir
6645 elif uargs[0] == '.':
6646 c = keys.get('c')
6647 if c and c.openDirectory:
6648 uargs[0] = c.openDirectory
6649 try:
6650 path = os.path.join(*uargs)
6651 except TypeError:
6652 g.trace(uargs, args, keys, g.callers())
6653 raise
6654 # May not be needed on some Pythons.
6655 if '\x00' in path:
6656 g.trace('NULL in', repr(path), g.callers())
6657 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10.
6658 # os.path.normpath does the *reverse* of what we want.
6659 if g.isWindows:
6660 path = path.replace('\\', '/')
6661 return path
6662#@+node:ekr.20031218072017.2156: *3* g.os_path_normcase
6663def os_path_normcase(path: str) -> str:
6664 """Normalize the path's case."""
6665 if not path:
6666 return ''
6667 path = os.path.normcase(path)
6668 if g.isWindows:
6669 path = path.replace('\\', '/')
6670 return path
6671#@+node:ekr.20031218072017.2157: *3* g.os_path_normpath
6672def os_path_normpath(path: str) -> str:
6673 """Normalize the path."""
6674 if not path:
6675 return ''
6676 path = os.path.normpath(path)
6677 # os.path.normpath does the *reverse* of what we want.
6678 if g.isWindows:
6679 path = path.replace('\\', '/').lower() # #2049: ignore case!
6680 return path
6681#@+node:ekr.20180314081254.1: *3* g.os_path_normslashes
6682def os_path_normslashes(path: str) -> str:
6684 # os.path.normpath does the *reverse* of what we want.
6685 if g.isWindows and path:
6686 path = path.replace('\\', '/')
6687 return path
6688#@+node:ekr.20080605064555.2: *3* g.os_path_realpath
6689def os_path_realpath(path: str) -> str:
6690 """Return the canonical path of the specified filename, eliminating any
6691 symbolic links encountered in the path (if they are supported by the
6692 operating system).
6693 """
6694 if not path:
6695 return ''
6696 path = os.path.realpath(path)
6697 # os.path.normpath does the *reverse* of what we want.
6698 if g.isWindows:
6699 path = path.replace('\\', '/')
6700 return path
6701#@+node:ekr.20031218072017.2158: *3* g.os_path_split
6702def os_path_split(path: str) -> Tuple[str, str]:
6703 if not path:
6704 return '', ''
6705 head, tail = os.path.split(path)
6706 return head, tail
6707#@+node:ekr.20031218072017.2159: *3* g.os_path_splitext
6708def os_path_splitext(path: str) -> Tuple[str, str]:
6710 if not path:
6711 return '', ''
6712 head, tail = os.path.splitext(path)
6713 return head, tail
6714#@+node:ekr.20090829140232.6036: *3* g.os_startfile
6715def os_startfile(fname: str) -> None:
6716 #@+others
6717 #@+node:bob.20170516112250.1: *4* stderr2log()
6718 def stderr2log(g: Any, ree: Any, fname: str) -> None:
6719 """ Display stderr output in the Leo-Editor log pane
6721 Arguments:
6722 g: Leo-Editor globals
6723 ree: Read file descriptor for stderr
6724 fname: file pathname
6726 Returns:
6727 None
6728 """
6730 while True:
6731 emsg = ree.read().decode('utf-8')
6732 if emsg:
6733 g.es_print_error(f"xdg-open {fname} caused output to stderr:\n{emsg}")
6734 else:
6735 break
6736 #@+node:bob.20170516112304.1: *4* itPoll()
6737 def itPoll(fname: str, ree: Any, subPopen: Any, g: Any, ito: Any) -> None:
6738 """ Poll for subprocess done
6740 Arguments:
6741 fname: File name
6742 ree: stderr read file descriptor
6743 subPopen: URL open subprocess object
6744 g: Leo-Editor globals
6745 ito: Idle time object for itPoll()
6747 Returns:
6748 None
6749 """
6751 stderr2log(g, ree, fname)
6752 rc = subPopen.poll()
6753 if not rc is None:
6754 ito.stop()
6755 ito.destroy_self()
6756 if rc != 0:
6757 g.es_print(f"xdg-open {fname} failed with exit code {rc}")
6758 stderr2log(g, ree, fname)
6759 ree.close()
6760 #@-others
6761 # pylint: disable=used-before-assignment
6762 if fname.find('"') > -1:
6763 quoted_fname = f"'{fname}'"
6764 else:
6765 quoted_fname = f'"{fname}"'
6766 if sys.platform.startswith('win'):
6767 # pylint: disable=no-member
6768 os.startfile(quoted_fname) # Exists only on Windows.
6769 elif sys.platform == 'darwin':
6770 # From Marc-Antoine Parent.
6771 try:
6772 # Fix bug 1226358: File URL's are broken on MacOS:
6773 # use fname, not quoted_fname, as the argument to subprocess.call.
6774 subprocess.call(['open', fname])
6775 except OSError:
6776 pass # There may be a spurious "Interrupted system call"
6777 except ImportError:
6778 os.system(f"open {quoted_fname}")
6779 else:
6780 try:
6781 ree = None
6782 wre = tempfile.NamedTemporaryFile()
6783 ree = io.open(wre.name, 'rb', buffering=0)
6784 except IOError:
6785 g.trace(f"error opening temp file for {fname!r}")
6786 if ree:
6787 ree.close()
6788 return
6789 try:
6790 subPopen = subprocess.Popen(['xdg-open', fname], stderr=wre, shell=False)
6791 except Exception:
6792 g.es_print(f"error opening {fname!r}")
6793 g.es_exception()
6794 try:
6795 itoPoll = g.IdleTime(
6796 (lambda ito: itPoll(fname, ree, subPopen, g, ito)),
6797 delay=1000,
6798 )
6799 itoPoll.start()
6800 # Let the Leo-Editor process run
6801 # so that Leo-Editor is usable while the file is open.
6802 except Exception:
6803 g.es_exception(f"exception executing g.startfile for {fname!r}")
6804#@+node:ekr.20111115155710.9859: ** g.Parsing & Tokenizing
6805#@+node:ekr.20031218072017.822: *3* g.createTopologyList
6806def createTopologyList(c: Cmdr, root: Pos=None, useHeadlines: bool=False) -> List:
6807 """Creates a list describing a node and all its descendents"""
6808 if not root:
6809 root = c.rootPosition()
6810 v = root
6811 if useHeadlines:
6812 aList = [(v.numberOfChildren(), v.headString()),] # type: ignore
6813 else:
6814 aList = [v.numberOfChildren()] # type: ignore
6815 child = v.firstChild()
6816 while child:
6817 aList.append(g.createTopologyList(c, child, useHeadlines)) # type: ignore
6818 child = child.next()
6819 return aList
6820#@+node:ekr.20111017204736.15898: *3* g.getDocString
6821def getDocString(s: str) -> str:
6822 """Return the text of the first docstring found in s."""
6823 tags = ('"""', "'''")
6824 tag1, tag2 = tags
6825 i1, i2 = s.find(tag1), s.find(tag2)
6826 if i1 == -1 and i2 == -1:
6827 return ''
6828 if i1 > -1 and i2 > -1:
6829 i = min(i1, i2)
6830 else:
6831 i = max(i1, i2)
6832 tag = s[i : i + 3]
6833 assert tag in tags
6834 j = s.find(tag, i + 3)
6835 if j > -1:
6836 return s[i + 3 : j]
6837 return ''
6838#@+node:ekr.20111017211256.15905: *3* g.getDocStringForFunction
6839def getDocStringForFunction(func: Any) -> str:
6840 """Return the docstring for a function that creates a Leo command."""
6842 def name(func: Any) -> str:
6843 return func.__name__ if hasattr(func, '__name__') else '<no __name__>'
6845 def get_defaults(func: str, i: int) -> Any:
6846 defaults = inspect.getfullargspec(func)[3]
6847 return defaults[i]
6849 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252
6850 # Minibuffer commands created by mod_scripting.py have no docstrings.
6851 # Do special cases first.
6853 s = ''
6854 if name(func) == 'minibufferCallback':
6855 func = get_defaults(func, 0)
6856 if hasattr(func, '__doc__') and func.__doc__.strip():
6857 s = func.__doc__
6858 if not s and name(func) == 'commonCommandCallback':
6859 script = get_defaults(func, 1)
6860 s = g.getDocString(script) # Do a text scan for the function.
6861 # Now the general cases. Prefer __doc__ to docstring()
6862 if not s and hasattr(func, '__doc__'):
6863 s = func.__doc__
6864 if not s and hasattr(func, 'docstring'):
6865 s = func.docstring
6866 return s
6867#@+node:ekr.20111115155710.9814: *3* g.python_tokenize (not used)
6868def python_tokenize(s: str) -> List:
6869 """
6870 Tokenize string s and return a list of tokens (kind, value, line_number)
6872 where kind is in ('comment,'id','nl','other','string','ws').
6873 """
6874 result: List[Tuple[str, str, int]] = []
6875 i, line_number = 0, 0
6876 while i < len(s):
6877 progress = j = i
6878 ch = s[i]
6879 if ch == '\n':
6880 kind, i = 'nl', i + 1
6881 elif ch in ' \t':
6882 kind = 'ws'
6883 while i < len(s) and s[i] in ' \t':
6884 i += 1
6885 elif ch == '#':
6886 kind, i = 'comment', g.skip_to_end_of_line(s, i)
6887 elif ch in '"\'':
6888 kind, i = 'string', g.skip_python_string(s, i)
6889 elif ch == '_' or ch.isalpha():
6890 kind, i = 'id', g.skip_id(s, i)
6891 else:
6892 kind, i = 'other', i + 1
6893 assert progress < i and j == progress
6894 val = s[j:i]
6895 assert val
6896 line_number += val.count('\n') # A comment.
6897 result.append((kind, val, line_number),)
6898 return result
6899#@+node:ekr.20040327103735.2: ** g.Scripting
6900#@+node:ekr.20161223090721.1: *3* g.exec_file
6901def exec_file(path: str, d: Dict[str, Any], script: str=None) -> None:
6902 """Simulate python's execfile statement for python 3."""
6903 if script is None:
6904 with open(path) as f:
6905 script = f.read()
6906 exec(compile(script, path, 'exec'), d)
6907#@+node:ekr.20131016032805.16721: *3* g.execute_shell_commands
6908def execute_shell_commands(commands: Any, trace: bool=False) -> None:
6909 """
6910 Execute each shell command in a separate process.
6911 Wait for each command to complete, except those starting with '&'
6912 """
6913 if isinstance(commands, str):
6914 commands = [commands]
6915 for command in commands:
6916 wait = not command.startswith('&')
6917 if trace:
6918 g.trace(command)
6919 if command.startswith('&'):
6920 command = command[1:].strip()
6921 proc = subprocess.Popen(command, shell=True)
6922 if wait:
6923 proc.communicate()
6924#@+node:ekr.20180217113719.1: *3* g.execute_shell_commands_with_options & helpers
6925def execute_shell_commands_with_options(
6926 base_dir: str=None,
6927 c: Cmdr=None,
6928 command_setting: str=None,
6929 commands: List=None,
6930 path_setting: str=None,
6931 trace: bool=False,
6932 warning: str=None,
6933) -> None:
6934 """
6935 A helper for prototype commands or any other code that
6936 runs programs in a separate process.
6938 base_dir: Base directory to use if no config path given.
6939 commands: A list of commands, for g.execute_shell_commands.
6940 commands_setting: Name of @data setting for commands.
6941 path_setting: Name of @string setting for the base directory.
6942 warning: A warning to be printed before executing the commands.
6943 """
6944 base_dir = g.computeBaseDir(c, base_dir, path_setting)
6945 if not base_dir:
6946 return
6947 commands = g.computeCommands(c, commands, command_setting)
6948 if not commands:
6949 return
6950 if warning:
6951 g.es_print(warning)
6952 os.chdir(base_dir) # Can't do this in the commands list.
6953 g.execute_shell_commands(commands, trace=trace)
6954#@+node:ekr.20180217152624.1: *4* g.computeBaseDir
6955def computeBaseDir(c: Cmdr, base_dir: str, path_setting: str) -> Optional[str]:
6956 """
6957 Compute a base_directory.
6958 If given, @string path_setting takes precedence.
6959 """
6960 # Prefer the path setting to the base_dir argument.
6961 if path_setting:
6962 if not c:
6963 g.es_print('@string path_setting requires valid c arg')
6964 return None
6965 # It's not an error for the setting to be empty.
6966 base_dir2 = c.config.getString(path_setting)
6967 if base_dir2:
6968 base_dir2 = base_dir2.replace('\\', '/')
6969 if g.os_path_exists(base_dir2):
6970 return base_dir2
6971 g.es_print(f"@string {path_setting} not found: {base_dir2!r}")
6972 return None
6973 # Fall back to given base_dir.
6974 if base_dir:
6975 base_dir = base_dir.replace('\\', '/')
6976 if g.os_path_exists(base_dir):
6977 return base_dir
6978 g.es_print(f"base_dir not found: {base_dir!r}")
6979 return None
6980 g.es_print(f"Please use @string {path_setting}")
6981 return None
6982#@+node:ekr.20180217153459.1: *4* g.computeCommands
6983def computeCommands(c: Cmdr, commands: List[str], command_setting: str) -> List[str]:
6984 """
6985 Get the list of commands.
6986 If given, @data command_setting takes precedence.
6987 """
6988 if not commands and not command_setting:
6989 g.es_print('Please use commands, command_setting or both')
6990 return []
6991 # Prefer the setting to the static commands.
6992 if command_setting:
6993 if c:
6994 aList = c.config.getData(command_setting)
6995 # It's not an error for the setting to be empty.
6996 # Fall back to the commands.
6997 return aList or commands
6998 g.es_print('@data command_setting requires valid c arg')
6999 return []
7000 return commands
7001#@+node:ekr.20050503112513.7: *3* g.executeFile
7002def executeFile(filename: str, options: str='') -> None:
7003 if not os.access(filename, os.R_OK):
7004 return
7005 fdir, fname = g.os_path_split(filename)
7006 # New in Leo 4.10: alway use subprocess.
7008 def subprocess_wrapper(cmdlst: str) -> Tuple:
7010 p = subprocess.Popen(cmdlst, cwd=fdir,
7011 universal_newlines=True,
7012 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
7013 stdo, stde = p.communicate()
7014 return p.wait(), stdo, stde
7016 rc, so, se = subprocess_wrapper(f"{sys.executable} {fname} {options}")
7017 if rc:
7018 g.pr('return code', rc)
7019 g.pr(so, se)
7020#@+node:ekr.20040321065415: *3* g.find*Node*
7021#@+others
7022#@+node:ekr.20210303123423.3: *4* g.findNodeAnywhere
7023def findNodeAnywhere(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]:
7024 h = headline.strip()
7025 for p in c.all_unique_positions(copy=False):
7026 if p.h.strip() == h:
7027 return p.copy()
7028 if not exact:
7029 for p in c.all_unique_positions(copy=False):
7030 if p.h.strip().startswith(h):
7031 return p.copy()
7032 return None
7033#@+node:ekr.20210303123525.1: *4* g.findNodeByPath
7034def findNodeByPath(c: Cmdr, path: str) -> Optional[Pos]:
7035 """Return the first @<file> node in Cmdr c whose path is given."""
7036 if not os.path.isabs(path): # #2049. Only absolute paths could possibly work.
7037 g.trace(f"path not absolute: {repr(path)}")
7038 return None
7039 path = g.os_path_normpath(path) # #2049. Do *not* use os.path.normpath.
7040 for p in c.all_positions():
7041 if p.isAnyAtFileNode():
7042 if path == g.os_path_normpath(g.fullPath(c, p)): # #2049. Do *not* use os.path.normpath.
7043 return p
7044 return None
7045#@+node:ekr.20210303123423.1: *4* g.findNodeInChildren
7046def findNodeInChildren(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]:
7047 """Search for a node in v's tree matching the given headline."""
7048 p1 = p.copy()
7049 h = headline.strip()
7050 for p in p1.children():
7051 if p.h.strip() == h:
7052 return p.copy()
7053 if not exact:
7054 for p in p1.children():
7055 if p.h.strip().startswith(h):
7056 return p.copy()
7057 return None
7058#@+node:ekr.20210303123423.2: *4* g.findNodeInTree
7059def findNodeInTree(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]:
7060 """Search for a node in v's tree matching the given headline."""
7061 h = headline.strip()
7062 p1 = p.copy()
7063 for p in p1.subtree():
7064 if p.h.strip() == h:
7065 return p.copy()
7066 if not exact:
7067 for p in p1.subtree():
7068 if p.h.strip().startswith(h):
7069 return p.copy()
7070 return None
7071#@+node:ekr.20210303123423.4: *4* g.findTopLevelNode
7072def findTopLevelNode(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]:
7073 h = headline.strip()
7074 for p in c.rootPosition().self_and_siblings(copy=False):
7075 if p.h.strip() == h:
7076 return p.copy()
7077 if not exact:
7078 for p in c.rootPosition().self_and_siblings(copy=False):
7079 if p.h.strip().startswith(h):
7080 return p.copy()
7081 return None
7082#@-others
7083#@+node:EKR.20040614071102.1: *3* g.getScript & helpers
7084def getScript(
7085 c: Cmdr,
7086 p: Pos,
7087 useSelectedText: bool=True,
7088 forcePythonSentinels: bool=True,
7089 useSentinels: bool=True,
7090) -> str:
7091 """
7092 Return the expansion of the selected text of node p.
7093 Return the expansion of all of node p's body text if
7094 p is not the current node or if there is no text selection.
7095 """
7096 w = c.frame.body.wrapper
7097 if not p:
7098 p = c.p
7099 try:
7100 if g.app.inBridge:
7101 s = p.b
7102 elif w and p == c.p and useSelectedText and w.hasSelection():
7103 s = w.getSelectedText()
7104 else:
7105 s = p.b
7106 # Remove extra leading whitespace so the user may execute indented code.
7107 s = textwrap.dedent(s)
7108 s = g.extractExecutableString(c, p, s)
7109 script = g.composeScript(c, p, s,
7110 forcePythonSentinels=forcePythonSentinels,
7111 useSentinels=useSentinels)
7112 except Exception:
7113 g.es_print("unexpected exception in g.getScript")
7114 g.es_exception()
7115 script = ''
7116 return script
7117#@+node:ekr.20170228082641.1: *4* g.composeScript
7118def composeScript(
7119 c: Cmdr,
7120 p: Pos,
7121 s: str,
7122 forcePythonSentinels: bool=True,
7123 useSentinels: bool=True,
7124) -> str:
7125 """Compose a script from p.b."""
7126 # This causes too many special cases.
7127 # if not g.unitTesting and forceEncoding:
7128 # aList = g.get_directives_dict_list(p)
7129 # encoding = scanAtEncodingDirectives(aList) or 'utf-8'
7130 # s = g.insertCodingLine(encoding,s)
7131 if not s.strip():
7132 return ''
7133 at = c.atFileCommands # type:ignore
7134 old_in_script = g.app.inScript
7135 try:
7136 # #1297: set inScript flags.
7137 g.app.inScript = g.inScript = True
7138 g.app.scriptDict["script1"] = s
7139 # Important: converts unicode to utf-8 encoded strings.
7140 script = at.stringToString(p.copy(), s,
7141 forcePythonSentinels=forcePythonSentinels,
7142 sentinels=useSentinels)
7143 # Important, the script is an **encoded string**, not a unicode string.
7144 script = script.replace("\r\n", "\n") # Use brute force.
7145 g.app.scriptDict["script2"] = script
7146 finally:
7147 g.app.inScript = g.inScript = old_in_script
7148 return script
7149#@+node:ekr.20170123074946.1: *4* g.extractExecutableString
7150def extractExecutableString(c: Cmdr, p: Pos, s: str) -> str:
7151 """
7152 Return all lines for the given @language directive.
7154 Ignore all lines under control of any other @language directive.
7155 """
7156 #
7157 # Rewritten to fix #1071.
7158 if g.unitTesting:
7159 return s # Regretable, but necessary.
7160 #
7161 # Return s if no @language in effect. Should never happen.
7162 language = g.scanForAtLanguage(c, p)
7163 if not language:
7164 return s
7165 #
7166 # Return s if @language is unambiguous.
7167 pattern = r'^@language\s+(\w+)'
7168 matches = list(re.finditer(pattern, s, re.MULTILINE))
7169 if len(matches) < 2:
7170 return s
7171 #
7172 # Scan the lines, extracting only the valid lines.
7173 extracting, result = False, []
7174 for i, line in enumerate(g.splitLines(s)):
7175 m = re.match(pattern, line)
7176 if m:
7177 # g.trace(language, m.group(1))
7178 extracting = m.group(1) == language
7179 elif extracting:
7180 result.append(line)
7181 return ''.join(result)
7182#@+node:ekr.20060624085200: *3* g.handleScriptException
7183def handleScriptException(c: Cmdr, p: Pos, script: str, script1: str) -> None:
7184 g.warning("exception executing script")
7185 full = c.config.getBool('show-full-tracebacks-in-scripts')
7186 fileName, n = g.es_exception(full=full)
7187 # Careful: this test is no longer guaranteed.
7188 if p.v.context == c:
7189 try:
7190 c.goToScriptLineNumber(n, p)
7191 #@+<< dump the lines near the error >>
7192 #@+node:EKR.20040612215018: *4* << dump the lines near the error >>
7193 if g.os_path_exists(fileName):
7194 with open(fileName) as f:
7195 lines = f.readlines()
7196 else:
7197 lines = g.splitLines(script)
7198 s = '-' * 20
7199 g.es_print('', s)
7200 # Print surrounding lines.
7201 i = max(0, n - 2)
7202 j = min(n + 2, len(lines))
7203 while i < j:
7204 ch = '*' if i == n - 1 else ' '
7205 s = f"{ch} line {i+1:d}: {lines[i]}"
7206 g.es('', s, newline=False)
7207 i += 1
7208 #@-<< dump the lines near the error >>
7209 except Exception:
7210 g.es_print('Unexpected exception in g.handleScriptException')
7211 g.es_exception()
7212#@+node:ekr.20140209065845.16767: *3* g.insertCodingLine
7213def insertCodingLine(encoding: str, script: str) -> str:
7214 """
7215 Insert a coding line at the start of script s if no such line exists.
7216 The coding line must start with @first because it will be passed to
7217 at.stringToString.
7218 """
7219 if script:
7220 tag = '@first # -*- coding:'
7221 lines = g.splitLines(script)
7222 for s in lines:
7223 if s.startswith(tag):
7224 break
7225 else:
7226 lines.insert(0, f"{tag} {encoding} -*-\n")
7227 script = ''.join(lines)
7228 return script
7229#@+node:ekr.20070524083513: ** g.Unit Tests
7230#@+node:ekr.20210901071523.1: *3* g.run_coverage_tests
7231def run_coverage_tests(module: str='', filename: str='') -> None:
7232 """
7233 Run the coverage tests given by the module and filename strings.
7234 """
7235 unittests_dir = g.os_path_finalize_join(g.app.loadDir, '..', 'unittests')
7236 assert os.path.exists(unittests_dir)
7237 os.chdir(unittests_dir)
7238 prefix = r"python -m pytest --cov-report html --cov-report term-missing --cov "
7239 command = f"{prefix} {module} {filename}"
7240 g.execute_shell_commands(command)
7241#@+node:ekr.20210901065224.1: *3* g.run_unit_tests
7242def run_unit_tests(tests: str=None, verbose: bool=False) -> None:
7243 """
7244 Run the unit tests given by the "tests" string.
7246 Run *all* unit tests if "tests" is not given.
7247 """
7248 if 'site-packages' in __file__:
7249 # Add site-packages to sys.path.
7250 parent_dir = g.os_path_finalize_join(g.app.loadDir, '..', '..')
7251 if parent_dir.endswith('site-packages'):
7252 if parent_dir not in sys.path:
7253 g.trace(f"Append {parent_dir!r} to sys.path")
7254 sys.path.append(parent_dir)
7255 else:
7256 g.trace('Can not happen: wrong parent directory', parent_dir)
7257 return
7258 # Run tests in site-packages/leo
7259 os.chdir(g.os_path_finalize_join(g.app.loadDir, '..'))
7260 else:
7261 # Run tests in leo-editor.
7262 os.chdir(g.os_path_finalize_join(g.app.loadDir, '..', '..'))
7263 verbosity = '-v' if verbose else ''
7264 command = f"{sys.executable} -m unittest {verbosity} {tests or ''} "
7265 g.execute_shell_commands(command)
7266#@+node:ekr.20120311151914.9916: ** g.Urls & UNLs
7267unl_regex = re.compile(r'\bunl:.*$')
7269kinds = '(http|https|file|mailto|ftp|gopher|news|nntp|prospero|telnet|wais)'
7270url_regex = re.compile(fr"""{kinds}://[^\s'"`>]+[\w=/]""")
7271#@+node:ekr.20120320053907.9776: *3* g.computeFileUrl
7272def computeFileUrl(fn: str, c: Cmdr=None, p: Pos=None) -> str:
7273 """
7274 Compute finalized url for filename fn.
7275 """
7276 # First, replace special characters (especially %20, by their equivalent).
7277 url = urllib.parse.unquote(fn)
7278 # Finalize the path *before* parsing the url.
7279 i = url.find('~')
7280 if i > -1:
7281 # Expand '~'.
7282 path = url[i:]
7283 path = g.os_path_expanduser(path)
7284 # #1338: This is way too dangerous, and a serious security violation.
7285 # path = c.os_path_expandExpression(path)
7286 path = g.os_path_finalize(path)
7287 url = url[:i] + path
7288 else:
7289 tag = 'file://'
7290 tag2 = 'file:///'
7291 if sys.platform.startswith('win') and url.startswith(tag2):
7292 path = url[len(tag2) :].lstrip()
7293 elif url.startswith(tag):
7294 path = url[len(tag) :].lstrip()
7295 else:
7296 path = url
7297 # #1338: This is way too dangerous, and a serious security violation.
7298 # path = c.os_path_expandExpression(path)
7299 # Handle ancestor @path directives.
7300 if c and c.openDirectory:
7301 base = c.getNodePath(p)
7302 path = g.os_path_finalize_join(c.openDirectory, base, path)
7303 else:
7304 path = g.os_path_finalize(path)
7305 url = f"{tag}{path}"
7306 return url
7307#@+node:ekr.20190608090856.1: *3* g.es_clickable_link
7308def es_clickable_link(c: Cmdr, p: Pos, line_number: int, message: str) -> None:
7309 """
7310 Write a clickable message to the given line number of p.b.
7312 Negative line numbers indicate global lines.
7314 """
7315 unl = p.get_UNL()
7316 c.frame.log.put(message.strip() + '\n', nodeLink=f"{unl}::{line_number}")
7317#@+node:tbrown.20140311095634.15188: *3* g.findUNL & helpers
7318def findUNL(unlList1: List[str], c: Cmdr) -> Optional[Pos]:
7319 """
7320 Find and move to the unl given by the unlList in the commander c.
7321 Return the found position, or None.
7322 """
7323 # Define the unl patterns.
7324 old_pat = re.compile(r'^(.*):(\d+),?(\d+)?,?([-\d]+)?,?(\d+)?$') # ':' is the separator.
7325 new_pat = re.compile(r'^(.*?)(::)([-\d]+)?$') # '::' is the separator.
7327 #@+others # Define helper functions
7328 #@+node:ekr.20220213142925.1: *4* function: convert_unl_list
7329 def convert_unl_list(aList: List[str]) -> List[str]:
7330 """
7331 Convert old-style UNLs to new UNLs, retaining line numbers if possible.
7332 """
7333 result = []
7334 for s in aList:
7335 # Try to get the line number.
7336 for m, line_group in (
7337 (old_pat.match(s), 4),
7338 (new_pat.match(s), 3),
7339 ):
7340 if m:
7341 try:
7342 n = int(m.group(line_group))
7343 result.append(f"{m.group(1)}::{n}")
7344 continue
7345 except Exception:
7346 pass
7347 # Finally, just add the whole UNL.
7348 result.append(s)
7349 return result
7350 #@+node:ekr.20220213142735.1: *4* function: full_match
7351 def full_match(p: Pos) -> bool:
7352 """Return True if the headlines of p and all p's parents match unlList."""
7353 # Careful: make copies.
7354 aList, p1 = unlList[:], p.copy()
7355 while aList and p1:
7356 m = new_pat.match(aList[-1])
7357 if m and m.group(1).strip() != p1.h.strip():
7358 return False
7359 if not m and aList[-1].strip() != p1.h.strip():
7360 return False
7361 aList.pop()
7362 p1.moveToParent()
7363 return not aList
7364 #@-others
7366 unlList = convert_unl_list(unlList1)
7367 if not unlList:
7368 return None
7369 # Find all target headlines.
7370 targets = []
7371 m = new_pat.match(unlList[-1])
7372 target = m and m.group(1) or unlList[-1]
7373 targets.append(target)
7374 targets.extend(unlList[:-1])
7375 # Find all target positions. Prefer later positions.
7376 positions = list(reversed(list(z for z in c.all_positions() if z.h.strip() in targets)))
7377 while unlList:
7378 for p in positions:
7379 p1 = p.copy()
7380 if full_match(p):
7381 assert p == p1, (p, p1)
7382 n = 0 # The default line number.
7383 # Parse the last target.
7384 m = new_pat.match(unlList[-1])
7385 if m:
7386 line = m.group(3)
7387 try:
7388 n = int(line)
7389 except(TypeError, ValueError):
7390 g.trace('bad line number', line)
7391 if n == 0:
7392 c.redraw(p)
7393 elif n < 0:
7394 p, offset, ok = c.gotoCommands.find_file_line(-n, p) # Calls c.redraw().
7395 return p if ok else None
7396 elif n > 0:
7397 insert_point = sum(len(i) + 1 for i in p.b.split('\n')[: n - 1])
7398 c.redraw(p)
7399 c.frame.body.wrapper.setInsertPoint(insert_point)
7400 c.frame.bringToFront()
7401 c.bodyWantsFocusNow()
7402 return p
7403 # Not found. Pop the first parent from unlList.
7404 unlList.pop(0)
7405 return None
7406#@+node:ekr.20120311151914.9917: *3* g.getUrlFromNode
7407def getUrlFromNode(p: Pos) -> Optional[str]:
7408 """
7409 Get an url from node p:
7410 1. Use the headline if it contains a valid url.
7411 2. Otherwise, look *only* at the first line of the body.
7412 """
7413 if not p:
7414 return None
7415 c = p.v.context
7416 assert c
7417 table = [p.h, g.splitLines(p.b)[0] if p.b else '']
7418 table = [s[4:] if g.match_word(s, 0, '@url') else s for s in table]
7419 table = [s.strip() for s in table if s.strip()]
7420 # First, check for url's with an explicit scheme.
7421 for s in table:
7422 if g.isValidUrl(s):
7423 return s
7424 # Next check for existing file and add a file:// scheme.
7425 for s in table:
7426 tag = 'file://'
7427 url = computeFileUrl(s, c=c, p=p)
7428 if url.startswith(tag):
7429 fn = url[len(tag) :].lstrip()
7430 fn = fn.split('#', 1)[0]
7431 if g.os_path_isfile(fn):
7432 # Return the *original* url, with a file:// scheme.
7433 # g.handleUrl will call computeFileUrl again.
7434 return 'file://' + s
7435 # Finally, check for local url's.
7436 for s in table:
7437 if s.startswith("#"):
7438 return s
7439 return None
7440#@+node:ekr.20170221063527.1: *3* g.handleUnl
7441def handleUnl(unl: str, c: Cmdr) -> Any:
7442 """
7443 Handle a Leo UNL. This must *never* open a browser.
7445 Return the commander for the found UNL, or None.
7447 Redraw the commander if the UNL is found.
7448 """
7449 if not unl:
7450 return None
7451 unll = unl.lower()
7452 if unll.startswith('unl://'):
7453 unl = unl[6:]
7454 elif unll.startswith('file://'):
7455 unl = unl[7:]
7456 unl = unl.strip()
7457 if not unl:
7458 return None
7459 unl = g.unquoteUrl(unl)
7460 # Compute path and unl.
7461 if '#' not in unl and '-->' not in unl:
7462 # The path is the entire unl.
7463 path, unl = unl, None
7464 elif '#' not in unl:
7465 # The path is empty.
7466 # Move to the unl in *this* commander.
7467 p = g.findUNL(unl.split("-->"), c)
7468 if p:
7469 c.redraw(p)
7470 return c
7471 else:
7472 path, unl = unl.split('#', 1)
7473 if unl and not path: # #2407
7474 # Move to the unl in *this* commander.
7475 p = g.findUNL(unl.split("-->"), c)
7476 if p:
7477 c.redraw(p)
7478 return c
7479 if c:
7480 base = g.os_path_dirname(c.fileName())
7481 c_path = g.os_path_finalize_join(base, path)
7482 else:
7483 c_path = None
7484 # Look for the file in various places.
7485 table = (
7486 c_path,
7487 g.os_path_finalize_join(g.app.loadDir, '..', path),
7488 g.os_path_finalize_join(g.app.loadDir, '..', '..', path),
7489 g.os_path_finalize_join(g.app.loadDir, '..', 'core', path),
7490 g.os_path_finalize_join(g.app.loadDir, '..', 'config', path),
7491 g.os_path_finalize_join(g.app.loadDir, '..', 'dist', path),
7492 g.os_path_finalize_join(g.app.loadDir, '..', 'doc', path),
7493 g.os_path_finalize_join(g.app.loadDir, '..', 'test', path),
7494 g.app.loadDir,
7495 g.app.homeDir,
7496 )
7497 for path2 in table:
7498 if path2 and path2.lower().endswith('.leo') and os.path.exists(path2):
7499 path = path2
7500 break
7501 else:
7502 g.es_print('path not found', repr(path))
7503 return None
7504 # End editing in *this* outline, so typing in the new outline works.
7505 c.endEditing()
7506 c.redraw()
7507 # Open the path.
7508 c2 = g.openWithFileName(path, old_c=c)
7509 if not c2:
7510 return None
7511 # Find and redraw.
7512 # #2445: Default to c2.rootPosition().
7513 p = g.findUNL(unl.split("-->"), c2) or c2.rootPosition()
7514 c2.redraw(p)
7515 c2.bringToFront()
7516 c2.bodyWantsFocusNow()
7517 return c2
7518#@+node:tbrown.20090219095555.63: *3* g.handleUrl & helpers
7519def handleUrl(url: str, c: Cmdr=None, p: Pos=None) -> Any:
7520 """Open a url or a unl."""
7521 if c and not p:
7522 p = c.p
7523 urll = url.lower()
7524 if urll.startswith('@url'):
7525 url = url[4:].lstrip()
7526 if (
7527 urll.startswith('unl://') or
7528 urll.startswith('file://') and url.find('-->') > -1 or
7529 urll.startswith('#')
7530 ):
7531 return g.handleUnl(url, c)
7532 try:
7533 return g.handleUrlHelper(url, c, p)
7534 except Exception:
7535 g.es_print("g.handleUrl: exception opening", repr(url))
7536 g.es_exception()
7537 return None
7538#@+node:ekr.20170226054459.1: *4* g.handleUrlHelper
7539def handleUrlHelper(url: str, c: Cmdr, p: Pos) -> None:
7540 """Open a url. Most browsers should handle:
7541 ftp://ftp.uu.net/public/whatever
7542 http://localhost/MySiteUnderDevelopment/index.html
7543 file:///home/me/todolist.html
7544 """
7545 tag = 'file://'
7546 original_url = url
7547 if url.startswith(tag) and not url.startswith(tag + '#'):
7548 # Finalize the path *before* parsing the url.
7549 url = g.computeFileUrl(url, c=c, p=p)
7550 parsed = urlparse.urlparse(url)
7551 if parsed.netloc:
7552 leo_path = os.path.join(parsed.netloc, parsed.path)
7553 # "readme.txt" gets parsed into .netloc...
7554 else:
7555 leo_path = parsed.path
7556 if leo_path.endswith('\\'):
7557 leo_path = leo_path[:-1]
7558 if leo_path.endswith('/'):
7559 leo_path = leo_path[:-1]
7560 if parsed.scheme == 'file' and leo_path.endswith('.leo'):
7561 g.handleUnl(original_url, c)
7562 elif parsed.scheme in ('', 'file'):
7563 unquote_path = g.unquoteUrl(leo_path)
7564 if g.unitTesting:
7565 pass
7566 elif g.os_path_exists(leo_path):
7567 g.os_startfile(unquote_path)
7568 else:
7569 g.es(f"File '{leo_path}' does not exist")
7570 else:
7571 if g.unitTesting:
7572 pass
7573 else:
7574 # Mozilla throws a weird exception, then opens the file!
7575 try:
7576 webbrowser.open(url)
7577 except Exception:
7578 pass
7579#@+node:ekr.20170226060816.1: *4* g.traceUrl
7580def traceUrl(c: Cmdr, path: str, parsed: Any, url: str) -> None:
7582 print()
7583 g.trace('url ', url)
7584 g.trace('c.frame.title', c.frame.title)
7585 g.trace('path ', path)
7586 g.trace('parsed.fragment', parsed.fragment)
7587 g.trace('parsed.netloc', parsed.netloc)
7588 g.trace('parsed.path ', parsed.path)
7589 g.trace('parsed.scheme', repr(parsed.scheme))
7590#@+node:ekr.20120311151914.9918: *3* g.isValidUrl
7591def isValidUrl(url: str) -> bool:
7592 """Return true if url *looks* like a valid url."""
7593 table = (
7594 'file', 'ftp', 'gopher', 'hdl', 'http', 'https', 'imap',
7595 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu',
7596 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', 'wais',
7597 )
7598 if url.lower().startswith('unl://') or url.startswith('#'):
7599 # All Leo UNL's.
7600 return True
7601 if url.startswith('@'):
7602 return False
7603 parsed = urlparse.urlparse(url)
7604 scheme = parsed.scheme
7605 for s in table:
7606 if scheme.startswith(s):
7607 return True
7608 return False
7609#@+node:ekr.20120315062642.9744: *3* g.openUrl
7610def openUrl(p: Pos) -> None:
7611 """
7612 Open the url of node p.
7613 Use the headline if it contains a valid url.
7614 Otherwise, look *only* at the first line of the body.
7615 """
7616 if p:
7617 url = g.getUrlFromNode(p)
7618 if url:
7619 c = p.v.context
7620 if not g.doHook("@url1", c=c, p=p, url=url):
7621 g.handleUrl(url, c=c, p=p)
7622 g.doHook("@url2", c=c, p=p, url=url)
7623#@+node:ekr.20110605121601.18135: *3* g.openUrlOnClick (open-url-under-cursor)
7624def openUrlOnClick(event: Any, url: str=None) -> Optional[str]:
7625 """Open the URL under the cursor. Return it for unit testing."""
7626 # This can be called outside Leo's command logic, so catch all exceptions.
7627 try:
7628 return openUrlHelper(event, url)
7629 except Exception:
7630 g.es_exception()
7631 return None
7632#@+node:ekr.20170216091704.1: *4* g.openUrlHelper
7633def openUrlHelper(event: Any, url: str=None) -> Optional[str]:
7634 """Open the UNL or URL under the cursor. Return it for unit testing."""
7635 c = getattr(event, 'c', None)
7636 if not c:
7637 return None
7638 w = getattr(event, 'w', c.frame.body.wrapper)
7639 if not g.app.gui.isTextWrapper(w):
7640 g.internalError('must be a text wrapper', w)
7641 return None
7642 setattr(event, 'widget', w)
7643 # Part 1: get the url.
7644 if url is None:
7645 s = w.getAllText()
7646 ins = w.getInsertPoint()
7647 i, j = w.getSelectionRange()
7648 if i != j:
7649 return None # So find doesn't open the url.
7650 row, col = g.convertPythonIndexToRowCol(s, ins)
7651 i, j = g.getLine(s, ins)
7652 line = s[i:j]
7654 # Navigation target types:
7655 #@+<< gnx >>
7656 #@+node:tom.20220328142302.1: *5* << gnx >>
7657 match = target = None
7658 for match in GNXre.finditer(line):
7659 # Don't open if we click after the gnx.
7660 if match.start() <= col < match.end():
7661 target = match.group()
7662 break
7664 if target:
7665 # pylint: disable=undefined-loop-variable
7666 found_gnx = target_is_self = False
7667 if c.p.gnx == target:
7668 found_gnx = target_is_self = True
7669 else:
7670 for p in c.all_unique_positions():
7671 if p.v.gnx == target:
7672 found_gnx = True
7673 break
7674 if found_gnx:
7675 if not target_is_self:
7676 c.selectPosition(p)
7677 c.redraw()
7678 return target
7679 #@-<< gnx >>
7680 #@+<< section ref >>
7681 #@+node:tom.20220328141455.1: *5* << section ref >>
7682 # Navigate to section reference if one was clicked.
7683 l_ = line.strip()
7684 if l_.startswith('<<') and l_.endswith('>>'):
7685 p = c.p
7686 px = None
7687 for p1 in p.subtree():
7688 if p1.h.strip() == l_:
7689 px = p1
7690 break
7691 if px:
7692 c.selectPosition(px)
7693 c.redraw()
7694 #@-<< section ref >>
7695 #@+<< url or unl >>
7696 #@+node:tom.20220328141544.1: *5* << url or unl >>
7697 # Find the url on the line.
7698 for match in g.url_regex.finditer(line):
7699 # Don't open if we click after the url.
7700 if match.start() <= col < match.end():
7701 url = match.group()
7702 if g.isValidUrl(url):
7703 break
7704 else:
7705 # Look for the unl:
7706 for match in g.unl_regex.finditer(line):
7707 # Don't open if we click after the unl.
7708 if match.start() <= col < match.end():
7709 unl = match.group()
7710 g.handleUnl(unl, c)
7711 return None
7712 #@-<< url or unl >>
7714 elif not isinstance(url, str):
7715 url = url.toString()
7716 url = g.toUnicode(url) # #571
7717 if url and g.isValidUrl(url):
7718 # Part 2: handle the url
7719 p = c.p
7720 if not g.doHook("@url1", c=c, p=p, url=url):
7721 g.handleUrl(url, c=c, p=p)
7722 g.doHook("@url2", c=c, p=p)
7723 return url
7724 # Part 3: call find-def.
7725 if not w.hasSelection():
7726 c.editCommands.extendToWord(event, select=True)
7727 word = w.getSelectedText().strip()
7728 if not word:
7729 return None
7730 p, pos, newpos = c.findCommands.find_def_strict(event)
7731 if p:
7732 return None
7733 # Part 4: #2546: look for a file name.
7734 s = w.getAllText()
7735 i, j = w.getSelectionRange()
7736 m = re.match(r'(\w+)\.(\w){1,4}\b', s[i:])
7737 if not m:
7738 return None
7739 # Find the first node whose headline ends with the filename.
7740 filename = m.group(0)
7741 for p in c.all_unique_positions():
7742 if p.h.strip().endswith(filename):
7743 # Set the find text.
7744 c.findCommands.ftm.set_find_text(filename)
7745 # Select.
7746 c.redraw(p)
7747 break
7748 return None
7749#@+node:ekr.20170226093349.1: *3* g.unquoteUrl
7750def unquoteUrl(url: str) -> str:
7751 """Replace special characters (especially %20, by their equivalent)."""
7752 return urllib.parse.unquote(url)
7753#@-others
7754# set g when the import is about to complete.
7755g: Any = sys.modules.get('leo.core.leoGlobals')
7756assert g, sorted(sys.modules.keys())
7757if __name__ == '__main__':
7758 unittest.main()
7760#@@language python
7761#@@tabwidth -4
7762#@@pagewidth 70
7763#@-leo