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

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. 

7 

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] 

92 

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 

110 

111global_commands_dict = {} 

112 

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. 

144 

145# Most code need only know about the *existence* of these patterns. 

146 

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. 

149 

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. 

177 

178 This is the recommended way of defining all new commands, including 

179 commands that could befined inside a class. The typical usage is: 

180 

181 @g.command('command-name') 

182 def A_Command(event): 

183 c = event.get('c') 

184 ... 

185 

186 g can *not* be used anywhere in this class! 

187 """ 

188 

189 def __init__(self, name: str, **kwargs: Any) -> None: 

190 """Ctor for command decorator class.""" 

191 self.name = name 

192 

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 

204 

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. 

217 

218 Usage: 

219 

220 @g.command('command-name') 

221 def command_name(self, *args, **kwargs): 

222 ... 

223 

224 The decorator injects command_name into the Commander class and calls 

225 funcToMethod so the ivar will be injected in all future commanders. 

226 

227 g can *not* be used anywhere in this class! 

228 """ 

229 

230 def __init__(self, name: str, **kwargs: Any) -> None: 

231 """Ctor for command decorator class.""" 

232 self.name = name 

233 

234 def __call__(self, func: Callable) -> Callable: 

235 """Register command for all future commanders.""" 

236 

237 def commander_command_wrapper(event: Any) -> None: 

238 c = event.get('c') 

239 method = getattr(c, func.__name__, None) 

240 method(event=event) 

241 

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 

256 

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. 

284 

285 Don't even think about removing the @cmd decorators! 

286 See https://github.com/leo-editor/leo-editor/issues/325 

287 """ 

288 

289 def _decorator(func: Callable) -> Callable: 

290 

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

303 

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. 

310 

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

320 

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]*$') 

328 

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. 

352 

353 The main backup directory is computed as follows: 

354 

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. 

384 

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

414 

415 __str__ = __repr__ 

416 

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 

436 

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: 

443 

444 Create a Bunch whenever you want to group a few variables: 

445 

446 point = Bunch(datum=y, squared=y*y, coord=x) 

447 

448 You can read/write the named attributes you just created, add others, 

449 del some of them, etc:: 

450 

451 if point.squared > threshold: 

452 point.isok = True 

453 """ 

454 

455 def __init__(self, **keywords: Any) -> None: 

456 self.__dict__.update(keywords) 

457 

458 def __repr__(self) -> str: 

459 return self.toString() 

460 

461 def ivars(self) -> List: 

462 return sorted(self.__dict__) 

463 

464 def keys(self) -> List: 

465 return sorted(self.__dict__) 

466 

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' 

477 

478 # Used by new undo code. 

479 

480 def __setitem__(self, key: str, value: Any) -> Any: 

481 """Support aBunch[key] = val""" 

482 return operator.setitem(self.__dict__, key, value) 

483 

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) 

488 

489 def get(self, key: str, theDefault: Any=None) -> Any: 

490 return self.__dict__.get(key, theDefault) 

491 

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__ 

495 

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. 

501 

502 If tkinter doesn't exist (#2512), this class just prints the message 

503 passed to the ctor. 

504 

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. 

533 

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. 

588 

589 

590class GeneralSetting: 

591 """A class representing any kind of setting except shortcuts.""" 

592 

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 

612 

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

619 

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. 

626 

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: 

632 

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 

642 

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 

649 

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 

656 

657 def __le__(self, other: Any) -> bool: 

658 return self.__lt__(other) or self.__eq__(other) 

659 

660 def __ne__(self, other: Any) -> bool: 

661 return not self.__eq__(other) 

662 

663 def __gt__(self, other: Any) -> bool: 

664 return not self.__lt__(other) and not self.__eq__(other) 

665 

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. 

670 

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

676 

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: 

692 

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. 

834 

835 User settings might specify an already-shifted key, which is not an error. 

836 

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. 

876 

877 def find(self, pattern: str) -> int: 

878 return self.s.find(pattern) 

879 

880 def lower(self) -> str: 

881 return self.s.lower() 

882 

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. 

920 

921 A plain key is a key that can be inserted into text. 

922 

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 

943 

944 def isPlainNumPad(self) -> bool: 

945 return ( 

946 self.isNumPadKey() and 

947 len(self.s.replace('Keypad+', '')) == 1 

948 ) 

949 

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: 

954 

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 

1012 

1013def isStroke(obj: Any) -> bool: 

1014 return isinstance(obj, KeyStroke) 

1015 

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. 

1022 

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. 

1123 

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. 

1128 

1129 Returns 

1130 new_left, new_right, bracket_char, index_of_bracket_char 

1131 if expansion succeeds, otherwise 

1132 None, None, None, None 

1133 

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 

1371 

1372 def run(self) -> None: 

1373 """The driver for the MatchBrackets class. 

1374 

1375 With no selected range: find the nearest bracket and select from 

1376 it to it's match, moving cursor to match. 

1377 

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 

1397 

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 

1409 

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) 

1434 

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. 

1450 

1451 This is deprecated, use leoNodes.PosList instead! 

1452 

1453 # Creates a PosList containing all positions in c. 

1454 aList = g.PosList(c) 

1455 

1456 # Creates a PosList from aList2. 

1457 aList = g.PosList(c,aList2) 

1458 

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) 

1465 

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 

1494 

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

1520 

1521 def __init__(self, s: str) -> None: 

1522 self.lines = g.splitLines(s) 

1523 self.i = 0 

1524 

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 

1532 

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. 

1549 

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: 

1577 

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

1588 

1589# Create two redirection objects, one for each stream. 

1590 

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. 

1598 

1599def redirectStderr() -> None: 

1600 global redirectStdErrObj 

1601 redirectStdErrObj.redirect(stdout=False) 

1602 

1603def redirectStdout() -> None: 

1604 global redirectStdOutObj 

1605 redirectStdOutObj.redirect() 

1606#@+node:ekr.20041012090942.1: *5* restoreStderr & restoreStdout 

1607# Restore standard streams. 

1608 

1609def restoreStderr() -> None: 

1610 global redirectStdErrObj 

1611 redirectStdErrObj.undirect(stdout=False) 

1612 

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

1620 

1621def stdOutIsRedirected() -> bool: 

1622 global redirectStdOutObj 

1623 return redirectStdOutObj.isRedirected() 

1624#@+node:ekr.20041012090942.3: *5* rawPrint 

1625# Send output to original stdout. 

1626 

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. 

1636 

1637 This class should work in any environment containing the re, os and sys modules. 

1638 

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

1642 

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. 

1648 

1649 Enabling and disabling depends on the order of arguments in the pattern 

1650 list. Consider the arguments for the Rope trace:: 

1651 

1652 patterns=['+.*','+:.*', 

1653 '-:.*\\lib\\.*','+:.*rope.*','-:.*leoGlobals.py', 

1654 '-:.*worder.py','-:.*prefs.py','-:.*resources.py',]) 

1655 

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. 

1660 

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. 

1664 

1665 Usage: 

1666 

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] = [] 

1786 

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 

1870 

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. 

1926 

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 

1941 

1942 def ignore_file() -> None: 

1943 if not base_name in self.ignored_files: 

1944 self.ignored_files.append(base_name) 

1945 

1946 def ignore_function() -> None: 

1947 if function_name not in self.ignored_functions: 

1948 self.ignored_functions.append(function_name) 

1949 

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. 

2023 

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

2042 

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

2075 

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

2083 

2084 title = 'Enter Leo id' 

2085 

2086 def __init__(self) -> None: 

2087 super().__init__(self.title, self.message) 

2088 self.val = '' 

2089 

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. 

2120 

2121 To trace a function and its callers, put the following at the function's start: 

2122 

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 

2234 

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 

2241 

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] = {} 

2246 

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 

2285 

2286 

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: 

2413 

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

2421 

2422 Overrides the following standard methods: 

2423 

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

2430 

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 ) 

2446 

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) 

2479 

2480 def _checkValType(self, val: Any) -> None: 

2481 if val.__class__ != self.valType: 

2482 self._reportTypeError(val, self.valType) 

2483 

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) 

2497 

2498 def items(self) -> Any: 

2499 return self.d.items() 

2500 

2501 def keys(self) -> Any: 

2502 return self.d.keys() 

2503 

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 

2512 

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 

2519 

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 

2533 

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: 

2543 

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: 

2553 

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) 

2572 

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: 

2579 

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. 

2610 

2611 excludeCaller: True (the default), g.callers itself is not on the list. 

2612 

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 

2664 

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

2694 

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) 

2725 

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) 

2730 

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. 

2743 

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] 

2752 

2753# Important: getLine is a completely different function. 

2754# getLine = get_line 

2755 

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] 

2763 

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 

2772 

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 

2882 

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

2893 

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: 

2923 

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. 

2934 

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 

2942 

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: 

2970 

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: 

2975 

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. 

2988 

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: 

2998 

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. 

3005 

3006 Here is the recommended code to gather stats for one method/function: 

3007 

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

3043 

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

3048 

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

3053 

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. 

3127 

3128 v0 = p.v 

3129 

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 

3137 

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. 

3162 

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

3173 

3174def get_directives_dict(p: Pos, root: Any=None) -> Dict[str, str]: 

3175 """ 

3176 Scan p for Leo directives found in globalDirectiveList. 

3177 

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. 

3216 

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. 

3230 

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] 

3237 

3238 # The same generator as in v.setAllAncestorAtFileNodesDirty. 

3239 # Original idea by Виталије Милошевић (Vitalije Milosevic). 

3240 # Modified by EKR. 

3241 

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) 

3250 

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 

3271 

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. 

3310 

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. 

3368 

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 

3428 

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 

3445 

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 

3464 

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. 

3488 

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. 

3524 

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:]. 

3571 

3572 The @language may have been stripped away. 

3573 

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) 

3616 

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. 

3628 

3629def computeGlobalConfigDir() -> str: 

3630 return g.app.loadManager.computeGlobalConfigDir() 

3631 

3632def computeHomeDir() -> str: 

3633 return g.app.loadManager.computeHomeDir() 

3634 

3635def computeLeoDir() -> str: 

3636 return g.app.loadManager.computeLeoDir() 

3637 

3638def computeLoadDir() -> str: 

3639 return g.app.loadManager.computeLoadDir() 

3640 

3641def computeMachineName() -> str: 

3642 return g.app.loadManager.computeMachineName() 

3643 

3644def computeStandardDirectories() -> str: 

3645 return g.app.loadManager.computeStandardDirectories() 

3646#@+node:ekr.20031218072017.3103: *3* g.computeWindowTitle 

3647def computeWindowTitle(fileName: str) -> str: 

3648 

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) 

3667 

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: 

3698 

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. 

3755 

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. 

3776 

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) 

3832 

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 

3843 

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. 

3867 

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. 

3893 

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. 

3920 

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 

3987 

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: 

4001 

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

4036 

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. 

4073 

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. 

4097 

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. 

4127 

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: 

4135 

4136 # A useful default predicate for python. 

4137 # pylint: disable=function-redefined 

4138 

4139 def predicate(p: Pos) -> bool: 

4140 return p.isAnyAtFileNode() and p.h.strip().endswith('.py') 

4141 

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. 

4168 

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. 

4176 

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

4196 

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 

4212 

4213def skip_braces(s: str, i: int) -> int: 

4214 """ 

4215 Skips from the opening to the matching brace. 

4216 

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

4253 

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: 

4339 

4340 <<<EOS 

4341 This is my string. 

4342 It is mine. I own it. 

4343 No one else has it. 

4344 EOS 

4345 

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. 

4384 

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 

4408 

4409def skip_pp_part(s: str, i: int) -> Tuple[int, int]: 

4410 

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. 

4440 

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! 

4486 

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. 

4491 

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 == ' ' 

4534 

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. 

4539 

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: 

4555 

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) 

4570 

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 

4577 

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 

4608 

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 

4618 

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 

4628 

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. 

4662 

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

4678 

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 

4723 

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' 

4735 

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. 

4794 

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: 

4800 

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

4893 

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: 

4914 

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 '', '', '' 

4952 

4953 info = [g.toUnicode(z) for z in s.splitlines()] 

4954 

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

4961 

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. 

4999 

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. 

5030 

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 

5080 

5081# This dummy definition keeps pylint happy. 

5082# Plugins can change this. 

5083 

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. 

5093 

5094 Returns the value returned by the hook routine, or None if the there is 

5095 an exception. 

5096 

5097 We look for a hook routine in three places: 

5098 1. c.hookFunction 

5099 2. app.hookFunction 

5100 3. leoPlugins.doPlugins() 

5101 

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) 

5135 

5136def registerExclusiveHandler(tags: List[str], fn: str) -> Any: 

5137 pc = g.app.pluginsController 

5138 return pc.registerExclusiveHandler(tags, fn) 

5139 

5140def registerHandler(tags: Any, fn: Any) -> Any: 

5141 pc = g.app.pluginsController 

5142 return pc.registerHandler(tags, fn) 

5143 

5144def plugin_signon(module_name: str, verbose: bool=False) -> Any: 

5145 pc = g.app.pluginsController 

5146 return pc.plugin_signon(module_name, verbose) 

5147 

5148def unloadOnePlugin(moduleOrFileName: str, verbose: bool=False) -> Any: 

5149 pc = g.app.pluginsController 

5150 return pc.unloadOnePlugin(moduleOrFileName, verbose) 

5151 

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) 

5159 

5160def getLoadedPlugins() -> List: 

5161 pc = g.app.pluginsController 

5162 return pc.getLoadedPlugins() 

5163 

5164def getPluginModule(moduleName: str) -> Any: 

5165 pc = g.app.pluginsController 

5166 return pc.getPluginModule(moduleName) 

5167 

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. 

5184 

5185 The IdleTime class executes a handler with a given delay at idle time. 

5186 The handler takes a single argument, the IdleTime instance:: 

5187 

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

5195 

5196 # Execute handler every 500 msec. at idle time. 

5197 timer = g.IdleTime(handler,delay=500) 

5198 if timer: timer.start() 

5199 

5200 Timer instances are completely independent:: 

5201 

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

5208 

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

5215 

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 

5309 

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. 

5336 

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 

5366 

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 

5372 

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 

5386 

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. 

5389 

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! 

5406 

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

5413 

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] = {} 

5450 

5451def checkUnicode(s: str, encoding: str=None) -> str: 

5452 """ 

5453 Warn when converting bytes. Report *all* errors. 

5454 

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 == '_')) 

5539 

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. 

5547 

5548 If there is no BOM, return (None,s) 

5549 

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. 

5584 

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. 

5616 

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. 

5628 

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. 

5641 

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 

5664 

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. 

5740 

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 

5753 

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. 

5776 

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! 

5797 

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. 

5805 

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

5821 

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) 

5874 

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) 

5880 

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) 

5889 

5890def error(*args: Any, **keys: Any) -> None: 

5891 g.es_print(color='error', *args, **keys) 

5892 

5893def note(*args: Any, **keys: Any) -> None: 

5894 g.es_print(color='note', *args, **keys) 

5895 

5896def red(*args: Any, **keys: Any) -> None: 

5897 g.es_print(color='red', *args, **keys) 

5898 

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

5950 

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) 

5967 

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 

6004 

6005def es_print(*args: Any, **keys: Any) -> None: 

6006 """ 

6007 Print all non-keyword args, and put them to the log pane. 

6008 

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 

6095 

6096def pr(*args: Any, **keys: Any) -> None: 

6097 """ 

6098 Print all non-keyword args. This is a wrapper for the print statement. 

6099 

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 

6252 

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 

6294 

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. 

6328 

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

6392 

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. 

6398 

6399 The function's first argument should be self. 

6400 

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. 

6410 

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. 

6447 

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. 

6469 

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

6479 

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. 

6598 

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: 

6633 

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: 

6683 

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

6709 

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 

6720 

6721 Arguments: 

6722 g: Leo-Editor globals 

6723 ree: Read file descriptor for stderr 

6724 fname: file pathname 

6725 

6726 Returns: 

6727 None 

6728 """ 

6729 

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 

6739 

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

6746 

6747 Returns: 

6748 None 

6749 """ 

6750 

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

6841 

6842 def name(func: Any) -> str: 

6843 return func.__name__ if hasattr(func, '__name__') else '<no __name__>' 

6844 

6845 def get_defaults(func: str, i: int) -> Any: 

6846 defaults = inspect.getfullargspec(func)[3] 

6847 return defaults[i] 

6848 

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. 

6852 

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) 

6871 

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. 

6937 

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. 

7007 

7008 def subprocess_wrapper(cmdlst: str) -> Tuple: 

7009 

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 

7015 

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. 

7153 

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. 

7245 

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:.*$') 

7268 

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. 

7311 

7312 Negative line numbers indicate global lines. 

7313 

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. 

7326 

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 

7365 

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. 

7444 

7445 Return the commander for the found UNL, or None. 

7446 

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: 

7581 

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] 

7653 

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 

7663 

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

7713 

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

7759 

7760#@@language python 

7761#@@tabwidth -4 

7762#@@pagewidth 70 

7763#@-leo