Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2#@+leo-ver=5-thin 

3#@+node:ekr.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 

26# import pdb # Do NOT import pdb here! 

27 # We shall define pdb as a _function_ below. 

28import re 

29import shlex 

30import string 

31import sys 

32import subprocess 

33import tempfile 

34import textwrap 

35import time 

36import traceback 

37import types 

38from typing import TYPE_CHECKING 

39from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Sequence, Set, Tuple, Union 

40import unittest 

41import urllib 

42import urllib.parse as urlparse 

43import webbrowser 

44try: 

45 import tkinter as Tk 

46except Exception: 

47 Tk = None 

48# 

49# Leo never imports any other Leo module. 

50if TYPE_CHECKING: # Always False at runtime. 

51 from leo.core.leoCommands import Commands as Cmdr 

52 from leo.core.leoNodes import Position as Pos 

53 from leo.core.leoNodes import VNode 

54else: 

55 Cmdr = Pos = VNode = Any 

56# 

57# Abbreviations... 

58StringIO = io.StringIO 

59#@-<< imports >> 

60in_bridge = False # True: leoApp object loads a null Gui. 

61in_vs_code = False # #2098. 

62minimum_python_version = '3.6' # #1215. 

63isPython3 = sys.version_info >= (3, 0, 0) 

64isMac = sys.platform.startswith('darwin') 

65isWindows = sys.platform.startswith('win') 

66#@+<< define g.globalDirectiveList >> 

67#@+node:EKR.20040610094819: ** << define g.globalDirectiveList >> 

68# Visible externally so plugins may add to the list of directives. 

69# The atFile write logic uses this, but not the atFile read logic. 

70globalDirectiveList = [ 

71 # Order does not matter. 

72 'all', 

73 'beautify', 

74 'colorcache', 'code', 'color', 'comment', 'c', 

75 'delims', 'doc', 

76 'encoding', 

77 # 'end_raw', # #2276. 

78 'first', 'header', 'ignore', 

79 'killbeautify', 'killcolor', 

80 'language', 'last', 'lineending', 

81 'markup', 

82 'nobeautify', 

83 'nocolor-node', 'nocolor', 'noheader', 'nowrap', 

84 'nopyflakes', # Leo 6.1. 

85 'nosearch', # Leo 5.3. 

86 'others', 'pagewidth', 'path', 'quiet', 

87 # 'raw', # #2276. 

88 'section-delims', # Leo 6.6. #2276. 

89 'silent', 

90 'tabwidth', 'terse', 

91 'unit', 'verbose', 'wrap', 

92] 

93 

94directives_pat = None # Set below. 

95#@-<< define g.globalDirectiveList >> 

96#@+<< define global decorator dicts >> 

97#@+node:ekr.20150510103918.1: ** << define global decorator dicts >> (leoGlobals.py) 

98#@@nobeautify 

99#@@language rest 

100#@+at 

101# The cmd_instance_dict supports per-class @cmd decorators. For example, the 

102# following appears in leo.commands. 

103# 

104# def cmd(name: Any) -> Any: 

105# """Command decorator for the abbrevCommands class.""" 

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

107# 

108# For commands based on functions, use the @g.command decorator. 

109#@@c 

110#@@language python 

111 

112global_commands_dict = {} 

113 

114cmd_instance_dict = { 

115 # Keys are class names, values are attribute chains. 

116 'AbbrevCommandsClass': ['c', 'abbrevCommands'], 

117 'AtFile': ['c', 'atFileCommands'], 

118 'AutoCompleterClass': ['c', 'k', 'autoCompleter'], 

119 'ChapterController': ['c', 'chapterController'], 

120 'Commands': ['c'], 

121 'ControlCommandsClass': ['c', 'controlCommands'], 

122 'DebugCommandsClass': ['c', 'debugCommands'], 

123 'EditCommandsClass': ['c', 'editCommands'], 

124 'EditFileCommandsClass': ['c', 'editFileCommands'], 

125 'FileCommands': ['c', 'fileCommands'], 

126 'HelpCommandsClass': ['c', 'helpCommands'], 

127 'KeyHandlerClass': ['c', 'k'], 

128 'KeyHandlerCommandsClass': ['c', 'keyHandlerCommands'], 

129 'KillBufferCommandsClass': ['c', 'killBufferCommands'], 

130 'LeoApp': ['g', 'app'], 

131 'LeoFind': ['c', 'findCommands'], 

132 'LeoImportCommands': ['c', 'importCommands'], 

133 # 'MacroCommandsClass': ['c', 'macroCommands'], 

134 'PrintingController': ['c', 'printingController'], 

135 'RectangleCommandsClass': ['c', 'rectangleCommands'], 

136 'RstCommands': ['c', 'rstCommands'], 

137 'SpellCommandsClass': ['c', 'spellCommands'], 

138 'Undoer': ['c', 'undoer'], 

139 'VimCommands': ['c', 'vimCommands'], 

140} 

141#@-<< define global decorator dicts >> 

142#@+<< define g.decorators >> 

143#@+node:ekr.20150508165324.1: ** << define g.Decorators >> 

144#@+others 

145#@+node:ekr.20150510104148.1: *3* g.check_cmd_instance_dict 

146def check_cmd_instance_dict(c: Cmdr, g: Any) -> None: 

147 """ 

148 Check g.check_cmd_instance_dict. 

149 This is a permanent unit test, called from c.finishCreate. 

150 """ 

151 d = cmd_instance_dict 

152 for key in d: 

153 ivars = d.get(key) 

154 # Produces warnings. 

155 obj = ivars2instance(c, g, ivars) # type:ignore 

156 if obj: 

157 name = obj.__class__.__name__ 

158 if name != key: 

159 g.trace('class mismatch', key, name) 

160#@+node:ville.20090521164644.5924: *3* g.command (decorator) 

161class Command: 

162 """ 

163 A global decorator for creating commands. 

164 

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

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

167 

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

169 def A_Command(event): 

170 c = event.get('c') 

171 ... 

172 

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

174 """ 

175 

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

177 """Ctor for command decorator class.""" 

178 self.name = name 

179 

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

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

182 global_commands_dict[self.name] = func 

183 if app: 

184 for c in app.commanders(): 

185 c.k.registerCommand(self.name, func) 

186 # Inject ivars for plugins_menu.py. 

187 func.__func_name__ = func.__name__ # For leoInteg. 

188 func.is_command = True 

189 func.command_name = self.name 

190 return func 

191 

192command = Command 

193#@+node:ekr.20171124070654.1: *3* g.command_alias 

194def command_alias(alias: str, func: Callable) -> None: 

195 """Create an alias for the *already defined* method in the Commands class.""" 

196 from leo.core import leoCommands 

197 assert hasattr(leoCommands.Commands, func.__name__) 

198 funcToMethod(func, leoCommands.Commands, alias) 

199#@+node:ekr.20171123095526.1: *3* g.commander_command (decorator) 

200class CommanderCommand: 

201 """ 

202 A global decorator for creating commander commands, that is, commands 

203 that were formerly methods of the Commands class in leoCommands.py. 

204 

205 Usage: 

206 

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

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

209 ... 

210 

211 The decorator injects command_name into the Commander class and calls 

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

213 

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

215 """ 

216 

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

218 """Ctor for command decorator class.""" 

219 self.name = name 

220 

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

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

223 

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

225 c = event.get('c') 

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

227 method(event=event) 

228 

229 # Inject ivars for plugins_menu.py. 

230 commander_command_wrapper.__func_name__ = func.__name__ # For leoInteg. 

231 commander_command_wrapper.__name__ = self.name 

232 commander_command_wrapper.__doc__ = func.__doc__ 

233 global_commands_dict[self.name] = commander_command_wrapper 

234 if app: 

235 from leo.core import leoCommands 

236 funcToMethod(func, leoCommands.Commands) 

237 for c in app.commanders(): 

238 c.k.registerCommand(self.name, func) 

239 # Inject ivars for plugins_menu.py. 

240 func.is_command = True 

241 func.command_name = self.name 

242 return func 

243 

244commander_command = CommanderCommand 

245#@+node:ekr.20150508164812.1: *3* g.ivars2instance 

246def ivars2instance(c: Cmdr, g: Any, ivars: List[str]) -> Any: 

247 """ 

248 Return the instance of c given by ivars. 

249 ivars is a list of strings. 

250 A special case: ivars may be 'g', indicating the leoGlobals module. 

251 """ 

252 if not ivars: 

253 g.trace('can not happen: no ivars') 

254 return None 

255 ivar = ivars[0] 

256 if ivar not in ('c', 'g'): 

257 g.trace('can not happen: unknown base', ivar) 

258 return None 

259 obj = c if ivar == 'c' else g 

260 for ivar in ivars[1:]: 

261 obj = getattr(obj, ivar, None) 

262 if not obj: 

263 g.trace('can not happen: unknown attribute', obj, ivar, ivars) 

264 break 

265 return obj 

266#@+node:ekr.20150508134046.1: *3* g.new_cmd_decorator (decorator) 

267def new_cmd_decorator(name: str, ivars: List[str]) -> Callable: 

268 """ 

269 Return a new decorator for a command with the given name. 

270 Compute the class *instance* using the ivar string or list. 

271 

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

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

274 """ 

275 

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

277 

278 def new_cmd_wrapper(event: Any) -> None: 

279 if isinstance(event, dict): 

280 c = event.get('c') 

281 else: 

282 c = event.c 

283 self = g.ivars2instance(c, g, ivars) 

284 try: 

285 func(self, event=event) 

286 # Don't use a keyword for self. 

287 # This allows the VimCommands class to use vc instead. 

288 except Exception: 

289 g.es_exception() 

290 

291 new_cmd_wrapper.__func_name__ = func.__name__ # For leoInteg. 

292 new_cmd_wrapper.__name__ = name 

293 new_cmd_wrapper.__doc__ = func.__doc__ 

294 global_commands_dict[name] = new_cmd_wrapper 

295 # Put the *wrapper* into the global dict. 

296 return func 

297 # The decorator must return the func itself. 

298 

299 return _decorator 

300#@-others 

301#@-<< define g.decorators >> 

302#@+<< define regex's >> 

303#@+node:ekr.20200810093517.1: ** << define regex's >> 

304# Regex used by this module, and in leoColorizer.py. 

305g_language_pat = re.compile(r'^@language\s+(\w+)+', re.MULTILINE) 

306# 

307# Patterns used only in this module... 

308 

309# g_is_directive_pattern excludes @encoding.whatever and @encoding(whatever) 

310# It must allow @language python, @nocolor-node, etc. 

311g_is_directive_pattern = re.compile(r'^\s*@([\w-]+)\s*') 

312g_noweb_root = re.compile('<' + '<' + '*' + '>' + '>' + '=', re.MULTILINE) 

313g_tabwidth_pat = re.compile(r'(^@tabwidth)', re.MULTILINE) 

314# #2267: Support for @section-delims. 

315g_section_delims_pat = re.compile(r'^@section-delims[ \t]+([^ \w\n\t]+)[ \t]+([^ \w\n\t]+)[ \t]*$') 

316#@-<< define regex's >> 

317tree_popup_handlers: List[Callable] = [] # Set later. 

318user_dict: Dict[Any, Any] = {} # Non-persistent dictionary for scripts and plugins. 

319app: Any = None # The singleton app object. Set by runLeo.py. 

320# Global status vars. 

321inScript = False # A synonym for app.inScript 

322unitTesting = False # A synonym for app.unitTesting. 

323#@+others 

324#@+node:ekr.20201211182722.1: ** g.Backup 

325#@+node:ekr.20201211182659.1: *3* g.standard_timestamp 

326def standard_timestamp() -> str: 

327 """Return a reasonable timestamp.""" 

328 return time.strftime("%Y%m%d-%H%M%S") 

329#@+node:ekr.20201211183100.1: *3* g.get_backup_directory 

330def get_backup_path(sub_directory: str) -> Optional[str]: 

331 """ 

332 Return the full path to the subdirectory of the main backup directory. 

333 

334 The main backup directory is computed as follows: 

335 

336 1. os.environ['LEO_BACKUP'] 

337 2. ~/Backup 

338 """ 

339 # Compute the main backup directory. 

340 # First, try the LEO_BACKUP directory. 

341 backup = None 

342 try: 

343 backup = os.environ['LEO_BACKUP'] 

344 if not os.path.exists(backup): 

345 backup = None 

346 except KeyError: 

347 pass 

348 except Exception: 

349 g.es_exception() 

350 # Second, try ~/Backup. 

351 if not backup: 

352 backup = os.path.join(str(Path.home()), 'Backup') 

353 if not os.path.exists(backup): 

354 backup = None 

355 if not backup: 

356 return None 

357 # Compute the path to backup/sub_directory 

358 directory = os.path.join(backup, sub_directory) 

359 return directory if os.path.exists(directory) else None 

360#@+node:ekr.20140711071454.17644: ** g.Classes & class accessors 

361#@+node:ekr.20120123115816.10209: *3* class g.BindingInfo & isBindingInfo 

362class BindingInfo: 

363 """ 

364 A class representing any kind of key binding line. 

365 

366 This includes other information besides just the KeyStroke. 

367 """ 

368 # Important: The startup code uses this class, 

369 # so it is convenient to define it in leoGlobals.py. 

370 #@+others 

371 #@+node:ekr.20120129040823.10254: *4* bi.__init__ 

372 def __init__( 

373 self, 

374 kind: str, 

375 commandName: str='', 

376 func: Any=None, 

377 nextMode: Any=None, 

378 pane: Any=None, 

379 stroke: Any=None, 

380 ) -> None: 

381 if not g.isStrokeOrNone(stroke): 

382 g.trace('***** (BindingInfo) oops', repr(stroke)) 

383 self.kind = kind 

384 self.commandName = commandName 

385 self.func = func 

386 self.nextMode = nextMode 

387 self.pane = pane 

388 self.stroke = stroke # The *caller* must canonicalize the shortcut. 

389 #@+node:ekr.20120203153754.10031: *4* bi.__hash__ 

390 def __hash__(self) -> Any: 

391 return self.stroke.__hash__() if self.stroke else 0 

392 #@+node:ekr.20120125045244.10188: *4* bi.__repr__ & ___str_& dump 

393 def __repr__(self) -> str: 

394 return self.dump() 

395 

396 __str__ = __repr__ 

397 

398 def dump(self) -> str: 

399 result = [f"BindingInfo {self.kind:17}"] 

400 # Print all existing ivars. 

401 table = ('commandName', 'func', 'nextMode', 'pane', 'stroke') 

402 for ivar in table: 

403 if hasattr(self, ivar): 

404 val = getattr(self, ivar) 

405 if val not in (None, 'none', 'None', ''): 

406 if ivar == 'func': 

407 # pylint: disable=no-member 

408 val = val.__name__ 

409 s = f"{ivar}: {val!r}" 

410 result.append(s) 

411 # Clearer w/o f-string. 

412 return "[%s]" % ' '.join(result).strip() 

413 #@+node:ekr.20120129040823.10226: *4* bi.isModeBinding 

414 def isModeBinding(self) -> bool: 

415 return self.kind.startswith('*mode') 

416 #@-others 

417def isBindingInfo(obj: Any) -> bool: 

418 return isinstance(obj, BindingInfo) 

419#@+node:ekr.20031218072017.3098: *3* class g.Bunch (Python Cookbook) 

420class Bunch: 

421 """ 

422 From The Python Cookbook: 

423 

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

425 

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

427 

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

429 del some of them, etc:: 

430 

431 if point.squared > threshold: 

432 point.isok = True 

433 """ 

434 

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

436 self.__dict__.update(keywords) 

437 

438 def __repr__(self) -> str: 

439 return self.toString() 

440 

441 def ivars(self) -> List: 

442 return sorted(self.__dict__) 

443 

444 def keys(self) -> List: 

445 return sorted(self.__dict__) 

446 

447 def toString(self) -> str: 

448 tag = self.__dict__.get('tag') 

449 entries = [ 

450 f"{key}: {str(self.__dict__.get(key)) or repr(self.__dict__.get(key))}" 

451 for key in self.ivars() if key != 'tag' 

452 ] 

453 # Fail. 

454 result = [f'g.Bunch({tag or ""})'] 

455 result.extend(entries) 

456 return '\n '.join(result) + '\n' 

457 

458 # Used by new undo code. 

459 

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

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

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

463 

464 def __getitem__(self, key: str) -> Any: 

465 """Support aBunch[key]""" 

466 # g.pr('g.Bunch.__getitem__', key) 

467 return operator.getitem(self.__dict__, key) 

468 

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

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

471 

472 def __contains__(self, key: str) -> bool: # New. 

473 # g.pr('g.Bunch.__contains__', key in self.__dict__, key) 

474 return key in self.__dict__ 

475 

476bunch = Bunch 

477#@+node:ekr.20120219154958.10492: *3* class g.EmergencyDialog 

478class EmergencyDialog: 

479 """ 

480 A class that creates an tkinter dialog with a single OK button. 

481  

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

483 passed to the ctor. 

484  

485 """ 

486 #@+others 

487 #@+node:ekr.20120219154958.10493: *4* emergencyDialog.__init__ 

488 def __init__(self, title: str, message: str) -> None: 

489 """Constructor for the leoTkinterDialog class.""" 

490 self.answer = None # Value returned from run() 

491 self.title = title 

492 self.message = message 

493 self.buttonsFrame = None # Frame to hold typical dialog buttons. 

494 # Command to call when user click's the window's close box. 

495 self.defaultButtonCommand = None 

496 self.frame = None # The outermost frame. 

497 self.root = None # Created in createTopFrame. 

498 self.top = None # The toplevel Tk widget. 

499 if Tk: # #2512. 

500 self.createTopFrame() 

501 buttons = [{ 

502 "text": "OK", 

503 "command": self.okButton, 

504 "default": True, 

505 }] 

506 self.createButtons(buttons) 

507 self.top.bind("<Key>", self.onKey) 

508 else: 

509 print(message.rstrip() + '\n') 

510 #@+node:ekr.20120219154958.10494: *4* emergencyDialog.createButtons 

511 def createButtons(self, buttons: List[Dict[str, Any]]) -> List[Any]: 

512 """Create a row of buttons. 

513 

514 buttons is a list of dictionaries containing 

515 the properties of each button. 

516 """ 

517 assert self.frame 

518 self.buttonsFrame = f = Tk.Frame(self.top) 

519 f.pack(side="top", padx=30) 

520 # Buttons is a list of dictionaries, with an empty dictionary 

521 # at the end if there is only one entry. 

522 buttonList = [] 

523 for d in buttons: 

524 text = d.get("text", "<missing button name>") 

525 isDefault = d.get("default", False) 

526 underline = d.get("underline", 0) 

527 command = d.get("command", None) 

528 bd = 4 if isDefault else 2 

529 b = Tk.Button(f, width=6, text=text, bd=bd, 

530 underline=underline, command=command) 

531 b.pack(side="left", padx=5, pady=10) 

532 buttonList.append(b) 

533 if isDefault and command: 

534 self.defaultButtonCommand = command 

535 return buttonList 

536 #@+node:ekr.20120219154958.10495: *4* emergencyDialog.createTopFrame 

537 def createTopFrame(self) -> None: 

538 """Create the Tk.Toplevel widget for a leoTkinterDialog.""" 

539 self.root = Tk.Tk() # type:ignore 

540 self.top = Tk.Toplevel(self.root) # type:ignore 

541 self.top.title(self.title) 

542 self.root.withdraw() # This root window should *never* be shown. 

543 self.frame = Tk.Frame(self.top) # type:ignore 

544 self.frame.pack(side="top", expand=1, fill="both") 

545 label = Tk.Label(self.frame, text=self.message, bg='white') 

546 label.pack(pady=10) 

547 #@+node:ekr.20120219154958.10496: *4* emergencyDialog.okButton 

548 def okButton(self) -> None: 

549 """Do default click action in ok button.""" 

550 self.top.destroy() 

551 self.top = None 

552 #@+node:ekr.20120219154958.10497: *4* emergencyDialog.onKey 

553 def onKey(self, event: Any) -> None: 

554 """Handle Key events in askOk dialogs.""" 

555 self.okButton() 

556 #@+node:ekr.20120219154958.10498: *4* emergencyDialog.run 

557 def run(self) -> None: 

558 """Run the modal emergency dialog.""" 

559 # Suppress f-stringify. 

560 self.top.geometry("%dx%d%+d%+d" % (300, 200, 50, 50)) 

561 self.top.lift() 

562 self.top.grab_set() # Make the dialog a modal dialog. 

563 self.root.wait_window(self.top) 

564 #@-others 

565#@+node:ekr.20120123143207.10223: *3* class g.GeneralSetting 

566# Important: The startup code uses this class, 

567# so it is convenient to define it in leoGlobals.py. 

568 

569 

570class GeneralSetting: 

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

572 

573 def __init__( 

574 self, 

575 kind: str, 

576 encoding: str=None, 

577 ivar: str=None, 

578 setting: str=None, 

579 val: Any=None, 

580 path: str=None, 

581 tag: str='setting', 

582 unl: str=None, 

583 ) -> None: 

584 self.encoding = encoding 

585 self.ivar = ivar 

586 self.kind = kind 

587 self.path = path 

588 self.unl = unl 

589 self.setting = setting 

590 self.val = val 

591 self.tag = tag 

592 

593 def __repr__(self) -> str: 

594 # Better for g.printObj. 

595 val = str(self.val).replace('\n', ' ') 

596 return ( 

597 f"GS: {g.shortFileName(self.path):20} " 

598 f"{self.kind:7} = {g.truncate(val, 50)}") 

599 

600 dump = __repr__ 

601 __str__ = __repr__ 

602#@+node:ekr.20120201164453.10090: *3* class g.KeyStroke & isStroke/OrNone 

603class KeyStroke: 

604 """ 

605 A class that represent any key stroke or binding. 

606 

607 stroke.s is the "canonicalized" stroke. 

608 """ 

609 #@+others 

610 #@+node:ekr.20180414195401.2: *4* ks.__init__ 

611 def __init__(self, binding: str) -> None: 

612 

613 if binding: 

614 self.s = self.finalize_binding(binding) 

615 else: 

616 self.s = None # type:ignore 

617 #@+node:ekr.20120203053243.10117: *4* ks.__eq__, etc 

618 #@+at All these must be defined in order to say, for example: 

619 # for key in sorted(d) 

620 # where the keys of d are KeyStroke objects. 

621 #@@c 

622 

623 def __eq__(self, other: Any) -> bool: 

624 if not other: 

625 return False 

626 if hasattr(other, 's'): 

627 return self.s == other.s 

628 return self.s == other 

629 

630 def __lt__(self, other: Any) -> bool: 

631 if not other: 

632 return False 

633 if hasattr(other, 's'): 

634 return self.s < other.s 

635 return self.s < other 

636 

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

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

639 

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

641 return not self.__eq__(other) 

642 

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

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

645 

646 def __ge__(self, other: Any) -> bool: 

647 return not self.__lt__(other) 

648 #@+node:ekr.20120203053243.10118: *4* ks.__hash__ 

649 # Allow KeyStroke objects to be keys in dictionaries. 

650 

651 def __hash__(self) -> Any: 

652 return self.s.__hash__() if self.s else 0 

653 #@+node:ekr.20120204061120.10067: *4* ks.__repr___ & __str__ 

654 def __repr__(self) -> str: 

655 return f"<KeyStroke: {repr(self.s)}>" 

656 

657 def __str__(self) -> str: 

658 return repr(self.s) 

659 #@+node:ekr.20180417160703.1: *4* ks.dump 

660 def dump(self) -> None: 

661 """Show results of printable chars.""" 

662 for i in range(128): 

663 s = chr(i) 

664 stroke = g.KeyStroke(s) 

665 if stroke.s != s: 

666 print(f"{i:2} {s!r:10} {stroke.s!r}") 

667 for ch in ('backspace', 'linefeed', 'return', 'tab'): 

668 stroke = g.KeyStroke(ch) 

669 print(f'{"":2} {ch!r:10} {stroke.s!r}') 

670 #@+node:ekr.20180415082249.1: *4* ks.finalize_binding 

671 def finalize_binding(self, binding: str) -> str: 

672 

673 # This trace is good for devs only. 

674 trace = False and 'keys' in g.app.debug 

675 self.mods = self.find_mods(binding) 

676 s = self.strip_mods(binding) 

677 s = self.finalize_char(s) # May change self.mods. 

678 mods = ''.join([f"{z.capitalize()}+" for z in self.mods]) 

679 if trace and 'meta' in self.mods: 

680 g.trace(f"{binding:20}:{self.mods:>20} ==> {mods+s}") 

681 return mods + s 

682 #@+node:ekr.20180415083926.1: *4* ks.finalize_char & helper 

683 def finalize_char(self, s: str) -> str: 

684 """Perform very-last-minute translations on bindings.""" 

685 # 

686 # Retain "bigger" spelling for gang-of-four bindings with modifiers. 

687 shift_d = { 

688 'bksp': 'BackSpace', 

689 'backspace': 'BackSpace', 

690 'backtab': 'Tab', # The shift mod will convert to 'Shift+Tab', 

691 'linefeed': 'Return', 

692 '\r': 'Return', 

693 'return': 'Return', 

694 'tab': 'Tab', 

695 } 

696 if self.mods and s.lower() in shift_d: 

697 # Returning '' breaks existing code. 

698 return shift_d.get(s.lower()) # type:ignore 

699 # 

700 # Make all other translations... 

701 # 

702 # This dict ensures proper capitalization. 

703 # It also translates legacy Tk binding names to ascii chars. 

704 translate_d = { 

705 # 

706 # The gang of four... 

707 'bksp': 'BackSpace', 

708 'backspace': 'BackSpace', 

709 'backtab': 'Tab', # The shift mod will convert to 'Shift+Tab', 

710 'linefeed': '\n', 

711 '\r': '\n', 

712 'return': '\n', 

713 'tab': 'Tab', 

714 # 

715 # Special chars... 

716 'delete': 'Delete', 

717 'down': 'Down', 

718 'end': 'End', 

719 'enter': 'Enter', 

720 'escape': 'Escape', 

721 'home': 'Home', 

722 'insert': 'Insert', 

723 'left': 'Left', 

724 'next': 'Next', 

725 'prior': 'Prior', 

726 'right': 'Right', 

727 'up': 'Up', 

728 # 

729 # Qt key names... 

730 'del': 'Delete', 

731 'dnarrow': 'Down', 

732 'esc': 'Escape', 

733 'ins': 'Insert', 

734 'ltarrow': 'Left', 

735 'pagedn': 'Next', 

736 'pageup': 'Prior', 

737 'pgdown': 'Next', 

738 'pgup': 'Prior', 

739 'rtarrow': 'Right', 

740 'uparrow': 'Up', 

741 # 

742 # Legacy Tk binding names... 

743 "ampersand": "&", 

744 "asciicircum": "^", 

745 "asciitilde": "~", 

746 "asterisk": "*", 

747 "at": "@", 

748 "backslash": "\\", 

749 "bar": "|", 

750 "braceleft": "{", 

751 "braceright": "}", 

752 "bracketleft": "[", 

753 "bracketright": "]", 

754 "colon": ":", 

755 "comma": ",", 

756 "dollar": "$", 

757 "equal": "=", 

758 "exclam": "!", 

759 "greater": ">", 

760 "less": "<", 

761 "minus": "-", 

762 "numbersign": "#", 

763 "quotedbl": '"', 

764 "quoteright": "'", 

765 "parenleft": "(", 

766 "parenright": ")", 

767 "percent": "%", 

768 "period": ".", 

769 "plus": "+", 

770 "question": "?", 

771 "quoteleft": "`", 

772 "semicolon": ";", 

773 "slash": "/", 

774 "space": " ", 

775 "underscore": "_", 

776 } 

777 # 

778 # pylint: disable=undefined-loop-variable 

779 # Looks like a pylint bug. 

780 if s in (None, 'none', 'None'): 

781 return 'None' 

782 if s.lower() in translate_d: 

783 s = translate_d.get(s.lower()) 

784 return self.strip_shift(s) # type:ignore 

785 if len(s) > 1 and s.find(' ') > -1: 

786 # #917: not a pure, but should be ignored. 

787 return '' 

788 if s.isalpha(): 

789 if len(s) == 1: 

790 if 'shift' in self.mods: 

791 if len(self.mods) == 1: 

792 self.mods.remove('shift') 

793 s = s.upper() 

794 else: 

795 s = s.lower() 

796 elif self.mods: 

797 s = s.lower() 

798 else: 

799 # 917: Ignore multi-byte alphas not in the table. 

800 s = '' 

801 if 0: 

802 # Make sure all special chars are in translate_d. 

803 if g.app.gui: # It may not exist yet. 

804 if s.capitalize() in g.app.gui.specialChars: 

805 s = s.capitalize() 

806 return s 

807 # 

808 # Translate shifted keys to their appropriate alternatives. 

809 return self.strip_shift(s) 

810 #@+node:ekr.20180502104829.1: *5* ks.strip_shift 

811 def strip_shift(self, s: str) -> str: 

812 """ 

813 Handle supposedly shifted keys. 

814 

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

816 

817 The legacy Tk binding names have already been translated, 

818 so we don't have to worry about Shift-ampersand, etc. 

819 """ 

820 # 

821 # The second entry in each line handles shifting an already-shifted character. 

822 # That's ok in user settings: the Shift modifier is just removed. 

823 shift_d = { 

824 # Top row of keyboard. 

825 "`": "~", "~": "~", 

826 "1": "!", "!": "!", 

827 "2": "@", "@": "@", 

828 "3": "#", "#": "#", 

829 "4": "$", "$": "$", 

830 "5": "%", "%": "%", 

831 "6": "^", "^": "^", 

832 "7": "&", "&": "&", 

833 "8": "*", "*": "*", 

834 "9": "(", "(": "(", 

835 "0": ")", ")": ")", 

836 "-": "_", "_": "_", 

837 "=": "+", "+": "+", 

838 # Second row of keyboard. 

839 "[": "{", "{": "{", 

840 "]": "}", "}": "}", 

841 "\\": '|', "|": "|", 

842 # Third row of keyboard. 

843 ";": ":", ":": ":", 

844 "'": '"', '"': '"', 

845 # Fourth row of keyboard. 

846 ".": "<", "<": "<", 

847 ",": ">", ">": ">", 

848 "//": "?", "?": "?", 

849 } 

850 if 'shift' in self.mods and s in shift_d: 

851 self.mods.remove('shift') 

852 s = shift_d.get(s) # type:ignore 

853 return s 

854 #@+node:ekr.20120203053243.10124: *4* ks.find, lower & startswith 

855 # These may go away later, but for now they make conversion of string strokes easier. 

856 

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

858 return self.s.find(pattern) 

859 

860 def lower(self) -> str: 

861 return self.s.lower() 

862 

863 def startswith(self, s: str) -> bool: 

864 return self.s.startswith(s) 

865 #@+node:ekr.20180415081209.2: *4* ks.find_mods 

866 def find_mods(self, s: str) -> List[str]: 

867 """Return the list of all modifiers seen in s.""" 

868 s = s.lower() 

869 table = ( 

870 ['alt',], 

871 ['command', 'cmd',], 

872 ['ctrl', 'control',], # Use ctrl, not control. 

873 ['meta',], 

874 ['shift', 'shft',], 

875 ['keypad', 'key_pad', 'numpad', 'num_pad'], 

876 # 868: Allow alternative spellings. 

877 ) 

878 result = [] 

879 for aList in table: 

880 kind = aList[0] 

881 for mod in aList: 

882 for suffix in '+-': 

883 if s.find(mod + suffix) > -1: 

884 s = s.replace(mod + suffix, '') 

885 result.append(kind) 

886 break 

887 return result 

888 #@+node:ekr.20180417101435.1: *4* ks.isAltCtl 

889 def isAltCtrl(self) -> bool: 

890 """Return True if this is an Alt-Ctrl character.""" 

891 mods = self.find_mods(self.s) 

892 return 'alt' in mods and 'ctrl' in mods 

893 #@+node:ekr.20120203053243.10121: *4* ks.isFKey 

894 def isFKey(self) -> bool: 

895 return self.s in g.app.gui.FKeys 

896 #@+node:ekr.20180417102341.1: *4* ks.isPlainKey (does not handle alt-ctrl chars) 

897 def isPlainKey(self) -> bool: 

898 """ 

899 Return True if self.s represents a plain key. 

900 

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

902 

903 **Note**: The caller is responsible for handling Alt-Ctrl keys. 

904 """ 

905 s = self.s 

906 if s in g.app.gui.ignoreChars: 

907 # For unit tests. 

908 return False 

909 # #868: 

910 if s.find('Keypad+') > -1: 

911 # Enable bindings. 

912 return False 

913 if self.find_mods(s) or self.isFKey(): 

914 return False 

915 if s in g.app.gui.specialChars: 

916 return False 

917 if s == 'BackSpace': 

918 return False 

919 return True 

920 #@+node:ekr.20180511092713.1: *4* ks.isNumPadKey, ks.isPlainNumPad & ks.removeNumPadModifier 

921 def isNumPadKey(self) -> bool: 

922 return self.s.find('Keypad+') > -1 

923 

924 def isPlainNumPad(self) -> bool: 

925 return ( 

926 self.isNumPadKey() and 

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

928 ) 

929 

930 def removeNumPadModifier(self) -> None: 

931 self.s = self.s.replace('Keypad+', '') 

932 #@+node:ekr.20180419170934.1: *4* ks.prettyPrint 

933 def prettyPrint(self) -> str: 

934 

935 s = self.s 

936 if not s: 

937 return '<None>' 

938 d = {' ': 'Space', '\t': 'Tab', '\n': 'Return', '\r': 'LineFeed'} 

939 ch = s[-1] 

940 return s[:-1] + d.get(ch, ch) 

941 #@+node:ekr.20180415124853.1: *4* ks.strip_mods 

942 def strip_mods(self, s: str) -> str: 

943 """Remove all modifiers from s, without changing the case of s.""" 

944 table = ( 

945 'alt', 

946 'cmd', 'command', 

947 'control', 'ctrl', 

948 'keypad', 'key_pad', # 868: 

949 'meta', 

950 'shift', 'shft', 

951 ) 

952 for mod in table: 

953 for suffix in '+-': 

954 target = mod + suffix 

955 i = s.lower().find(target) 

956 if i > -1: 

957 s = s[:i] + s[i + len(target) :] 

958 break 

959 return s 

960 #@+node:ekr.20120203053243.10125: *4* ks.toGuiChar 

961 def toGuiChar(self) -> str: 

962 """Replace special chars by the actual gui char.""" 

963 s = self.s.lower() 

964 if s in ('\n', 'return'): 

965 s = '\n' 

966 elif s in ('\t', 'tab'): 

967 s = '\t' 

968 elif s in ('\b', 'backspace'): 

969 s = '\b' 

970 elif s in ('.', 'period'): 

971 s = '.' 

972 return s 

973 #@+node:ekr.20180417100834.1: *4* ks.toInsertableChar 

974 def toInsertableChar(self) -> str: 

975 """Convert self to an (insertable) char.""" 

976 # pylint: disable=len-as-condition 

977 s = self.s 

978 if not s or self.find_mods(s): 

979 return '' 

980 # Handle the "Gang of Four" 

981 d = { 

982 'BackSpace': '\b', 

983 'LineFeed': '\n', 

984 # 'Insert': '\n', 

985 'Return': '\n', 

986 'Tab': '\t', 

987 } 

988 if s in d: 

989 return d.get(s) # type:ignore 

990 return s if len(s) == 1 else '' 

991 #@-others 

992 

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

994 return isinstance(obj, KeyStroke) 

995 

996def isStrokeOrNone(obj: Any) -> bool: 

997 return obj is None or isinstance(obj, KeyStroke) 

998#@+node:ekr.20160119093947.1: *3* class g.MatchBrackets 

999class MatchBrackets: 

1000 """ 

1001 A class implementing the match-brackets command. 

1002 

1003 In the interest of speed, the code assumes that the user invokes the 

1004 match-bracket command ouside of any string, comment or (for perl or 

1005 javascript) regex. 

1006 """ 

1007 #@+others 

1008 #@+node:ekr.20160119104510.1: *4* mb.ctor 

1009 def __init__(self, c: Cmdr, p: Pos, language: str) -> None: 

1010 """Ctor for MatchBrackets class.""" 

1011 self.c = c 

1012 self.p = p.copy() 

1013 self.language = language 

1014 # Constants. 

1015 self.close_brackets = ")]}>" 

1016 self.open_brackets = "([{<" 

1017 self.brackets = self.open_brackets + self.close_brackets 

1018 self.matching_brackets = self.close_brackets + self.open_brackets 

1019 # Language dependent. 

1020 d1, d2, d3 = g.set_delims_from_language(language) 

1021 self.single_comment, self.start_comment, self.end_comment = d1, d2, d3 

1022 # to track expanding selection 

1023 c.user_dict.setdefault('_match_brackets', {'count': 0, 'range': (0, 0)}) 

1024 #@+node:ekr.20160121164723.1: *4* mb.bi-directional helpers 

1025 #@+node:ekr.20160121112812.1: *5* mb.is_regex 

1026 def is_regex(self, s: str, i: int) -> bool: 

1027 """Return true if there is another slash on the line.""" 

1028 if self.language in ('javascript', 'perl',): 

1029 assert s[i] == '/' 

1030 offset = 1 if self.forward else -1 

1031 i += offset 

1032 while 0 <= i < len(s) and s[i] != '\n': 

1033 if s[i] == '/': 

1034 return True 

1035 i += offset 

1036 return False 

1037 return False 

1038 #@+node:ekr.20160121112536.1: *5* mb.scan_regex 

1039 def scan_regex(self, s: str, i: int) -> int: 

1040 """Scan a regex (or regex substitution for perl).""" 

1041 assert s[i] == '/' 

1042 offset = 1 if self.forward else -1 

1043 i1 = i 

1044 i += offset 

1045 found: Union[int, bool] = False 

1046 while 0 <= i < len(s) and s[i] != '\n': 

1047 ch = s[i] 

1048 i2 = i - 1 # in case we have to look behind. 

1049 i += offset 

1050 if ch == '/': 

1051 # Count the preceding backslashes. 

1052 n = 0 

1053 while 0 <= i2 < len(s) and s[i2] == '\\': 

1054 n += 1 

1055 i2 -= 1 

1056 if (n % 2) == 0: 

1057 if self.language == 'perl' and found is None: 

1058 found = i 

1059 else: 

1060 found = i 

1061 break 

1062 if found is None: 

1063 self.oops('unmatched regex delim') 

1064 return i1 + offset 

1065 return found 

1066 #@+node:ekr.20160121112303.1: *5* mb.scan_string 

1067 def scan_string(self, s: str, i: int) -> int: 

1068 """ 

1069 Scan the string starting at s[i] (forward or backward). 

1070 Return the index of the next character. 

1071 """ 

1072 # i1 = i if self.forward else i + 1 

1073 delim = s[i] 

1074 assert delim in "'\"", repr(delim) 

1075 offset = 1 if self.forward else -1 

1076 i += offset 

1077 while 0 <= i < len(s): 

1078 ch = s[i] 

1079 i2 = i - 1 # in case we have to look behind. 

1080 i += offset 

1081 if ch == delim: 

1082 # Count the preceding backslashes. 

1083 n = 0 

1084 while 0 <= i2 < len(s) and s[i2] == '\\': 

1085 n += 1 

1086 i2 -= 1 

1087 if (n % 2) == 0: 

1088 return i 

1089 # Annoying when matching brackets on the fly. 

1090 # self.oops('unmatched string') 

1091 return i + offset 

1092 #@+node:tbrown.20180226113621.1: *4* mb.expand_range 

1093 def expand_range( 

1094 self, 

1095 s: str, 

1096 left: int, 

1097 right: int, 

1098 max_right: int, 

1099 expand: bool=False, 

1100 ) -> Tuple[Any, Any, Any, Any]: 

1101 """ 

1102 Find the bracket nearest the cursor searching outwards left and right. 

1103 

1104 Expand the range (left, right) in string s until either s[left] or 

1105 s[right] is a bracket. right can not exceed max_right, and if expand is 

1106 True, the new range must encompass the old range, in addition to s[left] 

1107 or s[right] being a bracket. 

1108 

1109 Returns 

1110 new_left, new_right, bracket_char, index_of_bracket_char 

1111 if expansion succeeds, otherwise 

1112 None, None, None, None 

1113 

1114 Note that only one of new_left and new_right will necessarily be a 

1115 bracket, but index_of_bracket_char will definitely be a bracket. 

1116 """ 

1117 expanded: Union[bool, str] = False 

1118 left = max(0, min(left, len(s))) # #2240 

1119 right = max(0, min(right, len(s))) # #2240 

1120 orig_left = left 

1121 orig_right = right 

1122 while ( 

1123 (s[left] not in self.brackets or expand and not expanded) 

1124 and (s[right] not in self.brackets or expand and not expanded) 

1125 and (left > 0 or right < max_right) 

1126 ): 

1127 expanded = False 

1128 if left > 0: 

1129 left -= 1 

1130 if s[left] in self.brackets: 

1131 other = self.find_matching_bracket(s[left], s, left) 

1132 if other is not None and other >= orig_right: 

1133 expanded = 'left' 

1134 if right < max_right: 

1135 right += 1 

1136 if s[right] in self.brackets: 

1137 other = self.find_matching_bracket(s[right], s, right) 

1138 if other is not None and other <= orig_left: 

1139 expanded = 'right' 

1140 if s[left] in self.brackets and (not expand or expanded == 'left'): 

1141 return left, right, s[left], left 

1142 if s[right] in self.brackets and (not expand or expanded == 'right'): 

1143 return left, right, s[right], right 

1144 return None, None, None, None 

1145 #@+node:ekr.20061113221414: *4* mb.find_matching_bracket 

1146 def find_matching_bracket(self, ch1: str, s: str, i: int) -> Any: 

1147 """Find the bracket matching s[i] for self.language.""" 

1148 self.forward = ch1 in self.open_brackets 

1149 # Find the character matching the initial bracket. 

1150 for n in range(len(self.brackets)): # pylint: disable=consider-using-enumerate 

1151 if ch1 == self.brackets[n]: 

1152 target = self.matching_brackets[n] 

1153 break 

1154 else: 

1155 return None 

1156 f = self.scan if self.forward else self.scan_back 

1157 return f(ch1, target, s, i) 

1158 #@+node:ekr.20160121164556.1: *4* mb.scan & helpers 

1159 def scan(self, ch1: str, target: str, s: str, i: int) -> Optional[int]: 

1160 """Scan forward for target.""" 

1161 level = 0 

1162 while 0 <= i < len(s): 

1163 progress = i 

1164 ch = s[i] 

1165 if ch in '"\'': 

1166 # Scan to the end/beginning of the string. 

1167 i = self.scan_string(s, i) 

1168 elif self.starts_comment(s, i): 

1169 i = self.scan_comment(s, i) # type:ignore 

1170 elif ch == '/' and self.is_regex(s, i): 

1171 i = self.scan_regex(s, i) 

1172 elif ch == ch1: 

1173 level += 1 

1174 i += 1 

1175 elif ch == target: 

1176 level -= 1 

1177 if level <= 0: 

1178 return i 

1179 i += 1 

1180 else: 

1181 i += 1 

1182 assert i > progress 

1183 # Not found 

1184 return None 

1185 #@+node:ekr.20160119090634.1: *5* mb.scan_comment 

1186 def scan_comment(self, s: str, i: int) -> Optional[int]: 

1187 """Return the index of the character after a comment.""" 

1188 i1 = i 

1189 start = self.start_comment if self.forward else self.end_comment 

1190 end = self.end_comment if self.forward else self.start_comment 

1191 offset = 1 if self.forward else -1 

1192 if g.match(s, i, start): 

1193 if not self.forward: 

1194 i1 += len(end) 

1195 i += offset 

1196 while 0 <= i < len(s): 

1197 if g.match(s, i, end): 

1198 i = i + len(end) if self.forward else i - 1 

1199 return i 

1200 i += offset 

1201 self.oops('unmatched multiline comment') 

1202 elif self.forward: 

1203 # Scan to the newline. 

1204 target = '\n' 

1205 while 0 <= i < len(s): 

1206 if s[i] == '\n': 

1207 i += 1 

1208 return i 

1209 i += 1 

1210 else: 

1211 # Careful: scan to the *first* target on the line 

1212 target = self.single_comment 

1213 found = None 

1214 i -= 1 

1215 while 0 <= i < len(s) and s[i] != '\n': 

1216 if g.match(s, i, target): 

1217 found = i 

1218 i -= 1 

1219 if found is None: 

1220 self.oops('can not happen: unterminated single-line comment') 

1221 found = 0 

1222 return found 

1223 return i 

1224 #@+node:ekr.20160119101851.1: *5* mb.starts_comment 

1225 def starts_comment(self, s: str, i: int) -> bool: 

1226 """Return True if s[i] starts a comment.""" 

1227 assert 0 <= i < len(s) 

1228 if self.forward: 

1229 if self.single_comment and g.match(s, i, self.single_comment): 

1230 return True 

1231 return ( 

1232 self.start_comment and self.end_comment and 

1233 g.match(s, i, self.start_comment) 

1234 ) 

1235 if s[i] == '\n': 

1236 if self.single_comment: 

1237 # Scan backward for any single-comment delim. 

1238 i -= 1 

1239 while i >= 0 and s[i] != '\n': 

1240 if g.match(s, i, self.single_comment): 

1241 return True 

1242 i -= 1 

1243 return False 

1244 return ( 

1245 self.start_comment and self.end_comment and 

1246 g.match(s, i, self.end_comment) 

1247 ) 

1248 #@+node:ekr.20160119230141.1: *4* mb.scan_back & helpers 

1249 def scan_back(self, ch1: str, target: str, s: str, i: int) -> Optional[int]: 

1250 """Scan backwards for delim.""" 

1251 level = 0 

1252 while i >= 0: 

1253 progress = i 

1254 ch = s[i] 

1255 if self.ends_comment(s, i): 

1256 i = self.back_scan_comment(s, i) 

1257 elif ch in '"\'': 

1258 # Scan to the beginning of the string. 

1259 i = self.scan_string(s, i) 

1260 elif ch == '/' and self.is_regex(s, i): 

1261 i = self.scan_regex(s, i) 

1262 elif ch == ch1: 

1263 level += 1 

1264 i -= 1 

1265 elif ch == target: 

1266 level -= 1 

1267 if level <= 0: 

1268 return i 

1269 i -= 1 

1270 else: 

1271 i -= 1 

1272 assert i < progress 

1273 # Not found 

1274 return None 

1275 #@+node:ekr.20160119230141.2: *5* mb.back_scan_comment 

1276 def back_scan_comment(self, s: str, i: int) -> int: 

1277 """Return the index of the character after a comment.""" 

1278 i1 = i 

1279 if g.match(s, i, self.end_comment): 

1280 i1 += len(self.end_comment) # For traces. 

1281 i -= 1 

1282 while i >= 0: 

1283 if g.match(s, i, self.start_comment): 

1284 i -= 1 

1285 return i 

1286 i -= 1 

1287 self.oops('unmatched multiline comment') 

1288 return i 

1289 # Careful: scan to the *first* target on the line 

1290 found = None 

1291 i -= 1 

1292 while i >= 0 and s[i] != '\n': 

1293 if g.match(s, i, self.single_comment): 

1294 found = i - 1 

1295 i -= 1 

1296 if found is None: 

1297 self.oops('can not happen: unterminated single-line comment') 

1298 found = 0 

1299 return found 

1300 #@+node:ekr.20160119230141.4: *5* mb.ends_comment 

1301 def ends_comment(self, s: str, i: int) -> bool: 

1302 """ 

1303 Return True if s[i] ends a comment. This is called while scanning 

1304 backward, so this is a bit of a guess. 

1305 """ 

1306 if s[i] == '\n': 

1307 # This is the hard (dubious) case. 

1308 # Let w, x, y and z stand for any strings not containg // or quotes. 

1309 # Case 1: w"x//y"z Assume // is inside a string. 

1310 # Case 2: x//y"z Assume " is inside the comment. 

1311 # Case 3: w//x"y"z Assume both quotes are inside the comment. 

1312 # 

1313 # That is, we assume (perhaps wrongly) that a quote terminates a 

1314 # string if and *only* if the string starts *and* ends on the line. 

1315 if self.single_comment: 

1316 # Scan backward for single-line comment delims or quotes. 

1317 quote = None 

1318 i -= 1 

1319 while i >= 0 and s[i] != '\n': 

1320 progress = i 

1321 if quote and s[i] == quote: 

1322 quote = None 

1323 i -= 1 

1324 elif s[i] in '"\'': 

1325 if not quote: 

1326 quote = s[i] 

1327 i -= 1 

1328 elif g.match(s, i, self.single_comment): 

1329 # Assume that there is a comment only if the comment delim 

1330 # isn't inside a string that begins and ends on *this* line. 

1331 if quote: 

1332 while i >= 0 and s[i] != 'n': 

1333 if s[i] == quote: 

1334 return False 

1335 i -= 1 

1336 return True 

1337 else: 

1338 i -= 1 

1339 assert progress > i 

1340 return False 

1341 return ( 

1342 self.start_comment and 

1343 self.end_comment and 

1344 g.match(s, i, self.end_comment)) 

1345 #@+node:ekr.20160119104148.1: *4* mb.oops 

1346 def oops(self, s: str) -> None: 

1347 """Report an error in the match-brackets command.""" 

1348 g.es(s, color='red') 

1349 #@+node:ekr.20160119094053.1: *4* mb.run 

1350 #@@nobeautify 

1351 

1352 def run(self) -> None: 

1353 """The driver for the MatchBrackets class. 

1354 

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

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

1357 

1358 With selected range: the first time, move cursor back to other end of 

1359 range. The second time, select enclosing range. 

1360 """ 

1361 # 

1362 # A partial fix for bug 127: Bracket matching is buggy. 

1363 w = self.c.frame.body.wrapper 

1364 s = w.getAllText() 

1365 _mb = self.c.user_dict['_match_brackets'] 

1366 sel_range = w.getSelectionRange() 

1367 if not w.hasSelection(): 

1368 _mb['count'] = 1 

1369 if _mb['range'] == sel_range and _mb['count'] == 1: 

1370 # haven't been to other end yet 

1371 _mb['count'] += 1 

1372 # move insert point to other end of selection 

1373 insert = 1 if w.getInsertPoint() == sel_range[0] else 0 

1374 w.setSelectionRange( 

1375 sel_range[0], sel_range[1], insert=sel_range[insert]) 

1376 return 

1377 

1378 # Find the bracket nearest the cursor. 

1379 max_right = len(s) - 1 # insert point can be past last char. 

1380 left = right = min(max_right, w.getInsertPoint()) 

1381 left, right, ch, index = self.expand_range(s, left, right, max_right) 

1382 if left is None: 

1383 g.es("Bracket not found") 

1384 return 

1385 index2 = self.find_matching_bracket(ch, s, index) 

1386 if index2 is None: 

1387 g.es("No matching bracket.") # #1447. 

1388 return 

1389 

1390 # If this is the first time we've selected the range index-index2, do 

1391 # nothing extra. The second time, move cursor to other end (requires 

1392 # no special action here), and the third time, try to expand the range 

1393 # to any enclosing brackets 

1394 minmax = (min(index, index2), max(index, index2)+1) 

1395 # the range, +1 to match w.getSelectionRange() 

1396 if _mb['range'] == minmax: # count how many times this has been the answer 

1397 _mb['count'] += 1 

1398 else: 

1399 _mb['count'] = 1 

1400 _mb['range'] = minmax 

1401 if _mb['count'] >= 3: # try to expand range 

1402 left, right, ch, index3 = self.expand_range( 

1403 s, 

1404 max(minmax[0], 0), 

1405 min(minmax[1], max_right), 

1406 max_right, expand=True 

1407 ) 

1408 if index3 is not None: # found nearest bracket outside range 

1409 index4 = self.find_matching_bracket(ch, s, index3) 

1410 if index4 is not None: # found matching bracket, expand range 

1411 index, index2 = index3, index4 

1412 _mb['count'] = 1 

1413 _mb['range'] = (min(index3, index4), max(index3, index4)+1) 

1414 

1415 if index2 is not None: 

1416 if index2 < index: 

1417 w.setSelectionRange(index2, index + 1, insert=index2) 

1418 else: 

1419 w.setSelectionRange( 

1420 index, index2 + 1, insert=min(len(s), index2 + 1)) 

1421 w.see(index2) 

1422 else: 

1423 g.es("unmatched", repr(ch)) 

1424 #@-others 

1425#@+node:ekr.20090128083459.82: *3* class g.PosList (deprecated) 

1426class PosList(list): 

1427 #@+<< docstring for PosList >> 

1428 #@+node:ekr.20090130114732.2: *4* << docstring for PosList >> 

1429 """A subclass of list for creating and selecting lists of positions. 

1430 

1431 This is deprecated, use leoNodes.PosList instead! 

1432 

1433 # Creates a PosList containing all positions in c. 

1434 aList = g.PosList(c) 

1435 

1436 # Creates a PosList from aList2. 

1437 aList = g.PosList(c,aList2) 

1438 

1439 # Creates a PosList containing all positions p in aList 

1440 # such that p.h matches the pattern. 

1441 # The pattern is a regular expression if regex is True. 

1442 # if removeClones is True, all positions p2 are removed 

1443 # if a position p is already in the list and p2.v == p.v. 

1444 aList2 = aList.select(pattern,regex=False,removeClones=True) 

1445 

1446 # Prints all positions in aList, sorted if sort is True. 

1447 # Prints p.h, or repr(p) if verbose is True. 

1448 aList.dump(sort=False,verbose=False) 

1449 """ 

1450 #@-<< docstring for PosList >> 

1451 #@+others 

1452 #@+node:ekr.20140531104908.17611: *4* PosList.ctor 

1453 def __init__(self, c: Cmdr, aList: List[Cmdr]=None) -> None: 

1454 self.c = c 

1455 super().__init__() 

1456 if aList is None: 

1457 for p in c.all_positions(): 

1458 self.append(p.copy()) 

1459 else: 

1460 for p in aList: 

1461 self.append(p.copy()) 

1462 #@+node:ekr.20140531104908.17612: *4* PosList.dump 

1463 def dump(self, sort: bool=False, verbose: bool=False) -> str: 

1464 if verbose: 

1465 return g.listToString(self, sort=sort) 

1466 return g.listToString([p.h for p in self], sort=sort) 

1467 #@+node:ekr.20140531104908.17613: *4* PosList.select 

1468 def select(self, pat: str, regex: bool=False, removeClones: bool=True) -> "PosList": 

1469 """ 

1470 Return a new PosList containing all positions 

1471 in self that match the given pattern. 

1472 """ 

1473 c = self.c 

1474 

1475 aList = [] 

1476 if regex: 

1477 for p in self: 

1478 if re.match(pat, p.h): 

1479 aList.append(p.copy()) 

1480 else: 

1481 for p in self: 

1482 if p.h.find(pat) != -1: 

1483 aList.append(p.copy()) 

1484 if removeClones: 

1485 aList = self.removeClones(aList) 

1486 return PosList(c, aList) 

1487 #@+node:ekr.20140531104908.17614: *4* PosList.removeClones 

1488 def removeClones(self, aList: List[Pos]) -> List[Pos]: 

1489 seen = {} 

1490 aList2: List[Pos] = [] 

1491 for p in aList: 

1492 if p.v not in seen: 

1493 seen[p.v] = p.v 

1494 aList2.append(p) 

1495 return aList2 

1496 #@-others 

1497#@+node:EKR.20040612114220.4: *3* class g.ReadLinesClass 

1498class ReadLinesClass: 

1499 """A class whose next method provides a readline method for Python's tokenize module.""" 

1500 

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

1502 self.lines = g.splitLines(s) 

1503 self.i = 0 

1504 

1505 def next(self) -> str: 

1506 if self.i < len(self.lines): 

1507 line = self.lines[self.i] 

1508 self.i += 1 

1509 else: 

1510 line = '' 

1511 return line 

1512 

1513 __next__ = next 

1514#@+node:ekr.20031218072017.3121: *3* class g.RedirectClass & convenience functions 

1515class RedirectClass: 

1516 """A class to redirect stdout and stderr to Leo's log pane.""" 

1517 #@+<< RedirectClass methods >> 

1518 #@+node:ekr.20031218072017.1656: *4* << RedirectClass methods >> 

1519 #@+others 

1520 #@+node:ekr.20041012082437: *5* RedirectClass.__init__ 

1521 def __init__(self) -> None: 

1522 self.old = None 

1523 self.encoding = 'utf-8' # 2019/03/29 For pdb. 

1524 #@+node:ekr.20041012082437.1: *5* isRedirected 

1525 def isRedirected(self) -> bool: 

1526 return self.old is not None 

1527 #@+node:ekr.20041012082437.2: *5* flush 

1528 # For LeoN: just for compatibility. 

1529 

1530 def flush(self, *args: Any) -> None: 

1531 return 

1532 #@+node:ekr.20041012091252: *5* rawPrint 

1533 def rawPrint(self, s: str) -> None: 

1534 if self.old: 

1535 self.old.write(s + '\n') 

1536 else: 

1537 g.pr(s) 

1538 #@+node:ekr.20041012082437.3: *5* redirect 

1539 def redirect(self, stdout: bool=True) -> None: 

1540 if g.app.batchMode: 

1541 # Redirection is futile in batch mode. 

1542 return 

1543 if not self.old: 

1544 if stdout: 

1545 self.old, sys.stdout = sys.stdout, self # type:ignore 

1546 else: 

1547 self.old, sys.stderr = sys.stderr, self # type:ignore 

1548 #@+node:ekr.20041012082437.4: *5* undirect 

1549 def undirect(self, stdout: bool=True) -> None: 

1550 if self.old: 

1551 if stdout: 

1552 sys.stdout, self.old = self.old, None 

1553 else: 

1554 sys.stderr, self.old = self.old, None 

1555 #@+node:ekr.20041012082437.5: *5* write 

1556 def write(self, s: str) -> None: 

1557 

1558 if self.old: 

1559 if app.log: 

1560 app.log.put(s, from_redirect=True) 

1561 else: 

1562 self.old.write(s + '\n') 

1563 else: 

1564 # Can happen when g.batchMode is True. 

1565 g.pr(s) 

1566 #@-others 

1567 #@-<< RedirectClass methods >> 

1568 

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

1570 

1571redirectStdErrObj = RedirectClass() 

1572redirectStdOutObj = RedirectClass() 

1573#@+<< define convenience methods for redirecting streams >> 

1574#@+node:ekr.20031218072017.3122: *4* << define convenience methods for redirecting streams >> 

1575#@+others 

1576#@+node:ekr.20041012090942: *5* redirectStderr & redirectStdout 

1577# Redirect streams to the current log window. 

1578 

1579def redirectStderr() -> None: 

1580 global redirectStdErrObj 

1581 redirectStdErrObj.redirect(stdout=False) 

1582 

1583def redirectStdout() -> None: 

1584 global redirectStdOutObj 

1585 redirectStdOutObj.redirect() 

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

1587# Restore standard streams. 

1588 

1589def restoreStderr() -> None: 

1590 global redirectStdErrObj 

1591 redirectStdErrObj.undirect(stdout=False) 

1592 

1593def restoreStdout() -> None: 

1594 global redirectStdOutObj 

1595 redirectStdOutObj.undirect() 

1596#@+node:ekr.20041012090942.2: *5* stdErrIsRedirected & stdOutIsRedirected 

1597def stdErrIsRedirected() -> bool: 

1598 global redirectStdErrObj 

1599 return redirectStdErrObj.isRedirected() 

1600 

1601def stdOutIsRedirected() -> bool: 

1602 global redirectStdOutObj 

1603 return redirectStdOutObj.isRedirected() 

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

1605# Send output to original stdout. 

1606 

1607def rawPrint(s: str) -> None: 

1608 global redirectStdOutObj 

1609 redirectStdOutObj.rawPrint(s) 

1610#@-others 

1611#@-<< define convenience methods for redirecting streams >> 

1612#@+node:ekr.20121128031949.12605: *3* class g.SherlockTracer 

1613class SherlockTracer: 

1614 """ 

1615 A stand-alone tracer class with many of Sherlock's features. 

1616 

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

1618 

1619 The arguments in the pattern lists determine which functions get traced 

1620 or which stats get printed. Each pattern starts with "+", "-", "+:" or 

1621 "-:", followed by a regular expression:: 

1622 

1623 "+x" Enables tracing (or stats) for all functions/methods whose name 

1624 matches the regular expression x. 

1625 "-x" Disables tracing for functions/methods. 

1626 "+:x" Enables tracing for all functions in the **file** whose name matches x. 

1627 "-:x" Disables tracing for an entire file. 

1628 

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

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

1631 

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

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

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

1635 

1636 This enables tracing for everything, then disables tracing for all 

1637 library modules, except for all rope modules. Finally, it disables the 

1638 tracing for Rope's worder, prefs and resources modules. Btw, this is 

1639 one of the best uses for regular expressions that I know of. 

1640 

1641 Being able to zero in on the code of interest can be a big help in 

1642 studying other people's code. This is a non-invasive method: no tracing 

1643 code needs to be inserted anywhere. 

1644 

1645 Usage: 

1646 

1647 g.SherlockTracer(patterns).run() 

1648 """ 

1649 #@+others 

1650 #@+node:ekr.20121128031949.12602: *4* __init__ 

1651 def __init__( 

1652 self, 

1653 patterns: List[Any], 

1654 dots: bool=True, 

1655 show_args: bool=True, 

1656 show_return: bool=True, 

1657 verbose: bool=True, 

1658 ) -> None: 

1659 """SherlockTracer ctor.""" 

1660 self.bad_patterns: List[str] = [] # List of bad patterns. 

1661 self.dots = dots # True: print level dots. 

1662 self.contents_d: Dict[str, List] = {} # Keys are file names, values are file lines. 

1663 self.n = 0 # The frame level on entry to run. 

1664 self.stats: Dict[str, Dict] = {} # Keys are full file names, values are dicts. 

1665 self.patterns: List[Any] = None # A list of regex patterns to match. 

1666 self.pattern_stack: List[str] = [] 

1667 self.show_args = show_args # True: show args for each function call. 

1668 self.show_return = show_return # True: show returns from each function. 

1669 self.trace_lines = True # True: trace lines in enabled functions. 

1670 self.verbose = verbose # True: print filename:func 

1671 self.set_patterns(patterns) 

1672 from leo.core.leoQt import QtCore 

1673 if QtCore: 

1674 # pylint: disable=no-member 

1675 QtCore.pyqtRemoveInputHook() 

1676 #@+node:ekr.20140326100337.16844: *4* __call__ 

1677 def __call__(self, frame: Any, event: Any, arg: Any) -> Any: 

1678 """Exists so that self.dispatch can return self.""" 

1679 return self.dispatch(frame, event, arg) 

1680 #@+node:ekr.20140326100337.16846: *4* sherlock.bad_pattern 

1681 def bad_pattern(self, pattern: Any) -> None: 

1682 """Report a bad Sherlock pattern.""" 

1683 if pattern not in self.bad_patterns: 

1684 self.bad_patterns.append(pattern) 

1685 print(f"\nignoring bad pattern: {pattern}\n") 

1686 #@+node:ekr.20140326100337.16847: *4* sherlock.check_pattern 

1687 def check_pattern(self, pattern: str) -> bool: 

1688 """Give an error and return False for an invalid pattern.""" 

1689 try: 

1690 for prefix in ('+:', '-:', '+', '-'): 

1691 if pattern.startswith(prefix): 

1692 re.match(pattern[len(prefix) :], 'xyzzy') 

1693 return True 

1694 self.bad_pattern(pattern) 

1695 return False 

1696 except Exception: 

1697 self.bad_pattern(pattern) 

1698 return False 

1699 #@+node:ekr.20121128031949.12609: *4* sherlock.dispatch 

1700 def dispatch(self, frame: Any, event: Any, arg: Any) -> Any: 

1701 """The dispatch method.""" 

1702 if event == 'call': 

1703 self.do_call(frame, arg) 

1704 elif event == 'return' and self.show_return: 

1705 self.do_return(frame, arg) 

1706 elif event == 'line' and self.trace_lines: 

1707 self.do_line(frame, arg) 

1708 # Queue the SherlockTracer instance again. 

1709 return self 

1710 #@+node:ekr.20121128031949.12603: *4* sherlock.do_call & helper 

1711 def do_call(self, frame: Any, unused_arg: Any) -> None: 

1712 """Trace through a function call.""" 

1713 frame1 = frame 

1714 code = frame.f_code 

1715 file_name = code.co_filename 

1716 locals_ = frame.f_locals 

1717 function_name = code.co_name 

1718 try: 

1719 full_name = self.get_full_name(locals_, function_name) 

1720 except Exception: 

1721 full_name = function_name 

1722 if not self.is_enabled(file_name, full_name, self.patterns): 

1723 # 2020/09/09: Don't touch, for example, __ methods. 

1724 return 

1725 n = 0 # The number of callers of this def. 

1726 while frame: 

1727 frame = frame.f_back 

1728 n += 1 

1729 dots = '.' * max(0, n - self.n) if self.dots else '' 

1730 path = f"{os.path.basename(file_name):>20}" if self.verbose else '' 

1731 leadin = '+' if self.show_return else '' 

1732 args = "(%s)" % self.get_args(frame1) if self.show_args else '' 

1733 print(f"{path}:{dots}{leadin}{full_name}{args}") 

1734 # Always update stats. 

1735 d = self.stats.get(file_name, {}) 

1736 d[full_name] = 1 + d.get(full_name, 0) 

1737 self.stats[file_name] = d 

1738 #@+node:ekr.20130111185820.10194: *5* sherlock.get_args 

1739 def get_args(self, frame: Any) -> str: 

1740 """Return name=val for each arg in the function call.""" 

1741 code = frame.f_code 

1742 locals_ = frame.f_locals 

1743 name = code.co_name 

1744 n = code.co_argcount 

1745 if code.co_flags & 4: 

1746 n = n + 1 

1747 if code.co_flags & 8: 

1748 n = n + 1 

1749 result = [] 

1750 for i in range(n): 

1751 name = code.co_varnames[i] 

1752 if name != 'self': 

1753 arg = locals_.get(name, '*undefined*') 

1754 if arg: 

1755 if isinstance(arg, (list, tuple)): 

1756 # Clearer w/o f-string 

1757 val = "[%s]" % ','.join( 

1758 [self.show(z) for z in arg if self.show(z)]) 

1759 else: 

1760 val = self.show(arg) 

1761 if val: 

1762 result.append(f"{name}={val}") 

1763 return ','.join(result) 

1764 #@+node:ekr.20140402060647.16845: *4* sherlock.do_line (not used) 

1765 bad_fns: List[str] = [] 

1766 

1767 def do_line(self, frame: Any, arg: Any) -> None: 

1768 """print each line of enabled functions.""" 

1769 if 1: 

1770 return 

1771 code = frame.f_code 

1772 file_name = code.co_filename 

1773 locals_ = frame.f_locals 

1774 name = code.co_name 

1775 full_name = self.get_full_name(locals_, name) 

1776 if not self.is_enabled(file_name, full_name, self.patterns): 

1777 return 

1778 n = frame.f_lineno - 1 # Apparently, the first line is line 1. 

1779 d = self.contents_d 

1780 lines = d.get(file_name) 

1781 if not lines: 

1782 print(file_name) 

1783 try: 

1784 with open(file_name) as f: 

1785 s = f.read() 

1786 except Exception: 

1787 if file_name not in self.bad_fns: 

1788 self.bad_fns.append(file_name) 

1789 print(f"open({file_name}) failed") 

1790 return 

1791 lines = g.splitLines(s) 

1792 d[file_name] = lines 

1793 line = lines[n].rstrip() if n < len(lines) else '<EOF>' 

1794 if 0: 

1795 print(f"{name:3} {line}") 

1796 else: 

1797 print(f"{g.shortFileName(file_name)} {n} {full_name} {line}") 

1798 #@+node:ekr.20130109154743.10172: *4* sherlock.do_return & helper 

1799 def do_return(self, frame: Any, arg: Any) -> None: # Arg *is* used below. 

1800 """Trace a return statement.""" 

1801 code = frame.f_code 

1802 fn = code.co_filename 

1803 locals_ = frame.f_locals 

1804 name = code.co_name 

1805 full_name = self.get_full_name(locals_, name) 

1806 if self.is_enabled(fn, full_name, self.patterns): 

1807 n = 0 

1808 while frame: 

1809 frame = frame.f_back 

1810 n += 1 

1811 dots = '.' * max(0, n - self.n) if self.dots else '' 

1812 path = f"{os.path.basename(fn):>20}" if self.verbose else '' 

1813 if name and name == '__init__': 

1814 try: 

1815 ret1 = locals_ and locals_.get('self', None) 

1816 ret = self.format_ret(ret1) 

1817 except NameError: 

1818 ret = f"<{ret1.__class__.__name__}>" 

1819 else: 

1820 ret = self.format_ret(arg) 

1821 print(f"{path}{dots}-{full_name}{ret}") 

1822 #@+node:ekr.20130111120935.10192: *5* sherlock.format_ret 

1823 def format_ret(self, arg: Any) -> str: 

1824 """Format arg, the value returned by a "return" statement.""" 

1825 try: 

1826 if isinstance(arg, types.GeneratorType): 

1827 ret = '<generator>' 

1828 elif isinstance(arg, (tuple, list)): 

1829 # Clearer w/o f-string. 

1830 ret = "[%s]" % ','.join([self.show(z) for z in arg]) 

1831 if len(ret) > 40: 

1832 # Clearer w/o f-string. 

1833 ret = "[\n%s]" % ('\n,'.join([self.show(z) for z in arg])) 

1834 elif arg: 

1835 ret = self.show(arg) 

1836 if len(ret) > 40: 

1837 ret = f"\n {ret}" 

1838 else: 

1839 ret = '' if arg is None else repr(arg) 

1840 except Exception: 

1841 exctype, value = sys.exc_info()[:2] 

1842 s = f"<**exception: {exctype.__name__}, {value} arg: {arg !r}**>" 

1843 ret = f" ->\n {s}" if len(s) > 40 else f" -> {s}" 

1844 return f" -> {ret}" 

1845 #@+node:ekr.20121128111829.12185: *4* sherlock.fn_is_enabled (not used) 

1846 def fn_is_enabled(self, func: Any, patterns: List[str]) -> bool: 

1847 """Return True if tracing for the given function is enabled.""" 

1848 if func in self.ignored_functions: 

1849 return False 

1850 

1851 def ignore_function() -> None: 

1852 if func not in self.ignored_functions: 

1853 self.ignored_functions.append(func) 

1854 print(f"Ignore function: {func}") 

1855 # 

1856 # New in Leo 6.3. Never trace dangerous functions. 

1857 table = ( 

1858 '_deepcopy.*', 

1859 # Unicode primitives. 

1860 'encode\b', 'decode\b', 

1861 # System functions 

1862 '.*__next\b', 

1863 '<frozen>', '<genexpr>', '<listcomp>', 

1864 # '<decorator-gen-.*>', 

1865 'get\b', 

1866 # String primitives. 

1867 'append\b', 'split\b', 'join\b', 

1868 # File primitives... 

1869 'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b', 

1870 'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b', 

1871 ) 

1872 g.trace('=====', func) 

1873 for z in table: 

1874 if re.match(z, func): 

1875 ignore_function() 

1876 return False 

1877 # 

1878 # Legacy code. 

1879 try: 

1880 enabled, pattern = False, None 

1881 for pattern in patterns: 

1882 if pattern.startswith('+:'): 

1883 if re.match(pattern[2:], func): 

1884 enabled = True 

1885 elif pattern.startswith('-:'): 

1886 if re.match(pattern[2:], func): 

1887 enabled = False 

1888 return enabled 

1889 except Exception: 

1890 self.bad_pattern(pattern) 

1891 return False 

1892 #@+node:ekr.20130112093655.10195: *4* get_full_name 

1893 def get_full_name(self, locals_: Any, name: str) -> str: 

1894 """Return class_name::name if possible.""" 

1895 full_name = name 

1896 try: 

1897 user_self = locals_ and locals_.get('self', None) 

1898 if user_self: 

1899 full_name = user_self.__class__.__name__ + '::' + name 

1900 except Exception: 

1901 pass 

1902 return full_name 

1903 #@+node:ekr.20121128111829.12183: *4* sherlock.is_enabled 

1904 ignored_files: List[str] = [] # List of files. 

1905 ignored_functions: List[str] = [] # List of files. 

1906 

1907 def is_enabled( 

1908 self, 

1909 file_name: str, 

1910 function_name: str, 

1911 patterns: List[str]=None, 

1912 ) -> bool: 

1913 """Return True if tracing for function_name in the given file is enabled.""" 

1914 # 

1915 # New in Leo 6.3. Never trace through some files. 

1916 if not os: 

1917 return False # Shutting down. 

1918 base_name = os.path.basename(file_name) 

1919 if base_name in self.ignored_files: 

1920 return False 

1921 

1922 def ignore_file() -> None: 

1923 if not base_name in self.ignored_files: 

1924 self.ignored_files.append(base_name) 

1925 

1926 def ignore_function() -> None: 

1927 if function_name not in self.ignored_functions: 

1928 self.ignored_functions.append(function_name) 

1929 

1930 if f"{os.sep}lib{os.sep}" in file_name: 

1931 ignore_file() 

1932 return False 

1933 if base_name.startswith('<') and base_name.endswith('>'): 

1934 ignore_file() 

1935 return False 

1936 # 

1937 # New in Leo 6.3. Never trace dangerous functions. 

1938 table = ( 

1939 '_deepcopy.*', 

1940 # Unicode primitives. 

1941 'encode\b', 'decode\b', 

1942 # System functions 

1943 '.*__next\b', 

1944 '<frozen>', '<genexpr>', '<listcomp>', 

1945 # '<decorator-gen-.*>', 

1946 'get\b', 

1947 # String primitives. 

1948 'append\b', 'split\b', 'join\b', 

1949 # File primitives... 

1950 'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b', 

1951 'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b', 

1952 ) 

1953 for z in table: 

1954 if re.match(z, function_name): 

1955 ignore_function() 

1956 return False 

1957 # 

1958 # Legacy code. 

1959 enabled = False 

1960 if patterns is None: 

1961 patterns = self.patterns 

1962 for pattern in patterns: 

1963 try: 

1964 if pattern.startswith('+:'): 

1965 if re.match(pattern[2:], file_name): 

1966 enabled = True 

1967 elif pattern.startswith('-:'): 

1968 if re.match(pattern[2:], file_name): 

1969 enabled = False 

1970 elif pattern.startswith('+'): 

1971 if re.match(pattern[1:], function_name): 

1972 enabled = True 

1973 elif pattern.startswith('-'): 

1974 if re.match(pattern[1:], function_name): 

1975 enabled = False 

1976 else: 

1977 self.bad_pattern(pattern) 

1978 except Exception: 

1979 self.bad_pattern(pattern) 

1980 return enabled 

1981 #@+node:ekr.20121128111829.12182: *4* print_stats 

1982 def print_stats(self, patterns: List[str]=None) -> None: 

1983 """Print all accumulated statisitics.""" 

1984 print('\nSherlock statistics...') 

1985 if not patterns: 

1986 patterns = ['+.*', '+:.*',] 

1987 for fn in sorted(self.stats.keys()): 

1988 d = self.stats.get(fn) 

1989 if self.fn_is_enabled(fn, patterns): 

1990 result = sorted(d.keys()) # type:ignore 

1991 else: 

1992 result = [key for key in sorted(d.keys()) # type:ignore 

1993 if self.is_enabled(fn, key, patterns)] 

1994 if result: 

1995 print('') 

1996 fn = fn.replace('\\', '/') 

1997 parts = fn.split('/') 

1998 print('/'.join(parts[-2:])) 

1999 for key in result: 

2000 print(f"{d.get(key):4} {key}") 

2001 #@+node:ekr.20121128031949.12614: *4* run 

2002 # Modified from pdb.Pdb.set_trace. 

2003 

2004 def run(self, frame: Any=None) -> None: 

2005 """Trace from the given frame or the caller's frame.""" 

2006 print("SherlockTracer.run:patterns:\n%s" % '\n'.join(self.patterns)) 

2007 if frame is None: 

2008 frame = sys._getframe().f_back 

2009 # Compute self.n, the number of frames to ignore. 

2010 self.n = 0 

2011 while frame: 

2012 frame = frame.f_back 

2013 self.n += 1 

2014 # Pass self to sys.settrace to give easy access to all methods. 

2015 sys.settrace(self) 

2016 #@+node:ekr.20140322090829.16834: *4* push & pop 

2017 def push(self, patterns: List[str]) -> None: 

2018 """Push the old patterns and set the new.""" 

2019 self.pattern_stack.append(self.patterns) # type:ignore 

2020 self.set_patterns(patterns) 

2021 print(f"SherlockTracer.push: {self.patterns}") 

2022 

2023 def pop(self) -> None: 

2024 """Restore the pushed patterns.""" 

2025 if self.pattern_stack: 

2026 self.patterns = self.pattern_stack.pop() # type:ignore 

2027 print(f"SherlockTracer.pop: {self.patterns}") 

2028 else: 

2029 print('SherlockTracer.pop: pattern stack underflow') 

2030 #@+node:ekr.20140326100337.16845: *4* set_patterns 

2031 def set_patterns(self, patterns: List[str]) -> None: 

2032 """Set the patterns in effect.""" 

2033 self.patterns = [z for z in patterns if self.check_pattern(z)] 

2034 #@+node:ekr.20140322090829.16831: *4* show 

2035 def show(self, item: Any) -> str: 

2036 """return the best representation of item.""" 

2037 if not item: 

2038 return repr(item) 

2039 if isinstance(item, dict): 

2040 return 'dict' 

2041 if isinstance(item, str): 

2042 s = repr(item) 

2043 if len(s) <= 20: 

2044 return s 

2045 return s[:17] + '...' 

2046 return repr(item) 

2047 #@+node:ekr.20121128093229.12616: *4* stop 

2048 def stop(self) -> None: 

2049 """Stop all tracing.""" 

2050 sys.settrace(None) 

2051 #@-others 

2052#@+node:ekr.20191013145307.1: *3* class g.TkIDDialog (EmergencyDialog) 

2053class TkIDDialog(EmergencyDialog): 

2054 """A class that creates an tkinter dialog to get the Leo ID.""" 

2055 

2056 message = ( 

2057 "leoID.txt not found\n\n" 

2058 "Please enter an id that identifies you uniquely.\n" 

2059 "Your git/cvs/bzr login name is a good choice.\n\n" 

2060 "Leo uses this id to uniquely identify nodes.\n\n" 

2061 "Your id should contain only letters and numbers\n" 

2062 "and must be at least 3 characters in length.") 

2063 

2064 title = 'Enter Leo id' 

2065 

2066 def __init__(self) -> None: 

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

2068 self.val = '' 

2069 

2070 #@+others 

2071 #@+node:ekr.20191013145710.1: *4* leo_id_dialog.onKey 

2072 def onKey(self, event: Any) -> None: 

2073 """Handle Key events in askOk dialogs.""" 

2074 if event.char in '\n\r': 

2075 self.okButton() 

2076 #@+node:ekr.20191013145757.1: *4* leo_id_dialog.createTopFrame 

2077 def createTopFrame(self) -> None: 

2078 """Create the Tk.Toplevel widget for a leoTkinterDialog.""" 

2079 self.root = Tk.Tk() # type:ignore 

2080 self.top = Tk.Toplevel(self.root) # type:ignore 

2081 self.top.title(self.title) 

2082 self.root.withdraw() 

2083 self.frame = Tk.Frame(self.top) # type:ignore 

2084 self.frame.pack(side="top", expand=1, fill="both") 

2085 label = Tk.Label(self.frame, text=self.message, bg='white') 

2086 label.pack(pady=10) 

2087 self.entry = Tk.Entry(self.frame) 

2088 self.entry.pack() 

2089 self.entry.focus_set() 

2090 #@+node:ekr.20191013150158.1: *4* leo_id_dialog.okButton 

2091 def okButton(self) -> None: 

2092 """Do default click action in ok button.""" 

2093 self.val = self.entry.get() # Return is not possible. 

2094 self.top.destroy() 

2095 self.top = None 

2096 #@-others 

2097#@+node:ekr.20080531075119.1: *3* class g.Tracer 

2098class Tracer: 

2099 """A "debugger" that computes a call graph. 

2100 

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

2102 

2103 g.startTracer() 

2104 """ 

2105 #@+others 

2106 #@+node:ekr.20080531075119.2: *4* __init__ (Tracer) 

2107 def __init__(self, limit: int=0, trace: bool=False, verbose: bool=False) -> None: 

2108 self.callDict: Dict[str, Any] = {} 

2109 # Keys are function names. 

2110 # Values are the number of times the function was called by the caller. 

2111 self.calledDict: Dict[str, int] = {} 

2112 # Keys are function names. 

2113 # Values are the total number of times the function was called. 

2114 self.count = 0 

2115 self.inited = False 

2116 self.limit = limit # 0: no limit, otherwise, limit trace to n entries deep. 

2117 self.stack: List[str] = [] 

2118 self.trace = trace 

2119 self.verbose = verbose # True: print returns as well as calls. 

2120 #@+node:ekr.20080531075119.3: *4* computeName 

2121 def computeName(self, frame: Any) -> str: 

2122 if not frame: 

2123 return '' 

2124 code = frame.f_code 

2125 result = [] 

2126 module = inspect.getmodule(code) 

2127 if module: 

2128 module_name = module.__name__ 

2129 if module_name == 'leo.core.leoGlobals': 

2130 result.append('g') 

2131 else: 

2132 tag = 'leo.core.' 

2133 if module_name.startswith(tag): 

2134 module_name = module_name[len(tag) :] 

2135 result.append(module_name) 

2136 try: 

2137 # This can fail during startup. 

2138 self_obj = frame.f_locals.get('self') 

2139 if self_obj: 

2140 result.append(self_obj.__class__.__name__) 

2141 except Exception: 

2142 pass 

2143 result.append(code.co_name) 

2144 return '.'.join(result) 

2145 #@+node:ekr.20080531075119.4: *4* report 

2146 def report(self) -> None: 

2147 if 0: 

2148 g.pr('\nstack') 

2149 for z in self.stack: 

2150 g.pr(z) 

2151 g.pr('\ncallDict...') 

2152 for key in sorted(self.callDict): 

2153 # Print the calling function. 

2154 g.pr(f"{self.calledDict.get(key,0):d}", key) 

2155 # Print the called functions. 

2156 d = self.callDict.get(key) 

2157 for key2 in sorted(d): # type:ignore 

2158 g.pr(f"{d.get(key2):8d}", key2) # type:ignore 

2159 #@+node:ekr.20080531075119.5: *4* stop 

2160 def stop(self) -> None: 

2161 sys.settrace(None) 

2162 self.report() 

2163 #@+node:ekr.20080531075119.6: *4* tracer 

2164 def tracer(self, frame: Any, event: Any, arg: Any) -> Optional[Callable]: 

2165 """A function to be passed to sys.settrace.""" 

2166 n = len(self.stack) 

2167 if event == 'return': 

2168 n = max(0, n - 1) 

2169 pad = '.' * n 

2170 if event == 'call': 

2171 if not self.inited: 

2172 # Add an extra stack element for the routine containing the call to startTracer. 

2173 self.inited = True 

2174 name = self.computeName(frame.f_back) 

2175 self.updateStats(name) 

2176 self.stack.append(name) 

2177 name = self.computeName(frame) 

2178 if self.trace and (self.limit == 0 or len(self.stack) < self.limit): 

2179 g.trace(f"{pad}call", name) 

2180 self.updateStats(name) 

2181 self.stack.append(name) 

2182 return self.tracer 

2183 if event == 'return': 

2184 if self.stack: 

2185 name = self.stack.pop() 

2186 if ( 

2187 self.trace and 

2188 self.verbose and 

2189 (self.limit == 0 or len(self.stack) < self.limit) 

2190 ): 

2191 g.trace(f"{pad}ret ", name) 

2192 else: 

2193 g.trace('return underflow') 

2194 self.stop() 

2195 return None 

2196 if self.stack: 

2197 return self.tracer 

2198 self.stop() 

2199 return None 

2200 return self.tracer 

2201 #@+node:ekr.20080531075119.7: *4* updateStats 

2202 def updateStats(self, name: str) -> None: 

2203 if not self.stack: 

2204 return 

2205 caller = self.stack[-1] 

2206 # d is a dict reprenting the called functions. 

2207 # Keys are called functions, values are counts. 

2208 d: Dict[str, int] = self.callDict.get(caller, {}) 

2209 d[name] = 1 + d.get(name, 0) 

2210 self.callDict[caller] = d 

2211 # Update the total counts. 

2212 self.calledDict[name] = 1 + self.calledDict.get(name, 0) 

2213 #@-others 

2214 

2215def startTracer(limit: int=0, trace: bool=False, verbose: bool=False) -> Callable: 

2216 t = g.Tracer(limit=limit, trace=trace, verbose=verbose) 

2217 sys.settrace(t.tracer) 

2218 return t 

2219#@+node:ekr.20031219074948.1: *3* class g.Tracing/NullObject & helpers 

2220#@@nobeautify 

2221 

2222tracing_tags: Dict[int, str] = {} # Keys are id's, values are tags. 

2223tracing_vars: Dict[int, List] = {} # Keys are id's, values are names of ivars. 

2224# Keys are signatures: '%s.%s:%s' % (tag, attr, callers). Values not important. 

2225tracing_signatures: Dict[str, Any] = {} 

2226 

2227class NullObject: 

2228 """An object that does nothing, and does it very well.""" 

2229 def __init__(self, ivars: List[str]=None, *args: Any, **kwargs: Any) -> None: 

2230 if isinstance(ivars, str): 

2231 ivars = [ivars] 

2232 tracing_vars [id(self)] = ivars or [] 

2233 def __call__(self, *args: Any, **keys: Any) -> "NullObject": 

2234 return self 

2235 def __repr__(self) -> str: 

2236 return "NullObject" 

2237 def __str__(self) -> str: 

2238 return "NullObject" 

2239 # Attribute access... 

2240 def __delattr__(self, attr: str) -> None: 

2241 return None 

2242 def __getattr__(self, attr: str) -> Any: 

2243 if attr in tracing_vars.get(id(self), []): 

2244 return getattr(self, attr, None) 

2245 return self # Required. 

2246 def __setattr__(self, attr: str, val: Any) -> None: 

2247 if attr in tracing_vars.get(id(self), []): 

2248 object.__setattr__(self, attr, val) 

2249 # Container methods.. 

2250 def __bool__(self) -> bool: 

2251 return False 

2252 def __contains__(self, item: Any) -> bool: 

2253 return False 

2254 def __getitem__(self, key: str) -> None: 

2255 raise KeyError 

2256 def __setitem__(self, key: str, val: Any) -> None: 

2257 pass 

2258 def __iter__(self) -> "NullObject": 

2259 return self 

2260 def __len__(self) -> int: 

2261 return 0 

2262 # Iteration methods: 

2263 def __next__(self) -> None: 

2264 raise StopIteration 

2265 

2266 

2267class TracingNullObject: 

2268 """Tracing NullObject.""" 

2269 def __init__(self, tag: str, ivars: List[Any]=None, *args: Any, **kwargs: Any) -> None: 

2270 tracing_tags [id(self)] = tag 

2271 if isinstance(ivars, str): 

2272 ivars = [ivars] 

2273 tracing_vars [id(self)] = ivars or [] 

2274 def __call__(self, *args: Any, **kwargs: Any) -> "TracingNullObject": 

2275 return self 

2276 def __repr__(self) -> str: 

2277 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}' 

2278 def __str__(self) -> str: 

2279 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}' 

2280 # 

2281 # Attribute access... 

2282 def __delattr__(self, attr: str) -> None: 

2283 return None 

2284 def __getattr__(self, attr: str) -> "TracingNullObject": 

2285 null_object_print_attr(id(self), attr) 

2286 if attr in tracing_vars.get(id(self), []): 

2287 return getattr(self, attr, None) 

2288 return self # Required. 

2289 def __setattr__(self, attr: str, val: Any) -> None: 

2290 g.null_object_print(id(self), '__setattr__', attr, val) 

2291 if attr in tracing_vars.get(id(self), []): 

2292 object.__setattr__(self, attr, val) 

2293 # 

2294 # All other methods... 

2295 def __bool__(self) -> bool: 

2296 if 0: # To do: print only once. 

2297 suppress = ('getShortcut','on_idle', 'setItemText') 

2298 callers = g.callers(2) 

2299 if not callers.endswith(suppress): 

2300 g.null_object_print(id(self), '__bool__') 

2301 return False 

2302 def __contains__(self, item: Any) -> bool: 

2303 g.null_object_print(id(self), '__contains__') 

2304 return False 

2305 def __getitem__(self, key: str) -> None: 

2306 g.null_object_print(id(self), '__getitem__') 

2307 # pylint doesn't like trailing return None. 

2308 def __iter__(self) -> "TracingNullObject": 

2309 g.null_object_print(id(self), '__iter__') 

2310 return self 

2311 def __len__(self) -> int: 

2312 # g.null_object_print(id(self), '__len__') 

2313 return 0 

2314 def __next__(self) -> None: 

2315 g.null_object_print(id(self), '__next__') 

2316 raise StopIteration 

2317 def __setitem__(self, key: str, val: Any) -> None: 

2318 g.null_object_print(id(self), '__setitem__') 

2319 # pylint doesn't like trailing return None. 

2320#@+node:ekr.20190330062625.1: *4* g.null_object_print_attr 

2321def null_object_print_attr(id_: int, attr: str) -> None: 

2322 suppress = True 

2323 suppress_callers: List[str] = [] 

2324 suppress_attrs: List[str] = [] 

2325 if suppress: 

2326 #@+<< define suppression lists >> 

2327 #@+node:ekr.20190330072026.1: *5* << define suppression lists >> 

2328 suppress_callers = [ 

2329 'drawNode', 'drawTopTree', 'drawTree', 

2330 'contractItem', 'getCurrentItem', 

2331 'declutter_node', 

2332 'finishCreate', 

2333 'initAfterLoad', 

2334 'show_tips', 

2335 'writeWaitingLog', 

2336 # 'set_focus', 'show_tips', 

2337 ] 

2338 suppress_attrs = [ 

2339 # Leo... 

2340 'c.frame.body.wrapper', 

2341 'c.frame.getIconBar.add', 

2342 'c.frame.log.createTab', 

2343 'c.frame.log.enable', 

2344 'c.frame.log.finishCreate', 

2345 'c.frame.menu.createMenuBar', 

2346 'c.frame.menu.finishCreate', 

2347 # 'c.frame.menu.getMenu', 

2348 'currentItem', 

2349 'dw.leo_master.windowTitle', 

2350 # Pyzo... 

2351 'pyzo.keyMapper.connect', 

2352 'pyzo.keyMapper.keyMappingChanged', 

2353 'pyzo.keyMapper.setShortcut', 

2354 ] 

2355 #@-<< define suppression lists >> 

2356 tag = tracing_tags.get(id_, "<NO TAG>") 

2357 callers = g.callers(3).split(',') 

2358 callers = ','.join(callers[:-1]) 

2359 in_callers = any(z in callers for z in suppress_callers) 

2360 s = f"{tag}.{attr}" 

2361 if suppress: 

2362 # Filter traces. 

2363 if not in_callers and s not in suppress_attrs: 

2364 g.pr(f"{s:40} {callers}") 

2365 else: 

2366 # Print each signature once. No need to filter! 

2367 signature = f"{tag}.{attr}:{callers}" 

2368 if signature not in tracing_signatures: 

2369 tracing_signatures[signature] = True 

2370 g.pr(f"{s:40} {callers}") 

2371#@+node:ekr.20190330072832.1: *4* g.null_object_print 

2372def null_object_print(id_: int, kind: Any, *args: Any) -> None: 

2373 tag = tracing_tags.get(id_, "<NO TAG>") 

2374 callers = g.callers(3).split(',') 

2375 callers = ','.join(callers[:-1]) 

2376 s = f"{kind}.{tag}" 

2377 signature = f"{s}:{callers}" 

2378 if 1: 

2379 # Always print: 

2380 if args: 

2381 args_s = ', '.join([repr(z) for z in args]) 

2382 g.pr(f"{s:40} {callers}\n\t\t\targs: {args_s}") 

2383 else: 

2384 g.pr(f"{s:40} {callers}") 

2385 elif signature not in tracing_signatures: 

2386 # Print each signature once. 

2387 tracing_signatures[signature] = True 

2388 g.pr(f"{s:40} {callers}") 

2389#@+node:ekr.20120129181245.10220: *3* class g.TypedDict 

2390class TypedDict: 

2391 """ 

2392 A class providing additional dictionary-related methods: 

2393 

2394 __init__: Specifies types and the dict's name. 

2395 __repr__: Compatible with g.printObj, based on g.objToString. 

2396 __setitem__: Type checks its arguments. 

2397 __str__: A concise summary of the inner dict. 

2398 add_to_list: A convenience method that adds a value to its key's list. 

2399 name: The dict's name. 

2400 setName: Sets the dict's name, for use by __repr__. 

2401 

2402 Overrides the following standard methods: 

2403 

2404 copy: A thin wrapper for copy.deepcopy. 

2405 get: Returns self.d.get 

2406 items: Returns self.d.items 

2407 keys: Returns self.d.keys 

2408 update: Updates self.d from either a dict or a TypedDict. 

2409 """ 

2410 

2411 def __init__(self, name: str, keyType: Any, valType: Any) -> None: 

2412 self.d: Dict[str, Any] = {} 

2413 self._name = name # For __repr__ only. 

2414 self.keyType = keyType 

2415 self.valType = valType 

2416 #@+others 

2417 #@+node:ekr.20120205022040.17770: *4* td.__repr__ & __str__ 

2418 def __str__(self) -> str: 

2419 """Concise: used by repr.""" 

2420 return ( 

2421 f"<TypedDict name:{self._name} " 

2422 f"keys:{self.keyType.__name__} " 

2423 f"values:{self.valType.__name__} " 

2424 f"len(keys): {len(list(self.keys()))}>" 

2425 ) 

2426 

2427 def __repr__(self) -> str: 

2428 """Suitable for g.printObj""" 

2429 return f"{g.dictToString(self.d)}\n{str(self)}\n" 

2430 #@+node:ekr.20120205022040.17774: *4* td.__setitem__ 

2431 def __setitem__(self, key: Any, val: Any) -> None: 

2432 """Allow d[key] = val""" 

2433 if key is None: 

2434 g.trace('TypeDict: None is not a valid key', g.callers()) 

2435 return 

2436 self._checkKeyType(key) 

2437 try: 

2438 for z in val: 

2439 self._checkValType(z) 

2440 except TypeError: 

2441 self._checkValType(val) # val is not iterable. 

2442 self.d[key] = val 

2443 #@+node:ekr.20190904052828.1: *4* td.add_to_list 

2444 def add_to_list(self, key: Any, val: Any) -> None: 

2445 """Update the *list*, self.d [key]""" 

2446 if key is None: 

2447 g.trace('TypeDict: None is not a valid key', g.callers()) 

2448 return 

2449 self._checkKeyType(key) 

2450 self._checkValType(val) 

2451 aList = self.d.get(key, []) 

2452 if val not in aList: 

2453 aList.append(val) 

2454 self.d[key] = aList 

2455 #@+node:ekr.20120206134955.10150: *4* td.checking 

2456 def _checkKeyType(self, key: str) -> None: 

2457 if key and key.__class__ != self.keyType: 

2458 self._reportTypeError(key, self.keyType) 

2459 

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

2461 if val.__class__ != self.valType: 

2462 self._reportTypeError(val, self.valType) 

2463 

2464 def _reportTypeError(self, obj: Any, objType: Any) -> str: 

2465 return ( 

2466 f"{self._name}\n" 

2467 f"expected: {obj.__class__.__name__}\n" 

2468 f" got: {objType.__name__}") 

2469 #@+node:ekr.20120223062418.10422: *4* td.copy 

2470 def copy(self, name: str=None) -> Any: 

2471 """Return a new dict with the same contents.""" 

2472 import copy 

2473 return copy.deepcopy(self) 

2474 #@+node:ekr.20120205022040.17771: *4* td.get & keys & values 

2475 def get(self, key: Any, default: Any=None) -> Any: 

2476 return self.d.get(key, default) 

2477 

2478 def items(self) -> Any: 

2479 return self.d.items() 

2480 

2481 def keys(self) -> Any: 

2482 return self.d.keys() 

2483 

2484 def values(self) -> Any: 

2485 return self.d.values() 

2486 #@+node:ekr.20190903181030.1: *4* td.get_setting & get_string_setting 

2487 def get_setting(self, key: str) -> Any: 

2488 key = key.replace('-', '').replace('_', '') 

2489 gs = self.get(key) 

2490 val = gs and gs.val 

2491 return val 

2492 

2493 def get_string_setting(self, key: str) -> Optional[str]: 

2494 val = self.get_setting(key) 

2495 return val if val and isinstance(val, str) else None 

2496 #@+node:ekr.20190904103552.1: *4* td.name & setName 

2497 def name(self) -> str: 

2498 return self._name 

2499 

2500 def setName(self, name: str) -> None: 

2501 self._name = name 

2502 #@+node:ekr.20120205022040.17807: *4* td.update 

2503 def update(self, d: Dict[Any, Any]) -> None: 

2504 """Update self.d from a the appropriate dict.""" 

2505 if isinstance(d, TypedDict): 

2506 self.d.update(d.d) 

2507 else: 

2508 self.d.update(d) 

2509 #@-others 

2510#@+node:ville.20090827174345.9963: *3* class g.UiTypeException & g.assertui 

2511class UiTypeException(Exception): 

2512 pass 

2513 

2514def assertUi(uitype: Any) -> None: 

2515 if not g.app.gui.guiName() == uitype: 

2516 raise UiTypeException 

2517#@+node:ekr.20200219071828.1: *3* class TestLeoGlobals (leoGlobals.py) 

2518class TestLeoGlobals(unittest.TestCase): 

2519 """Tests for leoGlobals.py.""" 

2520 #@+others 

2521 #@+node:ekr.20200219071958.1: *4* test_comment_delims_from_extension 

2522 def test_comment_delims_from_extension(self) -> None: 

2523 

2524 # pylint: disable=import-self 

2525 from leo.core import leoGlobals as leo_g 

2526 from leo.core import leoApp 

2527 leo_g.app = leoApp.LeoApp() 

2528 assert leo_g.comment_delims_from_extension(".py") == ('#', '', '') 

2529 assert leo_g.comment_delims_from_extension(".c") == ('//', '/*', '*/') 

2530 assert leo_g.comment_delims_from_extension(".html") == ('', '<!--', '-->') 

2531 #@+node:ekr.20200219072957.1: *4* test_is_sentinel 

2532 def test_is_sentinel(self) -> None: 

2533 

2534 # pylint: disable=import-self 

2535 from leo.core import leoGlobals as leo_g 

2536 # Python. 

2537 py_delims = leo_g.comment_delims_from_extension('.py') 

2538 assert leo_g.is_sentinel("#@+node", py_delims) 

2539 assert not leo_g.is_sentinel("#comment", py_delims) 

2540 # C. 

2541 c_delims = leo_g.comment_delims_from_extension('.c') 

2542 assert leo_g.is_sentinel("//@+node", c_delims) 

2543 assert not g.is_sentinel("//comment", c_delims) 

2544 # Html. 

2545 html_delims = leo_g.comment_delims_from_extension('.html') 

2546 assert leo_g.is_sentinel("<!--@+node-->", html_delims) 

2547 assert not leo_g.is_sentinel("<!--comment-->", html_delims) 

2548 #@-others 

2549#@+node:ekr.20140904112935.18526: *3* g.isTextWrapper & isTextWidget 

2550def isTextWidget(w: Any) -> bool: 

2551 return g.app.gui.isTextWidget(w) 

2552 

2553def isTextWrapper(w: Any) -> bool: 

2554 return g.app.gui.isTextWrapper(w) 

2555#@+node:ekr.20160518074224.1: *3* class g.LinterTable 

2556class LinterTable(): 

2557 """A class to encapsulate lists of leo modules under test.""" 

2558 

2559 def __init__(self) -> None: 

2560 """Ctor for LinterTable class.""" 

2561 # Define self. relative to leo.core.leoGlobals 

2562 self.loadDir = g.os_path_finalize_join(g.__file__, '..', '..') 

2563 #@+others 

2564 #@+node:ekr.20160518074545.2: *4* commands 

2565 def commands(self) -> List: 

2566 """Return list of all command modules in leo/commands.""" 

2567 pattern = g.os_path_finalize_join(self.loadDir, 'commands', '*.py') 

2568 return self.get_files(pattern) 

2569 #@+node:ekr.20160518074545.3: *4* core 

2570 def core(self) -> List: 

2571 """Return list of all of Leo's core files.""" 

2572 pattern = g.os_path_finalize_join(self.loadDir, 'core', 'leo*.py') 

2573 aList = self.get_files(pattern) 

2574 for fn in ['runLeo.py',]: 

2575 aList.append(g.os_path_finalize_join(self.loadDir, 'core', fn)) 

2576 return sorted(aList) 

2577 #@+node:ekr.20160518074545.4: *4* external 

2578 def external(self) -> List: 

2579 """Return list of files in leo/external""" 

2580 pattern = g.os_path_finalize_join(self.loadDir, 'external', 'leo*.py') 

2581 aList = self.get_files(pattern) 

2582 remove = [ 

2583 'leoSAGlobals.py', 

2584 'leoftsindex.py', 

2585 ] 

2586 remove = [g.os_path_finalize_join(self.loadDir, 'external', fn) for fn in remove] 

2587 return sorted([z for z in aList if z not in remove]) 

2588 #@+node:ekr.20160520093506.1: *4* get_files (LinterTable) 

2589 def get_files(self, pattern: str) -> List: 

2590 """Return the list of absolute file names matching the pattern.""" 

2591 aList = sorted([ 

2592 fn for fn in g.glob_glob(pattern) 

2593 if g.os_path_isfile(fn) and g.shortFileName(fn) != '__init__.py']) 

2594 return aList 

2595 #@+node:ekr.20160518074545.9: *4* get_files_for_scope 

2596 def get_files_for_scope(self, scope: str, fn: str) -> List: 

2597 """Return a list of absolute filenames for external linters.""" 

2598 d = { 

2599 'all': [self.core, self.commands, self.external, self.plugins], 

2600 'commands': [self.commands], 

2601 'core': [self.core, self.commands, self.external, self.gui_plugins], 

2602 'external': [self.external], 

2603 'file': [fn], 

2604 'gui': [self.gui_plugins], 

2605 'modes': [self.modes], 

2606 'plugins': [self.plugins], 

2607 'tests': [self.tests], 

2608 } 

2609 suppress_list = ['freewin.py',] 

2610 functions = d.get(scope) 

2611 paths = [] 

2612 if functions: 

2613 for func in functions: 

2614 files = [func] if isinstance(func, str) else func() 

2615 # Bug fix: 2016/10/15 

2616 for fn in files: 

2617 fn = g.os_path_abspath(fn) 

2618 if g.shortFileName(fn) in suppress_list: 

2619 print(f"\npylint-leo: skip {fn}") 

2620 continue 

2621 if g.os_path_exists(fn): 

2622 if g.os_path_isfile(fn): 

2623 paths.append(fn) 

2624 else: 

2625 print(f"does not exist: {fn}") 

2626 paths = sorted(set(paths)) 

2627 return paths 

2628 print('LinterTable.get_table: bad scope', scope) 

2629 return [] 

2630 #@+node:ekr.20160518074545.5: *4* gui_plugins 

2631 def gui_plugins(self) -> List: 

2632 """Return list of all of Leo's gui-related files.""" 

2633 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', 'qt_*.py') 

2634 aList = self.get_files(pattern) 

2635 # These are not included, because they don't start with 'qt_': 

2636 add = ['free_layout.py', 'nested_splitter.py',] 

2637 remove = [ 

2638 'qt_main.py', # auto-generated file. 

2639 ] 

2640 for fn in add: 

2641 aList.append(g.os_path_finalize_join(self.loadDir, 'plugins', fn)) 

2642 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove] 

2643 return sorted(set([z for z in aList if z not in remove])) 

2644 #@+node:ekr.20160518074545.6: *4* modes 

2645 def modes(self) -> List: 

2646 """Return list of all files in leo/modes""" 

2647 pattern = g.os_path_finalize_join(self.loadDir, 'modes', '*.py') 

2648 return self.get_files(pattern) 

2649 #@+node:ekr.20160518074545.8: *4* plugins (LinterTable) 

2650 def plugins(self) -> List: 

2651 """Return a list of all important plugins.""" 

2652 aList = [] 

2653 for theDir in ('', 'importers', 'writers'): 

2654 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', theDir, '*.py') 

2655 aList.extend(self.get_files(pattern)) 

2656 # Don't use get_files here. 

2657 # for fn in g.glob_glob(pattern): 

2658 # sfn = g.shortFileName(fn) 

2659 # if sfn != '__init__.py': 

2660 # sfn = os.sep.join([theDir, sfn]) if theDir else sfn 

2661 # aList.append(sfn) 

2662 remove = [ 

2663 # 2016/05/20: *do* include gui-related plugins. 

2664 # This allows the -a option not to doubly-include gui-related plugins. 

2665 # 'free_layout.py', # Gui-related. 

2666 # 'nested_splitter.py', # Gui-related. 

2667 'gtkDialogs.py', # Many errors, not important. 

2668 'leofts.py', # Not (yet) in leoPlugins.leo. 

2669 'qtGui.py', # Dummy file 

2670 'qt_main.py', # Created automatically. 

2671 'viewrendered2.py', # To be removed. 

2672 'rst3.py', # Obsolete 

2673 ] 

2674 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove] 

2675 aList = sorted([z for z in aList if z not in remove]) 

2676 return sorted(set(aList)) 

2677 #@+node:ekr.20211115103929.1: *4* tests (LinterTable) 

2678 def tests(self) -> List: 

2679 """Return list of files in leo/unittests""" 

2680 aList = [] 

2681 for theDir in ('', 'commands', 'core', 'plugins'): 

2682 pattern = g.os_path_finalize_join(self.loadDir, 'unittests', theDir, '*.py') 

2683 aList.extend(self.get_files(pattern)) 

2684 remove = [ 

2685 'py3_test_grammar.py', 

2686 ] 

2687 remove = [g.os_path_finalize_join(self.loadDir, 'unittests', fn) for fn in remove] 

2688 return sorted([z for z in aList if z not in remove]) 

2689 #@-others 

2690#@+node:ekr.20140711071454.17649: ** g.Debugging, GC, Stats & Timing 

2691#@+node:ekr.20031218072017.3104: *3* g.Debugging 

2692#@+node:ekr.20180415144534.1: *4* g.assert_is 

2693def assert_is(obj: Any, list_or_class: Any, warn: bool=True) -> bool: 

2694 

2695 if warn: 

2696 ok = isinstance(obj, list_or_class) 

2697 if not ok: 

2698 g.es_print( 

2699 f"can not happen. {obj !r}: " 

2700 f"expected {list_or_class}, " 

2701 f"got: {obj.__class__.__name__}") 

2702 g.es_print(g.callers()) 

2703 return ok 

2704 ok = isinstance(obj, list_or_class) 

2705 assert ok, (obj, obj.__class__.__name__, g.callers()) 

2706 return ok 

2707#@+node:ekr.20180420081530.1: *4* g._assert 

2708def _assert(condition: Any, show_callers: bool=True) -> bool: 

2709 """A safer alternative to a bare assert.""" 

2710 if g.unitTesting: 

2711 assert condition 

2712 return True 

2713 ok = bool(condition) 

2714 if ok: 

2715 return True 

2716 g.es_print('\n===== g._assert failed =====\n') 

2717 if show_callers: 

2718 g.es_print(g.callers()) 

2719 return False 

2720#@+node:ekr.20051023083258: *4* g.callers & g.caller & _callerName 

2721def callers(n: int=4, count: int=0, excludeCaller: bool=True, verbose: bool=False) -> str: 

2722 """ 

2723 Return a string containing a comma-separated list of the callers 

2724 of the function that called g.callerList. 

2725 

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

2727 

2728 If the `verbose` keyword is True, return a list separated by newlines. 

2729 """ 

2730 # Be careful to call g._callerName with smaller values of i first: 

2731 # sys._getframe throws ValueError if there are less than i entries. 

2732 result = [] 

2733 i = 3 if excludeCaller else 2 

2734 while 1: 

2735 s = _callerName(n=i, verbose=verbose) 

2736 if s: 

2737 result.append(s) 

2738 if not s or len(result) >= n: 

2739 break 

2740 i += 1 

2741 result.reverse() 

2742 if count > 0: 

2743 result = result[:count] 

2744 if verbose: 

2745 return ''.join([f"\n {z}" for z in result]) 

2746 return ','.join(result) 

2747#@+node:ekr.20031218072017.3107: *5* g._callerName 

2748def _callerName(n: int, verbose: bool=False) -> str: 

2749 try: 

2750 # get the function name from the call stack. 

2751 f1 = sys._getframe(n) # The stack frame, n levels up. 

2752 code1 = f1.f_code # The code object 

2753 sfn = shortFilename(code1.co_filename) # The file name. 

2754 locals_ = f1.f_locals # The local namespace. 

2755 name = code1.co_name 

2756 line = code1.co_firstlineno 

2757 if verbose: 

2758 obj = locals_.get('self') 

2759 full_name = f"{obj.__class__.__name__}.{name}" if obj else name 

2760 return f"line {line:4} {sfn:>30} {full_name}" 

2761 return name 

2762 except ValueError: 

2763 return '' 

2764 # The stack is not deep enough OR 

2765 # sys._getframe does not exist on this platform. 

2766 except Exception: 

2767 es_exception() 

2768 return '' # "<no caller name>" 

2769#@+node:ekr.20180328170441.1: *5* g.caller 

2770def caller(i: int=1) -> str: 

2771 """Return the caller name i levels up the stack.""" 

2772 return g.callers(i + 1).split(',')[0] 

2773#@+node:ekr.20031218072017.3109: *4* g.dump 

2774def dump(s: str) -> str: 

2775 out = "" 

2776 for i in s: 

2777 out += str(ord(i)) + "," 

2778 return out 

2779 

2780def oldDump(s: str) -> str: 

2781 out = "" 

2782 for i in s: 

2783 if i == '\n': 

2784 out += "[" 

2785 out += "n" 

2786 out += "]" 

2787 if i == '\t': 

2788 out += "[" 

2789 out += "t" 

2790 out += "]" 

2791 elif i == ' ': 

2792 out += "[" 

2793 out += " " 

2794 out += "]" 

2795 else: 

2796 out += i 

2797 return out 

2798#@+node:ekr.20210904114446.1: *4* g.dump_tree & g.tree_to_string 

2799def dump_tree(c: Cmdr, dump_body: bool=False, msg: str=None) -> None: 

2800 if msg: 

2801 print(msg.rstrip()) 

2802 else: 

2803 print('') 

2804 for p in c.all_positions(): 

2805 print(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}") 

2806 if dump_body: 

2807 for z in g.splitLines(p.b): 

2808 print(z.rstrip()) 

2809 

2810def tree_to_string(c: Cmdr, dump_body: bool=False, msg: str=None) -> str: 

2811 result = ['\n'] 

2812 if msg: 

2813 result.append(msg) 

2814 for p in c.all_positions(): 

2815 result.append(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}") 

2816 if dump_body: 

2817 for z in g.splitLines(p.b): 

2818 result.append(z.rstrip()) 

2819 return '\n'.join(result) 

2820#@+node:ekr.20150227102835.8: *4* g.dump_encoded_string 

2821def dump_encoded_string(encoding: str, s: str) -> None: 

2822 """Dump s, assumed to be an encoded string.""" 

2823 # Can't use g.trace here: it calls this function! 

2824 print(f"dump_encoded_string: {g.callers()}") 

2825 print(f"dump_encoded_string: encoding {encoding}\n") 

2826 print(s) 

2827 in_comment = False 

2828 for ch in s: 

2829 if ch == '#': 

2830 in_comment = True 

2831 elif not in_comment: 

2832 print(f"{ord(ch):02x} {repr(ch)}") 

2833 elif ch == '\n': 

2834 in_comment = False 

2835#@+node:ekr.20031218072017.1317: *4* g.file/module/plugin_date 

2836def module_date(mod: Any, format: str=None) -> str: 

2837 theFile = g.os_path_join(app.loadDir, mod.__file__) 

2838 root, ext = g.os_path_splitext(theFile) 

2839 return g.file_date(root + ".py", format=format) 

2840 

2841def plugin_date(plugin_mod: Any, format: str=None) -> str: 

2842 theFile = g.os_path_join(app.loadDir, "..", "plugins", plugin_mod.__file__) 

2843 root, ext = g.os_path_splitext(theFile) 

2844 return g.file_date(root + ".py", format=str) 

2845 

2846def file_date(theFile: Any, format: str=None) -> str: 

2847 if theFile and g.os_path_exists(theFile): 

2848 try: 

2849 n = g.os_path_getmtime(theFile) 

2850 if format is None: 

2851 format = "%m/%d/%y %H:%M:%S" 

2852 return time.strftime(format, time.gmtime(n)) 

2853 except(ImportError, NameError): 

2854 pass # Time module is platform dependent. 

2855 return "" 

2856#@+node:ekr.20031218072017.3127: *4* g.get_line & get_line__after 

2857# Very useful for tracing. 

2858 

2859def get_line(s: str, i: int) -> str: 

2860 nl = "" 

2861 if g.is_nl(s, i): 

2862 i = g.skip_nl(s, i) 

2863 nl = "[nl]" 

2864 j = g.find_line_start(s, i) 

2865 k = g.skip_to_end_of_line(s, i) 

2866 return nl + s[j:k] 

2867 

2868# Important: getLine is a completely different function. 

2869# getLine = get_line 

2870 

2871def get_line_after(s: str, i: int) -> str: 

2872 nl = "" 

2873 if g.is_nl(s, i): 

2874 i = g.skip_nl(s, i) 

2875 nl = "[nl]" 

2876 k = g.skip_to_end_of_line(s, i) 

2877 return nl + s[i:k] 

2878 

2879getLineAfter = get_line_after 

2880#@+node:ekr.20080729142651.1: *4* g.getIvarsDict and checkUnchangedIvars 

2881def getIvarsDict(obj: Any) -> Dict[str, Any]: 

2882 """Return a dictionary of ivars:values for non-methods of obj.""" 

2883 d: Dict[str, Any] = dict( 

2884 [[key, getattr(obj, key)] for key in dir(obj) # type:ignore 

2885 if not isinstance(getattr(obj, key), types.MethodType)]) 

2886 return d 

2887 

2888def checkUnchangedIvars( 

2889 obj: Any, 

2890 d: Dict[str, Any], 

2891 exceptions: Sequence[str]=None, 

2892) -> bool: 

2893 if not exceptions: 

2894 exceptions = [] 

2895 ok = True 

2896 for key in d: 

2897 if key not in exceptions: 

2898 if getattr(obj, key) != d.get(key): 

2899 g.trace( 

2900 f"changed ivar: {key} " 

2901 f"old: {repr(d.get(key))} " 

2902 f"new: {repr(getattr(obj, key))}") 

2903 ok = False 

2904 return ok 

2905#@+node:ekr.20031218072017.3128: *4* g.pause 

2906def pause(s: str) -> None: 

2907 g.pr(s) 

2908 i = 0 

2909 while i < 1000 * 1000: 

2910 i += 1 

2911#@+node:ekr.20041105091148: *4* g.pdb 

2912def pdb(message: str='') -> None: 

2913 """Fall into pdb.""" 

2914 import pdb # Required: we have just defined pdb as a function! 

2915 if app and not app.useIpython: 

2916 try: 

2917 from leo.core.leoQt import QtCore 

2918 QtCore.pyqtRemoveInputHook() 

2919 except Exception: 

2920 pass 

2921 if message: 

2922 print(message) 

2923 # pylint: disable=forgotten-debug-statement 

2924 pdb.set_trace() 

2925#@+node:ekr.20041224080039: *4* g.dictToString 

2926def dictToString(d: Dict[str, str], indent: str='', tag: str=None) -> str: 

2927 """Pretty print a Python dict to a string.""" 

2928 # pylint: disable=unnecessary-lambda 

2929 if not d: 

2930 return '{}' 

2931 result = ['{\n'] 

2932 indent2 = indent + ' ' * 4 

2933 n = 2 + len(indent) + max([len(repr(z)) for z in d.keys()]) 

2934 for i, key in enumerate(sorted(d, key=lambda z: repr(z))): 

2935 pad = ' ' * max(0, (n - len(repr(key)))) 

2936 result.append(f"{pad}{key}:") 

2937 result.append(objToString(d.get(key), indent=indent2)) 

2938 if i + 1 < len(d.keys()): 

2939 result.append(',') 

2940 result.append('\n') 

2941 result.append(indent + '}') 

2942 s = ''.join(result) 

2943 return f"{tag}...\n{s}\n" if tag else s 

2944#@+node:ekr.20041126060136: *4* g.listToString 

2945def listToString(obj: Any, indent: str='', tag: str=None) -> str: 

2946 """Pretty print a Python list to a string.""" 

2947 if not obj: 

2948 return '[]' 

2949 result = ['['] 

2950 indent2 = indent + ' ' * 4 

2951 # I prefer not to compress lists. 

2952 for i, obj2 in enumerate(obj): 

2953 result.append('\n' + indent2) 

2954 result.append(objToString(obj2, indent=indent2)) 

2955 if i + 1 < len(obj) > 1: 

2956 result.append(',') 

2957 else: 

2958 result.append('\n' + indent) 

2959 result.append(']') 

2960 s = ''.join(result) 

2961 return f"{tag}...\n{s}\n" if tag else s 

2962#@+node:ekr.20050819064157: *4* g.objToSTring & g.toString 

2963def objToString(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> str: 

2964 """Pretty print any Python object to a string.""" 

2965 # pylint: disable=undefined-loop-variable 

2966 # Looks like a a pylint bug. 

2967 # 

2968 # Compute s. 

2969 if isinstance(obj, dict): 

2970 s = dictToString(obj, indent=indent) 

2971 elif isinstance(obj, list): 

2972 s = listToString(obj, indent=indent) 

2973 elif isinstance(obj, tuple): 

2974 s = tupleToString(obj, indent=indent) 

2975 elif isinstance(obj, str): 

2976 # Print multi-line strings as lists. 

2977 s = obj 

2978 lines = g.splitLines(s) 

2979 if len(lines) > 1: 

2980 s = listToString(lines, indent=indent) 

2981 else: 

2982 s = repr(s) 

2983 else: 

2984 s = repr(obj) 

2985 # 

2986 # Compute the return value. 

2987 if printCaller and tag: 

2988 prefix = f"{g.caller()}: {tag}" 

2989 elif printCaller or tag: 

2990 prefix = g.caller() if printCaller else tag 

2991 else: 

2992 prefix = '' 

2993 if prefix: 

2994 sep = '\n' if '\n' in s else ' ' 

2995 return f"{prefix}:{sep}{s}" 

2996 return s 

2997 

2998toString = objToString 

2999#@+node:ekr.20140401054342.16844: *4* g.run_pylint 

3000def run_pylint( 

3001 fn: str, # Path to file under test. 

3002 rc: str, # Path to settings file. 

3003 dots: bool=True, # Show level dots in Sherlock traces. 

3004 patterns: List[str]=None, # List of Sherlock trace patterns. 

3005 sherlock: bool=False, # Enable Sherlock tracing. 

3006 show_return: bool=True, # Show returns in Sherlock traces. 

3007 stats_patterns: bool=None, # Patterns for Sherlock statistics. 

3008 verbose: bool=True, # Show filenames in Sherlock traces. 

3009) -> None: 

3010 """ 

3011 Run pylint with the given args, with Sherlock tracing if requested. 

3012 

3013 **Do not assume g.app exists.** 

3014 

3015 run() in pylint-leo.py and PylintCommand.run_pylint *optionally* call this function. 

3016 """ 

3017 try: 

3018 from pylint import lint #type:ignore 

3019 except ImportError: 

3020 g.trace('can not import pylint') 

3021 return 

3022 if not g.os_path_exists(fn): 

3023 g.trace('does not exist:', fn) 

3024 return 

3025 if not g.os_path_exists(rc): 

3026 g.trace('does not exist', rc) 

3027 return 

3028 args = [f"--rcfile={rc}"] 

3029 # Prints error number. 

3030 # args.append('--msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}') 

3031 args.append(fn) 

3032 if sherlock: 

3033 sherlock = g.SherlockTracer( 

3034 dots=dots, 

3035 show_return=show_return, 

3036 verbose=True, # verbose: show filenames. 

3037 patterns=patterns or [], 

3038 ) 

3039 try: 

3040 sherlock.run() 

3041 lint.Run(args) 

3042 finally: 

3043 sherlock.stop() 

3044 sherlock.print_stats(patterns=stats_patterns or []) 

3045 else: 

3046 # print('run_pylint: %s' % g.shortFileName(fn)) 

3047 try: 

3048 lint.Run(args) # does sys.exit 

3049 finally: 

3050 # Printing does not work well here. 

3051 # When not waiting, printing from severl process can be interspersed. 

3052 pass 

3053#@+node:ekr.20120912153732.10597: *4* g.wait 

3054def sleep(n: float) -> None: 

3055 """Wait about n milliseconds.""" 

3056 from time import sleep # type:ignore 

3057 sleep(n) # type:ignore 

3058#@+node:ekr.20171023140544.1: *4* g.printObj & aliases 

3059def printObj(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> None: 

3060 """Pretty print any Python object using g.pr.""" 

3061 g.pr(objToString(obj, indent=indent, printCaller=printCaller, tag=tag)) 

3062 

3063printDict = printObj 

3064printList = printObj 

3065printTuple = printObj 

3066#@+node:ekr.20171023110057.1: *4* g.tupleToString 

3067def tupleToString(obj: Any, indent: str='', tag: str=None) -> str: 

3068 """Pretty print a Python tuple to a string.""" 

3069 if not obj: 

3070 return '(),' 

3071 result = ['('] 

3072 indent2 = indent + ' ' * 4 

3073 for i, obj2 in enumerate(obj): 

3074 if len(obj) > 1: 

3075 result.append('\n' + indent2) 

3076 result.append(objToString(obj2, indent=indent2)) 

3077 if len(obj) == 1 or i + 1 < len(obj): 

3078 result.append(',') 

3079 elif len(obj) > 1: 

3080 result.append('\n' + indent) 

3081 result.append(')') 

3082 s = ''.join(result) 

3083 return f"{tag}...\n{s}\n" if tag else s 

3084#@+node:ekr.20031218072017.1588: *3* g.Garbage Collection 

3085#@+node:ekr.20031218072017.1589: *4* g.clearAllIvars 

3086def clearAllIvars(o: Any) -> None: 

3087 """Clear all ivars of o, a member of some class.""" 

3088 if o: 

3089 o.__dict__.clear() 

3090#@+node:ekr.20060127162818: *4* g.enable_gc_debug 

3091def enable_gc_debug() -> None: 

3092 

3093 gc.set_debug( 

3094 gc.DEBUG_STATS | # prints statistics. 

3095 gc.DEBUG_LEAK | # Same as all below. 

3096 gc.DEBUG_COLLECTABLE | 

3097 gc.DEBUG_UNCOLLECTABLE | 

3098 # gc.DEBUG_INSTANCES | 

3099 # gc.DEBUG_OBJECTS | 

3100 gc.DEBUG_SAVEALL) 

3101#@+node:ekr.20031218072017.1592: *4* g.printGc 

3102# Formerly called from unit tests. 

3103 

3104def printGc() -> None: 

3105 """Called from trace_gc_plugin.""" 

3106 g.printGcSummary() 

3107 g.printGcObjects() 

3108 g.printGcRefs() 

3109#@+node:ekr.20060127164729.1: *4* g.printGcObjects 

3110lastObjectCount = 0 

3111 

3112def printGcObjects() -> int: 

3113 """Print a summary of GC statistics.""" 

3114 global lastObjectCount 

3115 n = len(gc.garbage) 

3116 n2 = len(gc.get_objects()) 

3117 delta = n2 - lastObjectCount 

3118 print('-' * 30) 

3119 print(f"garbage: {n}") 

3120 print(f"{delta:6d} = {n2:7d} totals") 

3121 # print number of each type of object. 

3122 d: Dict[str, int] = {} 

3123 count = 0 

3124 for obj in gc.get_objects(): 

3125 key = str(type(obj)) 

3126 n = d.get(key, 0) 

3127 d[key] = n + 1 

3128 count += 1 

3129 print(f"{count:7} objects...") 

3130 # Invert the dict. 

3131 d2: Dict[int, str] = {v: k for k, v in d.items()} 

3132 for key in reversed(sorted(d2.keys())): # type:ignore 

3133 val = d2.get(key) # type:ignore 

3134 print(f"{key:7} {val}") 

3135 lastObjectCount = count 

3136 return delta 

3137#@+node:ekr.20031218072017.1593: *4* g.printGcRefs 

3138def printGcRefs() -> None: 

3139 

3140 refs = gc.get_referrers(app.windowList[0]) 

3141 print(f"{len(refs):d} referers") 

3142#@+node:ekr.20060205043324.1: *4* g.printGcSummary 

3143def printGcSummary() -> None: 

3144 

3145 g.enable_gc_debug() 

3146 try: 

3147 n = len(gc.garbage) 

3148 n2 = len(gc.get_objects()) 

3149 s = f"printGCSummary: garbage: {n}, objects: {n2}" 

3150 print(s) 

3151 except Exception: 

3152 traceback.print_exc() 

3153#@+node:ekr.20180528151850.1: *3* g.printTimes 

3154def printTimes(times: List) -> None: 

3155 """ 

3156 Print the differences in the times array. 

3157 

3158 times: an array of times (calls to time.process_time()). 

3159 """ 

3160 for n, junk in enumerate(times[:-1]): 

3161 t = times[n + 1] - times[n] 

3162 if t > 0.1: 

3163 g.trace(f"*** {n} {t:5.4f} sec.") 

3164#@+node:ekr.20031218072017.3133: *3* g.Statistics 

3165#@+node:ekr.20031218072017.3134: *4* g.clearStats 

3166def clearStats() -> None: 

3167 

3168 g.app.statsDict = {} 

3169#@+node:ekr.20031218072017.3135: *4* g.printStats 

3170@command('show-stats') 

3171def printStats(event: Any=None, name: str=None) -> None: 

3172 """ 

3173 Print all gathered statistics. 

3174 

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

3176 

3177 if not g.app.statsLockout: 

3178 g.app.statsLockout = True 

3179 try: 

3180 d = g.app.statsDict 

3181 key = 'g.isUnicode:' + g.callers() 

3182 d [key] = d.get(key, 0) + 1 

3183 finally: 

3184 g.app.statsLockout = False 

3185 """ 

3186 if name: 

3187 if not isinstance(name, str): 

3188 name = repr(name) 

3189 else: 

3190 # Get caller name 2 levels back. 

3191 name = g._callerName(n=2) 

3192 # Print the stats, organized by number of calls. 

3193 d = g.app.statsDict 

3194 print('g.app.statsDict...') 

3195 for key in reversed(sorted(d)): 

3196 print(f"{key:7} {d.get(key)}") 

3197#@+node:ekr.20031218072017.3136: *4* g.stat 

3198def stat(name: str=None) -> None: 

3199 """Increments the statistic for name in g.app.statsDict 

3200 The caller's name is used by default. 

3201 """ 

3202 d = g.app.statsDict 

3203 if name: 

3204 if not isinstance(name, str): 

3205 name = repr(name) 

3206 else: 

3207 name = g._callerName(n=2) # Get caller name 2 levels back. 

3208 d[name] = 1 + d.get(name, 0) 

3209#@+node:ekr.20031218072017.3137: *3* g.Timing 

3210def getTime() -> float: 

3211 return time.time() 

3212 

3213def esDiffTime(message: str, start: float) -> float: 

3214 delta = time.time() - start 

3215 g.es('', f"{message} {delta:5.2f} sec.") 

3216 return time.time() 

3217 

3218def printDiffTime(message: str, start: float) -> float: 

3219 delta = time.time() - start 

3220 g.pr(f"{message} {delta:5.2f} sec.") 

3221 return time.time() 

3222 

3223def timeSince(start: float) -> str: 

3224 return f"{time.time()-start:5.2f} sec." 

3225#@+node:ekr.20031218072017.1380: ** g.Directives 

3226# Weird pylint bug, activated by TestLeoGlobals class. 

3227# Disabling this will be safe, because pyflakes will still warn about true redefinitions 

3228# pylint: disable=function-redefined 

3229#@+node:EKR.20040504150046.4: *3* g.comment_delims_from_extension 

3230def comment_delims_from_extension(filename: str) -> Tuple[str, str, str]: 

3231 """ 

3232 Return the comment delims corresponding to the filename's extension. 

3233 """ 

3234 if filename.startswith('.'): 

3235 root, ext = None, filename 

3236 else: 

3237 root, ext = os.path.splitext(filename) 

3238 if ext == '.tmp': 

3239 root, ext = os.path.splitext(root) 

3240 language = g.app.extension_dict.get(ext[1:]) 

3241 if ext: 

3242 return g.set_delims_from_language(language) 

3243 g.trace( 

3244 f"unknown extension: {ext!r}, " 

3245 f"filename: {filename!r}, " 

3246 f"root: {root!r}") 

3247 return '', '', '' 

3248#@+node:ekr.20170201150505.1: *3* g.findAllValidLanguageDirectives 

3249def findAllValidLanguageDirectives(s: str) -> List: 

3250 """Return list of all valid @language directives in p.b""" 

3251 if not s.strip(): 

3252 return [] 

3253 languages = set() 

3254 for m in g.g_language_pat.finditer(s): 

3255 language = m.group(1) 

3256 if g.isValidLanguage(language): 

3257 languages.add(language) 

3258 return list(sorted(languages)) 

3259#@+node:ekr.20090214075058.8: *3* g.findAtTabWidthDirectives (must be fast) 

3260def findTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[str]: 

3261 """Return the language in effect at position p.""" 

3262 if c is None: 

3263 return None # c may be None for testing. 

3264 w = None 

3265 # 2009/10/02: no need for copy arg to iter 

3266 for p in p.self_and_parents(copy=False): 

3267 if w: 

3268 break 

3269 for s in p.h, p.b: 

3270 if w: 

3271 break 

3272 anIter = g_tabwidth_pat.finditer(s) 

3273 for m in anIter: 

3274 word = m.group(0) 

3275 i = m.start(0) 

3276 j = g.skip_ws(s, i + len(word)) 

3277 junk, w = g.skip_long(s, j) 

3278 if w == 0: 

3279 w = None 

3280 return w 

3281#@+node:ekr.20170127142001.5: *3* g.findFirstAtLanguageDirective 

3282def findFirstValidAtLanguageDirective(s: str) -> Optional[str]: 

3283 """Return the first *valid* @language directive ins.""" 

3284 if not s.strip(): 

3285 return None 

3286 for m in g.g_language_pat.finditer(s): 

3287 language = m.group(1) 

3288 if g.isValidLanguage(language): 

3289 return language 

3290 return None 

3291#@+node:ekr.20090214075058.6: *3* g.findLanguageDirectives (must be fast) 

3292def findLanguageDirectives(c: Cmdr, p: Pos) -> Optional[str]: 

3293 """Return the language in effect at position p.""" 

3294 if c is None or p is None: 

3295 return None # c may be None for testing. 

3296 

3297 v0 = p.v 

3298 

3299 def find_language(p_or_v: Any) -> Optional[str]: 

3300 for s in p_or_v.h, p_or_v.b: 

3301 for m in g_language_pat.finditer(s): 

3302 language = m.group(1) 

3303 if g.isValidLanguage(language): 

3304 return language 

3305 return None 

3306 

3307 # First, search up the tree. 

3308 for p in p.self_and_parents(copy=False): 

3309 language = find_language(p) 

3310 if language: 

3311 return language 

3312 # #1625: Second, expand the search for cloned nodes. 

3313 seen = [] # vnodes that have already been searched. 

3314 parents = v0.parents[:] # vnodes whose ancestors are to be searched. 

3315 while parents: 

3316 parent_v = parents.pop() 

3317 if parent_v in seen: 

3318 continue 

3319 seen.append(parent_v) 

3320 language = find_language(parent_v) 

3321 if language: 

3322 return language 

3323 for grand_parent_v in parent_v.parents: 

3324 if grand_parent_v not in seen: 

3325 parents.append(grand_parent_v) 

3326 # Finally, fall back to the defaults. 

3327 return c.target_language.lower() if c.target_language else 'python' 

3328#@+node:ekr.20031218072017.1385: *3* g.findReference 

3329# Called from the syntax coloring method that colorizes section references. 

3330# Also called from write at.putRefAt. 

3331 

3332def findReference(name: str, root: Pos) -> Optional[Pos]: 

3333 """Return the position containing the section definition for name.""" 

3334 for p in root.subtree(copy=False): 

3335 assert p != root 

3336 if p.matchHeadline(name) and not p.isAtIgnoreNode(): 

3337 return p.copy() 

3338 return None 

3339#@+node:ekr.20090214075058.9: *3* g.get_directives_dict (must be fast) 

3340# The caller passes [root_node] or None as the second arg. 

3341# This allows us to distinguish between None and [None]. 

3342 

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

3344 """ 

3345 Scan p for Leo directives found in globalDirectiveList. 

3346 

3347 Returns a dict containing the stripped remainder of the line 

3348 following the first occurrence of each recognized directive 

3349 """ 

3350 if root: 

3351 root_node = root[0] 

3352 d = {} 

3353 # 

3354 # #1688: legacy: Always compute the pattern. 

3355 # g.directives_pat is updated whenever loading a plugin. 

3356 # 

3357 # The headline has higher precedence because it is more visible. 

3358 for kind, s in (('head', p.h), ('body', p.b)): 

3359 anIter = g.directives_pat.finditer(s) 

3360 for m in anIter: 

3361 word = m.group(1).strip() 

3362 i = m.start(1) 

3363 if word in d: 

3364 continue 

3365 j = i + len(word) 

3366 if j < len(s) and s[j] not in ' \t\n': 

3367 continue 

3368 # Not a valid directive: just ignore it. 

3369 # A unit test tests that @path:any is invalid. 

3370 k = g.skip_line(s, j) 

3371 val = s[j:k].strip() 

3372 d[word] = val 

3373 if root: 

3374 anIter = g_noweb_root.finditer(p.b) 

3375 for m in anIter: 

3376 if root_node: 

3377 d["root"] = 0 # value not immportant 

3378 else: 

3379 g.es(f'{g.angleBrackets("*")} may only occur in a topmost node (i.e., without a parent)') 

3380 break 

3381 return d 

3382#@+node:ekr.20080827175609.1: *3* g.get_directives_dict_list (must be fast) 

3383def get_directives_dict_list(p: Pos) -> List[Dict]: 

3384 """Scans p and all its ancestors for directives. 

3385 

3386 Returns a list of dicts containing pointers to 

3387 the start of each directive""" 

3388 result = [] 

3389 p1 = p.copy() 

3390 for p in p1.self_and_parents(copy=False): 

3391 # No copy necessary: g.get_directives_dict does not change p. 

3392 root = None if p.hasParent() else [p] 

3393 result.append(g.get_directives_dict(p, root=root)) 

3394 return result 

3395#@+node:ekr.20111010082822.15545: *3* g.getLanguageFromAncestorAtFileNode 

3396def getLanguageFromAncestorAtFileNode(p: Pos) -> Optional[str]: 

3397 """ 

3398 Return the language in effect at node p. 

3399  

3400 1. Use an unambiguous @language directive in p itself. 

3401 2. Search p's "extended parents" for an @<file> node. 

3402 3. Search p's "extended parents" for an unambiguous @language directive. 

3403 """ 

3404 v0 = p.v 

3405 seen: Set[VNode] 

3406 

3407 # The same generator as in v.setAllAncestorAtFileNodesDirty. 

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

3409 # Modified by EKR. 

3410 

3411 def v_and_parents(v: "VNode") -> Generator: 

3412 if v in seen: 

3413 return 

3414 seen.add(v) 

3415 yield v 

3416 for parent_v in v.parents: 

3417 if parent_v not in seen: 

3418 yield from v_and_parents(parent_v) 

3419 

3420 def find_language(v: "VNode", phase: int) -> Optional[str]: 

3421 """ 

3422 A helper for all searches. 

3423 Phase one searches only @<file> nodes. 

3424 """ 

3425 if phase == 1 and not v.isAnyAtFileNode(): 

3426 return None 

3427 # #1693: Scan v.b for an *unambiguous* @language directive. 

3428 languages = g.findAllValidLanguageDirectives(v.b) 

3429 if len(languages) == 1: # An unambiguous language 

3430 return languages[0] 

3431 if v.isAnyAtFileNode(): 

3432 # Use the file's extension. 

3433 name = v.anyAtFileNodeName() 

3434 junk, ext = g.os_path_splitext(name) 

3435 ext = ext[1:] # strip the leading period. 

3436 language = g.app.extension_dict.get(ext) 

3437 if g.isValidLanguage(language): 

3438 return language 

3439 return None 

3440 

3441 # First, see if p contains any @language directive. 

3442 language = g.findFirstValidAtLanguageDirective(p.b) 

3443 if language: 

3444 return language 

3445 # 

3446 # Phase 1: search only @<file> nodes: #2308. 

3447 # Phase 2: search all nodes. 

3448 for phase in (1, 2): 

3449 # Search direct parents. 

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

3451 language = find_language(p2.v, phase) 

3452 if language: 

3453 return language 

3454 # Search all extended parents. 

3455 seen = set([v0.context.hiddenRootNode]) 

3456 for v in v_and_parents(v0): 

3457 language = find_language(v, phase) 

3458 if language: 

3459 return language 

3460 return None 

3461#@+node:ekr.20150325075144.1: *3* g.getLanguageFromPosition 

3462def getLanguageAtPosition(c: Cmdr, p: Pos) -> str: 

3463 """ 

3464 Return the language in effect at position p. 

3465 This is always a lowercase language name, never None. 

3466 """ 

3467 aList = g.get_directives_dict_list(p) 

3468 d = g.scanAtCommentAndAtLanguageDirectives(aList) 

3469 language = ( 

3470 d and d.get('language') or 

3471 g.getLanguageFromAncestorAtFileNode(p) or 

3472 c.config.getString('target-language') or 

3473 'python' 

3474 ) 

3475 return language.lower() 

3476#@+node:ekr.20031218072017.1386: *3* g.getOutputNewline 

3477def getOutputNewline(c: Cmdr=None, name: str=None) -> str: 

3478 """Convert the name of a line ending to the line ending itself. 

3479 

3480 Priority: 

3481 - Use name if name given 

3482 - Use c.config.output_newline if c given, 

3483 - Otherwise use g.app.config.output_newline. 

3484 """ 

3485 if name: 

3486 s = name 

3487 elif c: 

3488 s = c.config.output_newline 

3489 else: 

3490 s = app.config.output_newline 

3491 if not s: 

3492 s = '' 

3493 s = s.lower() 

3494 if s in ("nl", "lf"): 

3495 s = '\n' 

3496 elif s == "cr": 

3497 s = '\r' 

3498 elif s == "platform": 

3499 s = os.linesep # 12/2/03: emakital 

3500 elif s == "crlf": 

3501 s = "\r\n" 

3502 else: 

3503 s = '\n' # Default for erroneous values. 

3504 assert isinstance(s, str), repr(s) 

3505 return s 

3506#@+node:ekr.20200521075143.1: *3* g.inAtNosearch 

3507def inAtNosearch(p: Pos) -> bool: 

3508 """Return True if p or p's ancestors contain an @nosearch directive.""" 

3509 if not p: 

3510 return False # #2288. 

3511 for p in p.self_and_parents(): 

3512 if p.is_at_ignore() or re.search(r'(^@|\n@)nosearch\b', p.b): 

3513 return True 

3514 return False 

3515#@+node:ekr.20131230090121.16528: *3* g.isDirective 

3516def isDirective(s: str) -> bool: 

3517 """Return True if s starts with a directive.""" 

3518 m = g_is_directive_pattern.match(s) 

3519 if m: 

3520 s2 = s[m.end(1) :] 

3521 if s2 and s2[0] in ".(": 

3522 return False 

3523 return bool(m.group(1) in g.globalDirectiveList) 

3524 return False 

3525#@+node:ekr.20200810074755.1: *3* g.isValidLanguage 

3526def isValidLanguage(language: str) -> bool: 

3527 """True if language exists in leo/modes.""" 

3528 # 2020/08/12: A hack for c++ 

3529 if language in ('c++', 'cpp'): 

3530 language = 'cplusplus' 

3531 fn = g.os_path_join(g.app.loadDir, '..', 'modes', f"{language}.py") 

3532 return g.os_path_exists(fn) 

3533#@+node:ekr.20080827175609.52: *3* g.scanAtCommentAndLanguageDirectives 

3534def scanAtCommentAndAtLanguageDirectives(aList: List) -> Optional[Dict[str, str]]: 

3535 """ 

3536 Scan aList for @comment and @language directives. 

3537 

3538 @comment should follow @language if both appear in the same node. 

3539 """ 

3540 lang = None 

3541 for d in aList: 

3542 comment = d.get('comment') 

3543 language = d.get('language') 

3544 # Important: assume @comment follows @language. 

3545 if language: 

3546 lang, delim1, delim2, delim3 = g.set_language(language, 0) 

3547 if comment: 

3548 delim1, delim2, delim3 = g.set_delims_from_string(comment) 

3549 if comment or language: 

3550 delims = delim1, delim2, delim3 

3551 d = {'language': lang, 'comment': comment, 'delims': delims} 

3552 return d 

3553 return None 

3554#@+node:ekr.20080827175609.32: *3* g.scanAtEncodingDirectives 

3555def scanAtEncodingDirectives(aList: List) -> Optional[str]: 

3556 """Scan aList for @encoding directives.""" 

3557 for d in aList: 

3558 encoding = d.get('encoding') 

3559 if encoding and g.isValidEncoding(encoding): 

3560 return encoding 

3561 if encoding and not g.unitTesting: 

3562 g.error("invalid @encoding:", encoding) 

3563 return None 

3564#@+node:ekr.20080827175609.53: *3* g.scanAtHeaderDirectives 

3565def scanAtHeaderDirectives(aList: List) -> None: 

3566 """scan aList for @header and @noheader directives.""" 

3567 for d in aList: 

3568 if d.get('header') and d.get('noheader'): 

3569 g.error("conflicting @header and @noheader directives") 

3570#@+node:ekr.20080827175609.33: *3* g.scanAtLineendingDirectives 

3571def scanAtLineendingDirectives(aList: List) -> Optional[str]: 

3572 """Scan aList for @lineending directives.""" 

3573 for d in aList: 

3574 e = d.get('lineending') 

3575 if e in ("cr", "crlf", "lf", "nl", "platform"): 

3576 lineending = g.getOutputNewline(name=e) 

3577 return lineending 

3578 # else: 

3579 # g.error("invalid @lineending directive:",e) 

3580 return None 

3581#@+node:ekr.20080827175609.34: *3* g.scanAtPagewidthDirectives 

3582def scanAtPagewidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[str]: 

3583 """Scan aList for @pagewidth directives.""" 

3584 for d in aList: 

3585 s = d.get('pagewidth') 

3586 if s is not None: 

3587 i, val = g.skip_long(s, 0) 

3588 if val is not None and val > 0: 

3589 return val 

3590 if issue_error_flag and not g.unitTesting: 

3591 g.error("ignoring @pagewidth", s) 

3592 return None 

3593#@+node:ekr.20101022172109.6108: *3* g.scanAtPathDirectives 

3594def scanAtPathDirectives(c: Cmdr, aList: List) -> str: 

3595 path = c.scanAtPathDirectives(aList) 

3596 return path 

3597 

3598def scanAllAtPathDirectives(c: Cmdr, p: Pos) -> str: 

3599 aList = g.get_directives_dict_list(p) 

3600 path = c.scanAtPathDirectives(aList) 

3601 return path 

3602#@+node:ekr.20080827175609.37: *3* g.scanAtTabwidthDirectives 

3603def scanAtTabwidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[int]: 

3604 """Scan aList for @tabwidth directives.""" 

3605 for d in aList: 

3606 s = d.get('tabwidth') 

3607 if s is not None: 

3608 junk, val = g.skip_long(s, 0) 

3609 if val not in (None, 0): 

3610 return val 

3611 if issue_error_flag and not g.unitTesting: 

3612 g.error("ignoring @tabwidth", s) 

3613 return None 

3614 

3615def scanAllAtTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[int]: 

3616 """Scan p and all ancestors looking for @tabwidth directives.""" 

3617 if c and p: 

3618 aList = g.get_directives_dict_list(p) 

3619 val = g.scanAtTabwidthDirectives(aList) 

3620 ret = c.tab_width if val is None else val 

3621 else: 

3622 ret = None 

3623 return ret 

3624#@+node:ekr.20080831084419.4: *3* g.scanAtWrapDirectives 

3625def scanAtWrapDirectives(aList: List, issue_error_flag: bool=False) -> Optional[bool]: 

3626 """Scan aList for @wrap and @nowrap directives.""" 

3627 for d in aList: 

3628 if d.get('wrap') is not None: 

3629 return True 

3630 if d.get('nowrap') is not None: 

3631 return False 

3632 return None 

3633 

3634def scanAllAtWrapDirectives(c: Cmdr, p: Pos) -> Optional[bool]: 

3635 """Scan p and all ancestors looking for @wrap/@nowrap directives.""" 

3636 if c and p: 

3637 default = bool(c and c.config.getBool("body-pane-wraps")) 

3638 aList = g.get_directives_dict_list(p) 

3639 val = g.scanAtWrapDirectives(aList) 

3640 ret = default if val is None else val 

3641 else: 

3642 ret = None 

3643 return ret 

3644#@+node:ekr.20040715155607: *3* g.scanForAtIgnore 

3645def scanForAtIgnore(c: Cmdr, p: Pos) -> bool: 

3646 """Scan position p and its ancestors looking for @ignore directives.""" 

3647 if g.unitTesting: 

3648 return False # For unit tests. 

3649 for p in p.self_and_parents(copy=False): 

3650 d = g.get_directives_dict(p) 

3651 if 'ignore' in d: 

3652 return True 

3653 return False 

3654#@+node:ekr.20040712084911.1: *3* g.scanForAtLanguage 

3655def scanForAtLanguage(c: Cmdr, p: Pos) -> str: 

3656 """Scan position p and p's ancestors looking only for @language and @ignore directives. 

3657 

3658 Returns the language found, or c.target_language.""" 

3659 # Unlike the code in x.scanAllDirectives, this code ignores @comment directives. 

3660 if c and p: 

3661 for p in p.self_and_parents(copy=False): 

3662 d = g.get_directives_dict(p) 

3663 if 'language' in d: 

3664 z = d["language"] 

3665 language, delim1, delim2, delim3 = g.set_language(z, 0) 

3666 return language 

3667 return c.target_language 

3668#@+node:ekr.20041123094807: *3* g.scanForAtSettings 

3669def scanForAtSettings(p: Pos) -> bool: 

3670 """Scan position p and its ancestors looking for @settings nodes.""" 

3671 for p in p.self_and_parents(copy=False): 

3672 h = p.h 

3673 h = g.app.config.canonicalizeSettingName(h) 

3674 if h.startswith("@settings"): 

3675 return True 

3676 return False 

3677#@+node:ekr.20031218072017.1382: *3* g.set_delims_from_language 

3678def set_delims_from_language(language: str) -> Tuple[str, str, str]: 

3679 """Return a tuple (single,start,end) of comment delims.""" 

3680 val = g.app.language_delims_dict.get(language) 

3681 if val: 

3682 delim1, delim2, delim3 = g.set_delims_from_string(val) 

3683 if delim2 and not delim3: 

3684 return '', delim1, delim2 

3685 # 0,1 or 3 params. 

3686 return delim1, delim2, delim3 

3687 return '', '', '' 

3688 # Indicate that no change should be made 

3689#@+node:ekr.20031218072017.1383: *3* g.set_delims_from_string 

3690def set_delims_from_string(s: str) -> Tuple[str, str, str]: 

3691 """ 

3692 Return (delim1, delim2, delim2), the delims following the @comment 

3693 directive. 

3694 

3695 This code can be called from @language logic, in which case s can 

3696 point at @comment 

3697 """ 

3698 # Skip an optional @comment 

3699 tag = "@comment" 

3700 i = 0 

3701 if g.match_word(s, i, tag): 

3702 i += len(tag) 

3703 count = 0 

3704 delims = ['', '', ''] 

3705 while count < 3 and i < len(s): 

3706 i = j = g.skip_ws(s, i) 

3707 while i < len(s) and not g.is_ws(s[i]) and not g.is_nl(s, i): 

3708 i += 1 

3709 if j == i: 

3710 break 

3711 delims[count] = s[j:i] or '' 

3712 count += 1 

3713 # 'rr 09/25/02 

3714 if count == 2: # delims[0] is always the single-line delim. 

3715 delims[2] = delims[1] 

3716 delims[1] = delims[0] 

3717 delims[0] = '' 

3718 for i in range(0, 3): 

3719 if delims[i]: 

3720 if delims[i].startswith("@0x"): 

3721 # Allow delimiter definition as @0x + hexadecimal encoded delimiter 

3722 # to avoid problems with duplicate delimiters on the @comment line. 

3723 # If used, whole delimiter must be encoded. 

3724 if len(delims[i]) == 3: 

3725 g.warning(f"'{delims[i]}' delimiter is invalid") 

3726 return None, None, None 

3727 try: 

3728 delims[i] = binascii.unhexlify(delims[i][3:]) # type:ignore 

3729 delims[i] = g.toUnicode(delims[i]) 

3730 except Exception as e: 

3731 g.warning(f"'{delims[i]}' delimiter is invalid: {e}") 

3732 return None, None, None 

3733 else: 

3734 # 7/8/02: The "REM hack": replace underscores by blanks. 

3735 # 9/25/02: The "perlpod hack": replace double underscores by newlines. 

3736 delims[i] = delims[i].replace("__", '\n').replace('_', ' ') 

3737 return delims[0], delims[1], delims[2] 

3738#@+node:ekr.20031218072017.1384: *3* g.set_language 

3739def set_language(s: str, i: int, issue_errors_flag: bool=False) -> Tuple: 

3740 """Scan the @language directive that appears at s[i:]. 

3741 

3742 The @language may have been stripped away. 

3743 

3744 Returns (language, delim1, delim2, delim3) 

3745 """ 

3746 tag = "@language" 

3747 assert i is not None 

3748 if g.match_word(s, i, tag): 

3749 i += len(tag) 

3750 # Get the argument. 

3751 i = g.skip_ws(s, i) 

3752 j = i 

3753 i = g.skip_c_id(s, i) 

3754 # Allow tcl/tk. 

3755 arg = s[j:i].lower() 

3756 if app.language_delims_dict.get(arg): 

3757 language = arg 

3758 delim1, delim2, delim3 = g.set_delims_from_language(language) 

3759 return language, delim1, delim2, delim3 

3760 if issue_errors_flag: 

3761 g.es("ignoring:", g.get_line(s, i)) 

3762 return None, None, None, None 

3763#@+node:ekr.20071109165315: *3* g.stripPathCruft 

3764def stripPathCruft(path: str) -> str: 

3765 """Strip cruft from a path name.""" 

3766 if not path: 

3767 return path # Retain empty paths for warnings. 

3768 if len(path) > 2 and ( 

3769 (path[0] == '<' and path[-1] == '>') or 

3770 (path[0] == '"' and path[-1] == '"') or 

3771 (path[0] == "'" and path[-1] == "'") 

3772 ): 

3773 path = path[1:-1].strip() 

3774 # We want a *relative* path, not an absolute path. 

3775 return path 

3776#@+node:ekr.20090214075058.10: *3* g.update_directives_pat 

3777def update_directives_pat() -> None: 

3778 """Init/update g.directives_pat""" 

3779 global globalDirectiveList, directives_pat 

3780 # Use a pattern that guarantees word matches. 

3781 aList = [ 

3782 fr"\b{z}\b" for z in globalDirectiveList if z != 'others' 

3783 ] 

3784 pat = "^@(%s)" % "|".join(aList) 

3785 directives_pat = re.compile(pat, re.MULTILINE) 

3786 

3787# #1688: Initialize g.directives_pat 

3788update_directives_pat() 

3789#@+node:ekr.20031218072017.3116: ** g.Files & Directories 

3790#@+node:ekr.20080606074139.2: *3* g.chdir 

3791def chdir(path: str) -> None: 

3792 if not g.os_path_isdir(path): 

3793 path = g.os_path_dirname(path) 

3794 if g.os_path_isdir(path) and g.os_path_exists(path): 

3795 os.chdir(path) 

3796#@+node:ekr.20120222084734.10287: *3* g.compute...Dir 

3797# For compatibility with old code. 

3798 

3799def computeGlobalConfigDir() -> str: 

3800 return g.app.loadManager.computeGlobalConfigDir() 

3801 

3802def computeHomeDir() -> str: 

3803 return g.app.loadManager.computeHomeDir() 

3804 

3805def computeLeoDir() -> str: 

3806 return g.app.loadManager.computeLeoDir() 

3807 

3808def computeLoadDir() -> str: 

3809 return g.app.loadManager.computeLoadDir() 

3810 

3811def computeMachineName() -> str: 

3812 return g.app.loadManager.computeMachineName() 

3813 

3814def computeStandardDirectories() -> str: 

3815 return g.app.loadManager.computeStandardDirectories() 

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

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

3818 

3819 branch, commit = g.gitInfoForFile(fileName) # #1616 

3820 if not fileName: 

3821 return branch + ": untitled" if branch else 'untitled' 

3822 path, fn = g.os_path_split(fileName) 

3823 if path: 

3824 title = fn + " in " + path 

3825 else: 

3826 title = fn 

3827 # Yet another fix for bug 1194209: regularize slashes. 

3828 if os.sep in '/\\': 

3829 title = title.replace('/', os.sep).replace('\\', os.sep) 

3830 if branch: 

3831 title = branch + ": " + title 

3832 return title 

3833#@+node:ekr.20031218072017.3117: *3* g.create_temp_file 

3834def create_temp_file(textMode: bool=False) -> Tuple[Any, str]: 

3835 """ 

3836 Return a tuple (theFile,theFileName) 

3837 

3838 theFile: a file object open for writing. 

3839 theFileName: the name of the temporary file. 

3840 """ 

3841 try: 

3842 # fd is an handle to an open file as would be returned by os.open() 

3843 fd, theFileName = tempfile.mkstemp(text=textMode) 

3844 mode = 'w' if textMode else 'wb' 

3845 theFile = os.fdopen(fd, mode) 

3846 except Exception: 

3847 g.error('unexpected exception in g.create_temp_file') 

3848 g.es_exception() 

3849 theFile, theFileName = None, '' 

3850 return theFile, theFileName 

3851#@+node:ekr.20210307060731.1: *3* g.createHiddenCommander 

3852def createHiddenCommander(fn: str) -> Optional[Cmdr]: 

3853 """Read the file into a hidden commander (Similar to g.openWithFileName).""" 

3854 from leo.core.leoCommands import Commands 

3855 c = Commands(fn, gui=g.app.nullGui) 

3856 theFile = g.app.loadManager.openAnyLeoFile(fn) 

3857 if theFile: 

3858 c.fileCommands.openLeoFile( # type:ignore 

3859 theFile, fn, readAtFileNodesFlag=True, silent=True) 

3860 return c 

3861 return None 

3862#@+node:vitalije.20170714085545.1: *3* g.defaultLeoFileExtension 

3863def defaultLeoFileExtension(c: Cmdr=None) -> str: 

3864 conf = c.config if c else g.app.config 

3865 return conf.getString('default-leo-extension') or '.leo' 

3866#@+node:ekr.20031218072017.3118: *3* g.ensure_extension 

3867def ensure_extension(name: str, ext: str) -> str: 

3868 

3869 theFile, old_ext = g.os_path_splitext(name) 

3870 if not name: 

3871 return name # don't add to an empty name. 

3872 if old_ext in ('.db', '.leo'): 

3873 return name 

3874 if old_ext and old_ext == ext: 

3875 return name 

3876 return name + ext 

3877#@+node:ekr.20150403150655.1: *3* g.fullPath 

3878def fullPath(c: Cmdr, p: Pos, simulate: bool=False) -> str: 

3879 """ 

3880 Return the full path (including fileName) in effect at p. Neither the 

3881 path nor the fileName will be created if it does not exist. 

3882 """ 

3883 # Search p and p's parents. 

3884 for p in p.self_and_parents(copy=False): 

3885 aList = g.get_directives_dict_list(p) 

3886 path = c.scanAtPathDirectives(aList) 

3887 fn = p.h if simulate else p.anyAtFileNodeName() # Use p.h for unit tests. 

3888 if fn: 

3889 # Fix #102: expand path expressions. 

3890 fn = c.expand_path_expression(fn) # #1341. 

3891 fn = os.path.expanduser(fn) # 1900. 

3892 return g.os_path_finalize_join(path, fn) # #1341. 

3893 return '' 

3894#@+node:ekr.20190327192721.1: *3* g.get_files_in_directory 

3895def get_files_in_directory(directory: str, kinds: List=None, recursive: bool=True) -> List[str]: 

3896 """ 

3897 Return a list of all files of the given file extensions in the directory. 

3898 Default kinds: ['*.py']. 

3899 """ 

3900 files: List[str] = [] 

3901 sep = os.path.sep 

3902 if not g.os.path.exists(directory): 

3903 g.es_print('does not exist', directory) 

3904 return files 

3905 try: 

3906 if kinds: 

3907 kinds = [z if z.startswith('*') else '*' + z for z in kinds] 

3908 else: 

3909 kinds = ['*.py'] 

3910 if recursive: 

3911 # Works for all versions of Python. 

3912 for root, dirnames, filenames in os.walk(directory): 

3913 for kind in kinds: 

3914 for filename in fnmatch.filter(filenames, kind): 

3915 files.append(os.path.join(root, filename)) 

3916 else: 

3917 for kind in kinds: 

3918 files.extend(glob.glob(directory + sep + kind)) 

3919 return list(set(sorted(files))) 

3920 except Exception: 

3921 g.es_exception() 

3922 return [] 

3923#@+node:ekr.20031218072017.1264: *3* g.getBaseDirectory 

3924# Handles the conventions applying to the "relative_path_base_directory" configuration option. 

3925 

3926def getBaseDirectory(c: Cmdr) -> str: 

3927 """Convert '!' or '.' to proper directory references.""" 

3928 base = app.config.relative_path_base_directory 

3929 if base and base == "!": 

3930 base = app.loadDir 

3931 elif base and base == ".": 

3932 base = c.openDirectory 

3933 if base and g.os_path_isabs(base): 

3934 # Set c.chdir_to_relative_path as needed. 

3935 if not hasattr(c, 'chdir_to_relative_path'): 

3936 c.chdir_to_relative_path = c.config.getBool('chdir-to-relative-path') 

3937 # Call os.chdir if requested. 

3938 if c.chdir_to_relative_path: 

3939 os.chdir(base) 

3940 return base # base need not exist yet. 

3941 return "" # No relative base given. 

3942#@+node:ekr.20170223093758.1: *3* g.getEncodingAt 

3943def getEncodingAt(p: Pos, s: str=None) -> str: 

3944 """ 

3945 Return the encoding in effect at p and/or for string s. 

3946 

3947 Read logic: s is not None. 

3948 Write logic: s is None. 

3949 """ 

3950 # A BOM overrides everything. 

3951 if s: 

3952 e, junk_s = g.stripBOM(s) 

3953 if e: 

3954 return e 

3955 aList = g.get_directives_dict_list(p) 

3956 e = g.scanAtEncodingDirectives(aList) 

3957 if s and s.strip() and not e: 

3958 e = 'utf-8' 

3959 return e 

3960#@+node:ville.20090701144325.14942: *3* g.guessExternalEditor 

3961def guessExternalEditor(c: Cmdr=None) -> Optional[str]: 

3962 """ Return a 'sensible' external editor """ 

3963 editor = ( 

3964 os.environ.get("LEO_EDITOR") or 

3965 os.environ.get("EDITOR") or 

3966 g.app.db and g.app.db.get("LEO_EDITOR") or 

3967 c and c.config.getString('external-editor')) 

3968 if editor: 

3969 return editor 

3970 # fallbacks 

3971 platform = sys.platform.lower() 

3972 if platform.startswith('win'): 

3973 return "notepad" 

3974 if platform.startswith('linux'): 

3975 return 'gedit' 

3976 g.es( 

3977 '''No editor set. 

3978Please set LEO_EDITOR or EDITOR environment variable, 

3979or do g.app.db['LEO_EDITOR'] = "gvim"''', 

3980 ) 

3981 return None 

3982#@+node:ekr.20160330204014.1: *3* g.init_dialog_folder 

3983def init_dialog_folder(c: Cmdr, p: Pos, use_at_path: bool=True) -> str: 

3984 """Return the most convenient folder to open or save a file.""" 

3985 if c and p and use_at_path: 

3986 path = g.fullPath(c, p) 

3987 if path: 

3988 dir_ = g.os_path_dirname(path) 

3989 if dir_ and g.os_path_exists(dir_): 

3990 return dir_ 

3991 table = ( 

3992 ('c.last_dir', c and c.last_dir), 

3993 ('os.curdir', g.os_path_abspath(os.curdir)), 

3994 ) 

3995 for kind, dir_ in table: 

3996 if dir_ and g.os_path_exists(dir_): 

3997 return dir_ 

3998 return '' 

3999#@+node:ekr.20100329071036.5744: *3* g.is_binary_file/external_file/string 

4000def is_binary_file(f: Any) -> bool: 

4001 return f and isinstance(f, io.BufferedIOBase) 

4002 

4003def is_binary_external_file(fileName: str) -> bool: 

4004 try: 

4005 with open(fileName, 'rb') as f: 

4006 s = f.read(1024) # bytes, in Python 3. 

4007 return g.is_binary_string(s) 

4008 except IOError: 

4009 return False 

4010 except Exception: 

4011 g.es_exception() 

4012 return False 

4013 

4014def is_binary_string(s: str) -> bool: 

4015 # http://stackoverflow.com/questions/898669 

4016 # aList is a list of all non-binary characters. 

4017 aList = [7, 8, 9, 10, 12, 13, 27] + list(range(0x20, 0x100)) 

4018 return bool(s.translate(None, bytes(aList))) # type:ignore 

4019#@+node:EKR.20040504154039: *3* g.is_sentinel 

4020def is_sentinel(line: str, delims: Sequence) -> bool: 

4021 """Return True if line starts with a sentinel comment.""" 

4022 delim1, delim2, delim3 = delims 

4023 line = line.lstrip() 

4024 if delim1: 

4025 return line.startswith(delim1 + '@') 

4026 if delim2 and delim3: 

4027 i = line.find(delim2 + '@') 

4028 j = line.find(delim3) 

4029 return 0 == i < j 

4030 g.error(f"is_sentinel: can not happen. delims: {repr(delims)}") 

4031 return False 

4032#@+node:ekr.20031218072017.3119: *3* g.makeAllNonExistentDirectories 

4033def makeAllNonExistentDirectories(theDir: str) -> Optional[str]: 

4034 """ 

4035 A wrapper from os.makedirs. 

4036 Attempt to make all non-existent directories. 

4037 

4038 Return True if the directory exists or was created successfully. 

4039 """ 

4040 # Return True if the directory already exists. 

4041 theDir = g.os_path_normpath(theDir) 

4042 ok = g.os_path_isdir(theDir) and g.os_path_exists(theDir) 

4043 if ok: 

4044 return theDir 

4045 # #1450: Create the directory with os.makedirs. 

4046 try: 

4047 os.makedirs(theDir, mode=0o777, exist_ok=False) 

4048 return theDir 

4049 except Exception: 

4050 return None 

4051#@+node:ekr.20071114113736: *3* g.makePathRelativeTo 

4052def makePathRelativeTo(fullPath: str, basePath: str) -> str: 

4053 if fullPath.startswith(basePath): 

4054 s = fullPath[len(basePath) :] 

4055 if s.startswith(os.path.sep): 

4056 s = s[len(os.path.sep) :] 

4057 return s 

4058 return fullPath 

4059#@+node:ekr.20090520055433.5945: *3* g.openWithFileName 

4060def openWithFileName(fileName: str, old_c: Cmdr=None, gui: str=None) -> Cmdr: 

4061 """ 

4062 Create a Leo Frame for the indicated fileName if the file exists. 

4063 

4064 Return the commander of the newly-opened outline. 

4065 """ 

4066 return g.app.loadManager.loadLocalFile(fileName, gui, old_c) 

4067#@+node:ekr.20150306035851.7: *3* g.readFileIntoEncodedString 

4068def readFileIntoEncodedString(fn: str, silent: bool=False) -> Optional[bytes]: 

4069 """Return the raw contents of the file whose full path is fn.""" 

4070 try: 

4071 with open(fn, 'rb') as f: 

4072 return f.read() 

4073 except IOError: 

4074 if not silent: 

4075 g.error('can not open', fn) 

4076 except Exception: 

4077 if not silent: 

4078 g.error(f"readFileIntoEncodedString: exception reading {fn}") 

4079 g.es_exception() 

4080 return None 

4081#@+node:ekr.20100125073206.8710: *3* g.readFileIntoString 

4082def readFileIntoString( 

4083 fileName: str, 

4084 encoding: str='utf-8', # BOM may override this. 

4085 kind: str=None, # @file, @edit, ... 

4086 verbose: bool=True, 

4087) -> Tuple[Any, Any]: 

4088 """ 

4089 Return the contents of the file whose full path is fileName. 

4090 

4091 Return (s,e) 

4092 s is the string, converted to unicode, or None if there was an error. 

4093 e is the encoding of s, computed in the following order: 

4094 - The BOM encoding if the file starts with a BOM mark. 

4095 - The encoding given in the # -*- coding: utf-8 -*- line for python files. 

4096 - The encoding given by the 'encoding' keyword arg. 

4097 - None, which typically means 'utf-8'. 

4098 """ 

4099 if not fileName: 

4100 if verbose: 

4101 g.trace('no fileName arg given') 

4102 return None, None 

4103 if g.os_path_isdir(fileName): 

4104 if verbose: 

4105 g.trace('not a file:', fileName) 

4106 return None, None 

4107 if not g.os_path_exists(fileName): 

4108 if verbose: 

4109 g.error('file not found:', fileName) 

4110 return None, None 

4111 try: 

4112 e = None 

4113 with open(fileName, 'rb') as f: 

4114 s = f.read() 

4115 # Fix #391. 

4116 if not s: 

4117 return '', None 

4118 # New in Leo 4.11: check for unicode BOM first. 

4119 e, s = g.stripBOM(s) 

4120 if not e: 

4121 # Python's encoding comments override everything else. 

4122 junk, ext = g.os_path_splitext(fileName) 

4123 if ext == '.py': 

4124 e = g.getPythonEncodingFromString(s) 

4125 s = g.toUnicode(s, encoding=e or encoding) 

4126 return s, e 

4127 except IOError: 

4128 # Translate 'can not open' and kind, but not fileName. 

4129 if verbose: 

4130 g.error('can not open', '', (kind or ''), fileName) 

4131 except Exception: 

4132 g.error(f"readFileIntoString: unexpected exception reading {fileName}") 

4133 g.es_exception() 

4134 return None, None 

4135#@+node:ekr.20160504062833.1: *3* g.readFileToUnicodeString 

4136def readFileIntoUnicodeString(fn: str, encoding: Optional[str]=None, silent: bool=False) -> Optional[str]: 

4137 """Return the raw contents of the file whose full path is fn.""" 

4138 try: 

4139 with open(fn, 'rb') as f: 

4140 s = f.read() 

4141 return g.toUnicode(s, encoding=encoding) 

4142 except IOError: 

4143 if not silent: 

4144 g.error('can not open', fn) 

4145 except Exception: 

4146 g.error(f"readFileIntoUnicodeString: unexpected exception reading {fn}") 

4147 g.es_exception() 

4148 return None 

4149#@+node:ekr.20031218072017.3120: *3* g.readlineForceUnixNewline 

4150#@+at Stephen P. Schaefer 9/7/2002 

4151# 

4152# The Unix readline() routine delivers "\r\n" line end strings verbatim, 

4153# while the windows versions force the string to use the Unix convention 

4154# of using only "\n". This routine causes the Unix readline to do the 

4155# same. 

4156#@@c 

4157 

4158def readlineForceUnixNewline(f: Any, fileName: Optional[str]=None) -> str: 

4159 try: 

4160 s = f.readline() 

4161 except UnicodeDecodeError: 

4162 g.trace(f"UnicodeDecodeError: {fileName}", f, g.callers()) 

4163 s = '' 

4164 if len(s) >= 2 and s[-2] == "\r" and s[-1] == "\n": 

4165 s = s[0:-2] + "\n" 

4166 return s 

4167#@+node:ekr.20031218072017.3124: *3* g.sanitize_filename 

4168def sanitize_filename(s: str) -> str: 

4169 """ 

4170 Prepares string s to be a valid file name: 

4171 

4172 - substitute '_' for whitespace and special path characters. 

4173 - eliminate all other non-alphabetic characters. 

4174 - convert double quotes to single quotes. 

4175 - strip leading and trailing whitespace. 

4176 - return at most 128 characters. 

4177 """ 

4178 result = [] 

4179 for ch in s: 

4180 if ch in string.ascii_letters: 

4181 result.append(ch) 

4182 elif ch == '\t': 

4183 result.append(' ') 

4184 elif ch == '"': 

4185 result.append("'") 

4186 elif ch in '\\/:|<>*:._': 

4187 result.append('_') 

4188 s = ''.join(result).strip() 

4189 while len(s) > 1: 

4190 n = len(s) 

4191 s = s.replace('__', '_') 

4192 if len(s) == n: 

4193 break 

4194 return s[:128] 

4195#@+node:ekr.20060328150113: *3* g.setGlobalOpenDir 

4196def setGlobalOpenDir(fileName: str) -> None: 

4197 if fileName: 

4198 g.app.globalOpenDir = g.os_path_dirname(fileName) 

4199 # g.es('current directory:',g.app.globalOpenDir) 

4200#@+node:ekr.20031218072017.3125: *3* g.shortFileName & shortFilename 

4201def shortFileName(fileName: str, n: int=None) -> str: 

4202 """Return the base name of a path.""" 

4203 if n is not None: 

4204 g.trace('"n" keyword argument is no longer used') 

4205 return g.os_path_basename(fileName) if fileName else '' 

4206 

4207shortFilename = shortFileName 

4208#@+node:ekr.20150610125813.1: *3* g.splitLongFileName 

4209def splitLongFileName(fn: str, limit: int=40) -> str: 

4210 """Return fn, split into lines at slash characters.""" 

4211 aList = fn.replace('\\', '/').split('/') 

4212 n, result = 0, [] 

4213 for i, s in enumerate(aList): 

4214 n += len(s) 

4215 result.append(s) 

4216 if i + 1 < len(aList): 

4217 result.append('/') 

4218 n += 1 

4219 if n > limit: 

4220 result.append('\n') 

4221 n = 0 

4222 return ''.join(result) 

4223#@+node:ekr.20190114061452.26: *3* g.writeFile 

4224def writeFile(contents: Union[bytes, str], encoding: str, fileName: str) -> bool: 

4225 """Create a file with the given contents.""" 

4226 try: 

4227 if isinstance(contents, str): 

4228 contents = g.toEncodedString(contents, encoding=encoding) 

4229 # 'wb' preserves line endings. 

4230 with open(fileName, 'wb') as f: 

4231 f.write(contents) # type:ignore 

4232 return True 

4233 except Exception as e: 

4234 print(f"exception writing: {fileName}:\n{e}") 

4235 # g.trace(g.callers()) 

4236 # g.es_exception() 

4237 return False 

4238#@+node:ekr.20031218072017.3151: ** g.Finding & Scanning 

4239#@+node:ekr.20140602083643.17659: *3* g.find_word 

4240def find_word(s: str, word: str, i: int=0) -> int: 

4241 """ 

4242 Return the index of the first occurance of word in s, or -1 if not found. 

4243 

4244 g.find_word is *not* the same as s.find(i,word); 

4245 g.find_word ensures that only word-matches are reported. 

4246 """ 

4247 while i < len(s): 

4248 progress = i 

4249 i = s.find(word, i) 

4250 if i == -1: 

4251 return -1 

4252 # Make sure we are at the start of a word. 

4253 if i > 0: 

4254 ch = s[i - 1] 

4255 if ch == '_' or ch.isalnum(): 

4256 i += len(word) 

4257 continue 

4258 if g.match_word(s, i, word): 

4259 return i 

4260 i += len(word) 

4261 assert progress < i 

4262 return -1 

4263#@+node:ekr.20211029090118.1: *3* g.findAncestorVnodeByPredicate 

4264def findAncestorVnodeByPredicate(p: Pos, v_predicate: Any) -> Optional["VNode"]: 

4265 """ 

4266 Return first ancestor vnode matching the predicate. 

4267  

4268 The predicate must must be a function of a single vnode argument. 

4269 """ 

4270 if not p: 

4271 return None 

4272 # First, look up the tree. 

4273 for p2 in p.self_and_parents(): 

4274 if v_predicate(p2.v): 

4275 return p2.v 

4276 # Look at parents of all cloned nodes. 

4277 if not p.isCloned(): 

4278 return None 

4279 seen = [] # vnodes that have already been searched. 

4280 parents = p.v.parents[:] # vnodes to be searched. 

4281 while parents: 

4282 parent_v = parents.pop() 

4283 if parent_v in seen: 

4284 continue 

4285 seen.append(parent_v) 

4286 if v_predicate(parent_v): 

4287 return parent_v 

4288 for grand_parent_v in parent_v.parents: 

4289 if grand_parent_v not in seen: 

4290 parents.append(grand_parent_v) 

4291 return None 

4292#@+node:ekr.20170220103251.1: *3* g.findRootsWithPredicate 

4293def findRootsWithPredicate(c: Cmdr, root: Pos, predicate: Any=None) -> List[Pos]: 

4294 """ 

4295 Commands often want to find one or more **roots**, given a position p. 

4296 A root is the position of any node matching a predicate. 

4297 

4298 This function formalizes the search order used by the black, 

4299 pylint, pyflakes and the rst3 commands, returning a list of zero 

4300 or more found roots. 

4301 """ 

4302 seen = [] 

4303 roots = [] 

4304 if predicate is None: 

4305 

4306 # A useful default predicate for python. 

4307 # pylint: disable=function-redefined 

4308 

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

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

4311 

4312 # 1. Search p's tree. 

4313 for p in root.self_and_subtree(copy=False): 

4314 if predicate(p) and p.v not in seen: 

4315 seen.append(p.v) 

4316 roots.append(p.copy()) 

4317 if roots: 

4318 return roots 

4319 # 2. Look up the tree. 

4320 for p in root.parents(): 

4321 if predicate(p): 

4322 return [p.copy()] 

4323 # 3. Expand the search if root is a clone. 

4324 clones = [] 

4325 for p in root.self_and_parents(copy=False): 

4326 if p.isCloned(): 

4327 clones.append(p.v) 

4328 if clones: 

4329 for p in c.all_positions(copy=False): 

4330 if predicate(p): 

4331 # Match if any node in p's tree matches any clone. 

4332 for p2 in p.self_and_subtree(): 

4333 if p2.v in clones: 

4334 return [p.copy()] 

4335 return [] 

4336#@+node:ekr.20031218072017.3156: *3* g.scanError 

4337# It is dubious to bump the Tangle error count here, but it really doesn't hurt. 

4338 

4339def scanError(s: str) -> None: 

4340 """Bump the error count in the tangle command.""" 

4341 # New in Leo 4.4b1: just set this global. 

4342 g.app.scanErrors += 1 

4343 g.es('', s) 

4344#@+node:ekr.20031218072017.3157: *3* g.scanf 

4345# A quick and dirty sscanf. Understands only %s and %d. 

4346 

4347def scanf(s: str, pat: str) -> List[str]: 

4348 count = pat.count("%s") + pat.count("%d") 

4349 pat = pat.replace("%s", r"(\S+)") 

4350 pat = pat.replace("%d", r"(\d+)") 

4351 parts = re.split(pat, s) 

4352 result: List[str] = [] 

4353 for part in parts: 

4354 if part and len(result) < count: 

4355 result.append(part) 

4356 return result 

4357#@+node:ekr.20031218072017.3158: *3* g.Scanners: calling scanError 

4358#@+at These scanners all call g.scanError() directly or indirectly, so they 

4359# will call g.es if they find an error. g.scanError() also bumps 

4360# c.tangleCommands.errors, which is harmless if we aren't tangling, and 

4361# useful if we are. 

4362# 

4363# These routines are called by the Import routines and the Tangle routines. 

4364#@+node:ekr.20031218072017.3159: *4* g.skip_block_comment 

4365# Scans past a block comment (an old_style C comment). 

4366 

4367def skip_block_comment(s: str, i: int) -> int: 

4368 assert g.match(s, i, "/*") 

4369 j = i 

4370 i += 2 

4371 n = len(s) 

4372 k = s.find("*/", i) 

4373 if k == -1: 

4374 g.scanError("Run on block comment: " + s[j:i]) 

4375 return n 

4376 return k + 2 

4377#@+node:ekr.20031218072017.3160: *4* g.skip_braces 

4378#@+at This code is called only from the import logic, so we are allowed to 

4379# try some tricks. In particular, we assume all braces are matched in 

4380# if blocks. 

4381#@@c 

4382 

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

4384 """ 

4385 Skips from the opening to the matching brace. 

4386 

4387 If no matching is found i is set to len(s) 

4388 """ 

4389 # start = g.get_line(s,i) 

4390 assert g.match(s, i, '{') 

4391 level = 0 

4392 n = len(s) 

4393 while i < n: 

4394 c = s[i] 

4395 if c == '{': 

4396 level += 1 

4397 i += 1 

4398 elif c == '}': 

4399 level -= 1 

4400 if level <= 0: 

4401 return i 

4402 i += 1 

4403 elif c == '\'' or c == '"': 

4404 i = g.skip_string(s, i) 

4405 elif g.match(s, i, '//'): 

4406 i = g.skip_to_end_of_line(s, i) 

4407 elif g.match(s, i, '/*'): 

4408 i = g.skip_block_comment(s, i) 

4409 # 7/29/02: be more careful handling conditional code. 

4410 elif ( 

4411 g.match_word(s, i, "#if") or 

4412 g.match_word(s, i, "#ifdef") or 

4413 g.match_word(s, i, "#ifndef") 

4414 ): 

4415 i, delta = g.skip_pp_if(s, i) 

4416 level += delta 

4417 else: i += 1 

4418 return i 

4419#@+node:ekr.20031218072017.3162: *4* g.skip_parens 

4420def skip_parens(s: str, i: int) -> int: 

4421 """ 

4422 Skips from the opening ( to the matching ). 

4423 

4424 If no matching is found i is set to len(s). 

4425 """ 

4426 level = 0 

4427 n = len(s) 

4428 assert g.match(s, i, '('), repr(s[i]) 

4429 while i < n: 

4430 c = s[i] 

4431 if c == '(': 

4432 level += 1 

4433 i += 1 

4434 elif c == ')': 

4435 level -= 1 

4436 if level <= 0: 

4437 return i 

4438 i += 1 

4439 elif c == '\'' or c == '"': 

4440 i = g.skip_string(s, i) 

4441 elif g.match(s, i, "//"): 

4442 i = g.skip_to_end_of_line(s, i) 

4443 elif g.match(s, i, "/*"): 

4444 i = g.skip_block_comment(s, i) 

4445 else: 

4446 i += 1 

4447 return i 

4448#@+node:ekr.20031218072017.3163: *4* g.skip_pascal_begin_end 

4449def skip_pascal_begin_end(s: str, i: int) -> int: 

4450 """ 

4451 Skips from begin to matching end. 

4452 If found, i points to the end. Otherwise, i >= len(s) 

4453 The end keyword matches begin, case, class, record, and try. 

4454 """ 

4455 assert g.match_c_word(s, i, "begin") 

4456 level = 1 

4457 i = g.skip_c_id(s, i) # Skip the opening begin. 

4458 while i < len(s): 

4459 ch = s[i] 

4460 if ch == '{': 

4461 i = g.skip_pascal_braces(s, i) 

4462 elif ch == '"' or ch == '\'': 

4463 i = g.skip_pascal_string(s, i) 

4464 elif g.match(s, i, "//"): 

4465 i = g.skip_line(s, i) 

4466 elif g.match(s, i, "(*"): 

4467 i = g.skip_pascal_block_comment(s, i) 

4468 elif g.match_c_word(s, i, "end"): 

4469 level -= 1 

4470 if level == 0: 

4471 return i 

4472 i = g.skip_c_id(s, i) 

4473 elif g.is_c_id(ch): 

4474 j = i 

4475 i = g.skip_c_id(s, i) 

4476 name = s[j:i] 

4477 if name in ["begin", "case", "class", "record", "try"]: 

4478 level += 1 

4479 else: 

4480 i += 1 

4481 return i 

4482#@+node:ekr.20031218072017.3164: *4* g.skip_pascal_block_comment 

4483def skip_pascal_block_comment(s: str, i: int) -> int: 

4484 """Scan past a pascal comment delimited by (* and *).""" 

4485 j = i 

4486 assert g.match(s, i, "(*") 

4487 i = s.find("*)", i) 

4488 if i > -1: 

4489 return i + 2 

4490 g.scanError("Run on comment" + s[j:i]) 

4491 return len(s) 

4492#@+node:ekr.20031218072017.3165: *4* g.skip_pascal_string 

4493def skip_pascal_string(s: str, i: int) -> int: 

4494 j = i 

4495 delim = s[i] 

4496 i += 1 

4497 assert delim == '"' or delim == '\'' 

4498 while i < len(s): 

4499 if s[i] == delim: 

4500 return i + 1 

4501 i += 1 

4502 g.scanError("Run on string: " + s[j:i]) 

4503 return i 

4504#@+node:ekr.20031218072017.3166: *4* g.skip_heredoc_string 

4505def skip_heredoc_string(s: str, i: int) -> int: 

4506 """ 

4507 08-SEP-2002 DTHEIN. 

4508 A heredoc string in PHP looks like: 

4509 

4510 <<<EOS 

4511 This is my string. 

4512 It is mine. I own it. 

4513 No one else has it. 

4514 EOS 

4515 

4516 It begins with <<< plus a token (naming same as PHP variable names). 

4517 It ends with the token on a line by itself (must start in first position. 

4518 """ 

4519 j = i 

4520 assert g.match(s, i, "<<<") 

4521 m = re.match(r"\<\<\<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)", s[i:]) 

4522 if m is None: 

4523 i += 3 

4524 return i 

4525 # 14-SEP-2002 DTHEIN: needed to add \n to find word, not just string 

4526 delim = m.group(1) + '\n' 

4527 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: look after \n, not before 

4528 n = len(s) 

4529 while i < n and not g.match(s, i, delim): 

4530 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: move past \n 

4531 if i >= n: 

4532 g.scanError("Run on string: " + s[j:i]) 

4533 elif g.match(s, i, delim): 

4534 i += len(delim) 

4535 return i 

4536#@+node:ekr.20031218072017.3167: *4* g.skip_pp_directive 

4537def skip_pp_directive(s: str, i: int) -> int: 

4538 """Now handles continuation lines and block comments.""" 

4539 while i < len(s): 

4540 if g.is_nl(s, i): 

4541 if g.escaped(s, i): 

4542 i = g.skip_nl(s, i) 

4543 else: 

4544 break 

4545 elif g.match(s, i, "//"): 

4546 i = g.skip_to_end_of_line(s, i) 

4547 elif g.match(s, i, "/*"): 

4548 i = g.skip_block_comment(s, i) 

4549 else: 

4550 i += 1 

4551 return i 

4552#@+node:ekr.20031218072017.3168: *4* g.skip_pp_if 

4553# Skips an entire if or if def statement, including any nested statements. 

4554 

4555def skip_pp_if(s: str, i: int) -> Tuple[int, int]: 

4556 start_line = g.get_line(s, i) # used for error messages. 

4557 assert( 

4558 g.match_word(s, i, "#if") or 

4559 g.match_word(s, i, "#ifdef") or 

4560 g.match_word(s, i, "#ifndef")) 

4561 i = g.skip_line(s, i) 

4562 i, delta1 = g.skip_pp_part(s, i) 

4563 i = g.skip_ws(s, i) 

4564 if g.match_word(s, i, "#else"): 

4565 i = g.skip_line(s, i) 

4566 i = g.skip_ws(s, i) 

4567 i, delta2 = g.skip_pp_part(s, i) 

4568 if delta1 != delta2: 

4569 g.es("#if and #else parts have different braces:", start_line) 

4570 i = g.skip_ws(s, i) 

4571 if g.match_word(s, i, "#endif"): 

4572 i = g.skip_line(s, i) 

4573 else: 

4574 g.es("no matching #endif:", start_line) 

4575 return i, delta1 

4576#@+node:ekr.20031218072017.3169: *4* g.skip_pp_part 

4577# Skip to an #else or #endif. The caller has eaten the #if, #ifdef, #ifndef or #else 

4578 

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

4580 

4581 delta = 0 

4582 while i < len(s): 

4583 c = s[i] 

4584 if ( 

4585 g.match_word(s, i, "#if") or 

4586 g.match_word(s, i, "#ifdef") or 

4587 g.match_word(s, i, "#ifndef") 

4588 ): 

4589 i, delta1 = g.skip_pp_if(s, i) 

4590 delta += delta1 

4591 elif g.match_word(s, i, "#else") or g.match_word(s, i, "#endif"): 

4592 return i, delta 

4593 elif c == '\'' or c == '"': 

4594 i = g.skip_string(s, i) 

4595 elif c == '{': 

4596 delta += 1 

4597 i += 1 

4598 elif c == '}': 

4599 delta -= 1 

4600 i += 1 

4601 elif g.match(s, i, "//"): 

4602 i = g.skip_line(s, i) 

4603 elif g.match(s, i, "/*"): 

4604 i = g.skip_block_comment(s, i) 

4605 else: 

4606 i += 1 

4607 return i, delta 

4608#@+node:ekr.20031218072017.3171: *4* g.skip_to_semicolon 

4609# Skips to the next semicolon that is not in a comment or a string. 

4610 

4611def skip_to_semicolon(s: str, i: int) -> int: 

4612 n = len(s) 

4613 while i < n: 

4614 c = s[i] 

4615 if c == ';': 

4616 return i 

4617 if c == '\'' or c == '"': 

4618 i = g.skip_string(s, i) 

4619 elif g.match(s, i, "//"): 

4620 i = g.skip_to_end_of_line(s, i) 

4621 elif g.match(s, i, "/*"): 

4622 i = g.skip_block_comment(s, i) 

4623 else: 

4624 i += 1 

4625 return i 

4626#@+node:ekr.20031218072017.3172: *4* g.skip_typedef 

4627def skip_typedef(s: str, i: int) -> int: 

4628 n = len(s) 

4629 while i < n and g.is_c_id(s[i]): 

4630 i = g.skip_c_id(s, i) 

4631 i = g.skip_ws_and_nl(s, i) 

4632 if g.match(s, i, '{'): 

4633 i = g.skip_braces(s, i) 

4634 i = g.skip_to_semicolon(s, i) 

4635 return i 

4636#@+node:ekr.20201127143342.1: *3* g.see_more_lines 

4637def see_more_lines(s: str, ins: int, n: int=4) -> int: 

4638 """ 

4639 Extend index i within string s to include n more lines. 

4640 """ 

4641 # Show more lines, if they exist. 

4642 if n > 0: 

4643 for z in range(n): 

4644 if ins >= len(s): 

4645 break 

4646 i, j = g.getLine(s, ins) 

4647 ins = j 

4648 return max(0, min(ins, len(s))) 

4649#@+node:ekr.20031218072017.3195: *3* g.splitLines 

4650def splitLines(s: str) -> List[str]: 

4651 """ 

4652 Split s into lines, preserving the number of lines and 

4653 the endings of all lines, including the last line. 

4654 """ 

4655 return s.splitlines(True) if s else [] # This is a Python string function! 

4656 

4657splitlines = splitLines 

4658#@+node:ekr.20031218072017.3173: *3* Scanners: no error messages 

4659#@+node:ekr.20031218072017.3174: *4* g.escaped 

4660# Returns True if s[i] is preceded by an odd number of backslashes. 

4661 

4662def escaped(s: str, i: int) -> bool: 

4663 count = 0 

4664 while i - 1 >= 0 and s[i - 1] == '\\': 

4665 count += 1 

4666 i -= 1 

4667 return (count % 2) == 1 

4668#@+node:ekr.20031218072017.3175: *4* g.find_line_start 

4669def find_line_start(s: str, i: int) -> int: 

4670 """Return the index in s of the start of the line containing s[i].""" 

4671 if i < 0: 

4672 return 0 # New in Leo 4.4.5: add this defensive code. 

4673 # bug fix: 11/2/02: change i to i+1 in rfind 

4674 i = s.rfind('\n', 0, i + 1) # Finds the highest index in the range. 

4675 return 0 if i == -1 else i + 1 

4676#@+node:ekr.20031218072017.3176: *4* g.find_on_line 

4677def find_on_line(s: str, i: int, pattern: str) -> int: 

4678 j = s.find('\n', i) 

4679 if j == -1: 

4680 j = len(s) 

4681 k = s.find(pattern, i, j) 

4682 return k 

4683#@+node:ekr.20031218072017.3179: *4* g.g.is_special 

4684def is_special(s: str, directive: str) -> Tuple[bool, int]: 

4685 """Return True if the body text contains the @ directive.""" 

4686 assert(directive and directive[0] == '@') 

4687 # Most directives must start the line. 

4688 lws = directive in ("@others", "@all") 

4689 pattern_s = r'^\s*(%s\b)' if lws else r'^(%s\b)' 

4690 pattern = re.compile(pattern_s % directive, re.MULTILINE) 

4691 m = re.search(pattern, s) 

4692 if m: 

4693 return True, m.start(1) 

4694 return False, -1 

4695#@+node:ekr.20031218072017.3177: *4* g.is_c_id 

4696def is_c_id(ch: str) -> bool: 

4697 return g.isWordChar(ch) 

4698#@+node:ekr.20031218072017.3178: *4* g.is_nl 

4699def is_nl(s: str, i: int) -> bool: 

4700 return i < len(s) and (s[i] == '\n' or s[i] == '\r') 

4701#@+node:ekr.20031218072017.3180: *4* g.is_ws & is_ws_or_nl 

4702def is_ws(ch: str) -> bool: 

4703 return ch == '\t' or ch == ' ' 

4704 

4705def is_ws_or_nl(s: str, i: int) -> bool: 

4706 return g.is_nl(s, i) or (i < len(s) and g.is_ws(s[i])) 

4707#@+node:ekr.20031218072017.3181: *4* g.match 

4708# Warning: this code makes no assumptions about what follows pattern. 

4709 

4710def match(s: str, i: int, pattern: str) -> bool: 

4711 return bool(s and pattern and s.find(pattern, i, i + len(pattern)) == i) 

4712#@+node:ekr.20031218072017.3182: *4* g.match_c_word 

4713def match_c_word(s: str, i: int, name: str) -> bool: 

4714 n = len(name) 

4715 return bool( 

4716 name and 

4717 name == s[i : i + n] and 

4718 (i + n == len(s) or not g.is_c_id(s[i + n])) 

4719 ) 

4720#@+node:ekr.20031218072017.3183: *4* g.match_ignoring_case 

4721def match_ignoring_case(s1: str, s2: str) -> bool: 

4722 return bool(s1 and s2 and s1.lower() == s2.lower()) 

4723#@+node:ekr.20031218072017.3184: *4* g.match_word & g.match_words 

4724def match_word(s: str, i: int, pattern: str) -> bool: 

4725 

4726 # Using a regex is surprisingly tricky. 

4727 if pattern is None: 

4728 return False 

4729 if i > 0 and g.isWordChar(s[i - 1]): # Bug fix: 2017/06/01. 

4730 return False 

4731 j = len(pattern) 

4732 if j == 0: 

4733 return False 

4734 if s.find(pattern, i, i + j) != i: 

4735 return False 

4736 if i + j >= len(s): 

4737 return True 

4738 ch = s[i + j] 

4739 return not g.isWordChar(ch) 

4740 

4741def match_words(s: str, i: int, patterns: Sequence[str]) -> bool: 

4742 return any(g.match_word(s, i, pattern) for pattern in patterns) 

4743#@+node:ekr.20031218072017.3185: *4* g.skip_blank_lines 

4744# This routine differs from skip_ws_and_nl in that 

4745# it does not advance over whitespace at the start 

4746# of a non-empty or non-nl terminated line 

4747 

4748def skip_blank_lines(s: str, i: int) -> int: 

4749 while i < len(s): 

4750 if g.is_nl(s, i): 

4751 i = g.skip_nl(s, i) 

4752 elif g.is_ws(s[i]): 

4753 j = g.skip_ws(s, i) 

4754 if g.is_nl(s, j): 

4755 i = j 

4756 else: break 

4757 else: break 

4758 return i 

4759#@+node:ekr.20031218072017.3186: *4* g.skip_c_id 

4760def skip_c_id(s: str, i: int) -> int: 

4761 n = len(s) 

4762 while i < n and g.isWordChar(s[i]): 

4763 i += 1 

4764 return i 

4765#@+node:ekr.20040705195048: *4* g.skip_id 

4766def skip_id(s: str, i: int, chars: str=None) -> int: 

4767 chars = g.toUnicode(chars) if chars else '' 

4768 n = len(s) 

4769 while i < n and (g.isWordChar(s[i]) or s[i] in chars): 

4770 i += 1 

4771 return i 

4772#@+node:ekr.20031218072017.3187: *4* g.skip_line, skip_to_start/end_of_line 

4773#@+at These methods skip to the next newline, regardless of whether the 

4774# newline may be preceeded by a backslash. Consequently, they should be 

4775# used only when we know that we are not in a preprocessor directive or 

4776# string. 

4777#@@c 

4778 

4779def skip_line(s: str, i: int) -> int: 

4780 if i >= len(s): 

4781 return len(s) 

4782 if i < 0: 

4783 i = 0 

4784 i = s.find('\n', i) 

4785 if i == -1: 

4786 return len(s) 

4787 return i + 1 

4788 

4789def skip_to_end_of_line(s: str, i: int) -> int: 

4790 if i >= len(s): 

4791 return len(s) 

4792 if i < 0: 

4793 i = 0 

4794 i = s.find('\n', i) 

4795 if i == -1: 

4796 return len(s) 

4797 return i 

4798 

4799def skip_to_start_of_line(s: str, i: int) -> int: 

4800 if i >= len(s): 

4801 return len(s) 

4802 if i <= 0: 

4803 return 0 

4804 # Don't find s[i], so it doesn't matter if s[i] is a newline. 

4805 i = s.rfind('\n', 0, i) 

4806 if i == -1: 

4807 return 0 

4808 return i + 1 

4809#@+node:ekr.20031218072017.3188: *4* g.skip_long 

4810def skip_long(s: str, i: int) -> Tuple[int, Optional[int]]: 

4811 """ 

4812 Scan s[i:] for a valid int. 

4813 Return (i, val) or (i, None) if s[i] does not point at a number. 

4814 """ 

4815 val = 0 

4816 i = g.skip_ws(s, i) 

4817 n = len(s) 

4818 if i >= n or (not s[i].isdigit() and s[i] not in '+-'): 

4819 return i, None 

4820 j = i 

4821 if s[i] in '+-': # Allow sign before the first digit 

4822 i += 1 

4823 while i < n and s[i].isdigit(): 

4824 i += 1 

4825 try: # There may be no digits. 

4826 val = int(s[j:i]) 

4827 return i, val 

4828 except Exception: 

4829 return i, None 

4830#@+node:ekr.20031218072017.3190: *4* g.skip_nl 

4831# We need this function because different systems have different end-of-line conventions. 

4832 

4833def skip_nl(s: str, i: int) -> int: 

4834 """Skips a single "logical" end-of-line character.""" 

4835 if g.match(s, i, "\r\n"): 

4836 return i + 2 

4837 if g.match(s, i, '\n') or g.match(s, i, '\r'): 

4838 return i + 1 

4839 return i 

4840#@+node:ekr.20031218072017.3191: *4* g.skip_non_ws 

4841def skip_non_ws(s: str, i: int) -> int: 

4842 n = len(s) 

4843 while i < n and not g.is_ws(s[i]): 

4844 i += 1 

4845 return i 

4846#@+node:ekr.20031218072017.3192: *4* g.skip_pascal_braces 

4847# Skips from the opening { to the matching }. 

4848 

4849def skip_pascal_braces(s: str, i: int) -> int: 

4850 # No constructs are recognized inside Pascal block comments! 

4851 if i == -1: 

4852 return len(s) 

4853 return s.find('}', i) 

4854#@+node:ekr.20031218072017.3170: *4* g.skip_python_string 

4855def skip_python_string(s: str, i: int) -> int: 

4856 if g.match(s, i, "'''") or g.match(s, i, '"""'): 

4857 delim = s[i] * 3 

4858 i += 3 

4859 k = s.find(delim, i) 

4860 if k > -1: 

4861 return k + 3 

4862 return len(s) 

4863 return g.skip_string(s, i) 

4864#@+node:ekr.20031218072017.2369: *4* g.skip_string 

4865def skip_string(s: str, i: int) -> int: 

4866 """Scan forward to the end of a string.""" 

4867 delim = s[i] 

4868 i += 1 

4869 assert delim in '\'"', (repr(delim), repr(s)) 

4870 n = len(s) 

4871 while i < n and s[i] != delim: 

4872 if s[i] == '\\': 

4873 i += 2 

4874 else: 

4875 i += 1 

4876 if i >= n: 

4877 pass 

4878 elif s[i] == delim: 

4879 i += 1 

4880 return i 

4881#@+node:ekr.20031218072017.3193: *4* g.skip_to_char 

4882def skip_to_char(s: str, i: int, ch: str) -> Tuple[int, str]: 

4883 j = s.find(ch, i) 

4884 if j == -1: 

4885 return len(s), s[i:] 

4886 return j, s[i:j] 

4887#@+node:ekr.20031218072017.3194: *4* g.skip_ws, skip_ws_and_nl 

4888def skip_ws(s: str, i: int) -> int: 

4889 n = len(s) 

4890 while i < n and g.is_ws(s[i]): 

4891 i += 1 

4892 return i 

4893 

4894def skip_ws_and_nl(s: str, i: int) -> int: 

4895 n = len(s) 

4896 while i < n and (g.is_ws(s[i]) or g.is_nl(s, i)): 

4897 i += 1 

4898 return i 

4899#@+node:ekr.20170414034616.1: ** g.Git 

4900#@+node:ekr.20180325025502.1: *3* g.backupGitIssues 

4901def backupGitIssues(c: Cmdr, base_url: str=None) -> None: 

4902 """Get a list of issues from Leo's GitHub site.""" 

4903 if base_url is None: 

4904 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues' 

4905 

4906 root = c.lastTopLevel().insertAfter() 

4907 root.h = f'Backup of issues: {time.strftime("%Y/%m/%d")}' 

4908 label_list: List[str] = [] 

4909 GitIssueController().backup_issues(base_url, c, label_list, root) 

4910 root.expand() 

4911 c.selectPosition(root) 

4912 c.redraw() 

4913 g.trace('done') 

4914#@+node:ekr.20170616102324.1: *3* g.execGitCommand 

4915def execGitCommand(command: str, directory: str) -> List[str]: 

4916 """Execute the given git command in the given directory.""" 

4917 git_dir = g.os_path_finalize_join(directory, '.git') 

4918 if not g.os_path_exists(git_dir): 

4919 g.trace('not found:', git_dir, g.callers()) 

4920 return [] 

4921 if '\n' in command: 

4922 g.trace('removing newline from', command) 

4923 command = command.replace('\n', '') 

4924 # #1777: Save/restore os.curdir 

4925 old_dir = os.getcwd() 

4926 if directory: 

4927 os.chdir(directory) 

4928 try: 

4929 p = subprocess.Popen( 

4930 shlex.split(command), 

4931 stdout=subprocess.PIPE, 

4932 stderr=None, # Shows error traces. 

4933 shell=False, 

4934 ) 

4935 out, err = p.communicate() 

4936 lines = [g.toUnicode(z) for z in g.splitLines(out or [])] 

4937 finally: 

4938 os.chdir(old_dir) 

4939 return lines 

4940#@+node:ekr.20180126043905.1: *3* g.getGitIssues 

4941def getGitIssues(c: Cmdr, 

4942 base_url: str=None, 

4943 label_list: List=None, 

4944 milestone: str=None, 

4945 state: Optional[str]=None, # in (None, 'closed', 'open') 

4946) -> None: 

4947 """Get a list of issues from Leo's GitHub site.""" 

4948 if base_url is None: 

4949 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues' 

4950 if isinstance(label_list, (list, tuple)): 

4951 root = c.lastTopLevel().insertAfter() 

4952 root.h = 'Issues for ' + milestone if milestone else 'Backup' 

4953 GitIssueController().backup_issues(base_url, c, label_list, root) 

4954 root.expand() 

4955 c.selectPosition(root) 

4956 c.redraw() 

4957 g.trace('done') 

4958 else: 

4959 g.trace('label_list must be a list or tuple', repr(label_list)) 

4960#@+node:ekr.20180126044602.1: *4* class GitIssueController 

4961class GitIssueController: 

4962 """ 

4963 A class encapsulating the retrieval of GitHub issues. 

4964 

4965 The GitHub api: https://developer.github.com/v3/issues/ 

4966 """ 

4967 #@+others 

4968 #@+node:ekr.20180325023336.1: *5* git.backup_issues 

4969 def backup_issues(self, base_url: str, c: Cmdr, label_list: List, root: Pos, state: Any=None) -> None: 

4970 

4971 self.base_url = base_url 

4972 self.root = root 

4973 self.milestone = None 

4974 if label_list: 

4975 for state in ('closed', 'open'): 

4976 for label in label_list: 

4977 self.get_one_issue(label, state) 

4978 elif state is None: 

4979 for state in ('closed', 'open'): 

4980 organizer = root.insertAsLastChild() 

4981 organizer.h = f"{state} issues..." 

4982 self.get_all_issues(label_list, organizer, state) 

4983 elif state in ('closed', 'open'): 

4984 self.get_all_issues(label_list, root, state) 

4985 else: 

4986 g.es_print('state must be in (None, "open", "closed")') 

4987 #@+node:ekr.20180325024334.1: *5* git.get_all_issues 

4988 def get_all_issues(self, label_list: List, root: Pos, state: Any, limit: int=100) -> None: 

4989 """Get all issues for the base url.""" 

4990 try: 

4991 import requests 

4992 except Exception: 

4993 g.trace('requests not found: `pip install requests`') 

4994 return 

4995 label = None 

4996 assert state in ('open', 'closed') 

4997 page_url = self.base_url + '?&state=%s&page=%s' 

4998 page, total = 1, 0 

4999 while True: 

5000 url = page_url % (state, page) 

5001 r = requests.get(url) 

5002 try: 

5003 done, n = self.get_one_page(label, page, r, root) 

5004 # Do not remove this trace. It's reassuring. 

5005 g.trace(f"done: {done:5} page: {page:3} found: {n} label: {label}") 

5006 except AttributeError: 

5007 g.trace('Possible rate limit') 

5008 self.print_header(r) 

5009 g.es_exception() 

5010 break 

5011 total += n 

5012 if done: 

5013 break 

5014 page += 1 

5015 if page > limit: 

5016 g.trace('too many pages') 

5017 break 

5018 #@+node:ekr.20180126044850.1: *5* git.get_issues 

5019 def get_issues(self, base_url: str, label_list: List, milestone: Any, root: Pos, state: Any) -> None: 

5020 """Create a list of issues for each label in label_list.""" 

5021 self.base_url = base_url 

5022 self.milestone = milestone 

5023 self.root = root 

5024 for label in label_list: 

5025 self.get_one_issue(label, state) 

5026 #@+node:ekr.20180126043719.3: *5* git.get_one_issue 

5027 def get_one_issue(self, label: str, state: Any, limit: int=20) -> None: 

5028 """Create a list of issues with the given label.""" 

5029 try: 

5030 import requests 

5031 except Exception: 

5032 g.trace('requests not found: `pip install requests`') 

5033 return 

5034 root = self.root.insertAsLastChild() 

5035 page, total = 1, 0 

5036 page_url = self.base_url + '?labels=%s&state=%s&page=%s' 

5037 while True: 

5038 url = page_url % (label, state, page) 

5039 r = requests.get(url) 

5040 try: 

5041 done, n = self.get_one_page(label, page, r, root) 

5042 # Do not remove this trace. It's reassuring. 

5043 g.trace(f"done: {done:5} page: {page:3} found: {n:3} label: {label}") 

5044 except AttributeError: 

5045 g.trace('Possible rate limit') 

5046 self.print_header(r) 

5047 g.es_exception() 

5048 break 

5049 total += n 

5050 if done: 

5051 break 

5052 page += 1 

5053 if page > limit: 

5054 g.trace('too many pages') 

5055 break 

5056 state = state.capitalize() 

5057 if self.milestone: 

5058 root.h = f"{total} {state} {label} issues for milestone {self.milestone}" 

5059 else: 

5060 root.h = f"{total} {state} {label} issues" 

5061 #@+node:ekr.20180126043719.4: *5* git.get_one_page 

5062 def get_one_page(self, label: str, page: int, r: Any, root: Pos) -> Tuple[bool, int]: 

5063 

5064 if self.milestone: 

5065 aList = [ 

5066 z for z in r.json() 

5067 if z.get('milestone') is not None and 

5068 self.milestone == z.get('milestone').get('title') 

5069 ] 

5070 else: 

5071 aList = [z for z in r.json()] 

5072 for d in aList: 

5073 n, title = d.get('number'), d.get('title') 

5074 html_url = d.get('html_url') or self.base_url 

5075 p = root.insertAsNthChild(0) 

5076 p.h = f"#{n}: {title}" 

5077 p.b = f"{html_url}\n\n" 

5078 p.b += d.get('body').strip() 

5079 link = r.headers.get('Link') 

5080 done = not link or link.find('rel="next"') == -1 

5081 return done, len(aList) 

5082 #@+node:ekr.20180127092201.1: *5* git.print_header 

5083 def print_header(self, r: Any) -> None: 

5084 

5085 # r.headers is a CaseInsensitiveDict 

5086 # so g.printObj(r.headers) is just repr(r.headers) 

5087 if 0: 

5088 print('Link', r.headers.get('Link')) 

5089 else: 

5090 for key in r.headers: 

5091 print(f"{key:35}: {r.headers.get(key)}") 

5092 #@-others 

5093#@+node:ekr.20190428173354.1: *3* g.getGitVersion 

5094def getGitVersion(directory: str=None) -> Tuple[str, str, str]: 

5095 """Return a tuple (author, build, date) from the git log, or None.""" 

5096 # 

5097 # -n: Get only the last log. 

5098 trace = 'git' in g.app.debug 

5099 try: 

5100 s = subprocess.check_output( 

5101 'git log -n 1 --date=iso', 

5102 cwd=directory or g.app.loadDir, 

5103 stderr=subprocess.DEVNULL, 

5104 shell=True, 

5105 ) 

5106 # #1209. 

5107 except subprocess.CalledProcessError as e: 

5108 s = e.output 

5109 if trace: 

5110 g.trace('return code', e.returncode) 

5111 g.trace('value', repr(s)) 

5112 g.es_print('Exception in g.getGitVersion') 

5113 g.es_exception() 

5114 s = g.toUnicode(s) 

5115 if not isinstance(s, str): 

5116 return '', '', '' 

5117 except Exception: 

5118 if trace: 

5119 g.es_print('Exception in g.getGitVersion') 

5120 g.es_exception() 

5121 return '', '', '' 

5122 

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

5124 

5125 def find(kind: str) -> str: 

5126 """Return the given type of log line.""" 

5127 for z in info: 

5128 if z.startswith(kind): 

5129 return z.lstrip(kind).lstrip(':').strip() 

5130 return '' 

5131 

5132 return find('Author'), find('commit')[:10], find('Date') 

5133#@+node:ekr.20170414034616.2: *3* g.gitBranchName 

5134def gitBranchName(path: str=None) -> str: 

5135 """ 

5136 Return the git branch name associated with path/.git, or the empty 

5137 string if path/.git does not exist. If path is None, use the leo-editor 

5138 directory. 

5139 """ 

5140 branch, commit = g.gitInfo(path) 

5141 return branch 

5142#@+node:ekr.20170414034616.4: *3* g.gitCommitNumber 

5143def gitCommitNumber(path: str=None) -> str: 

5144 """ 

5145 Return the git commit number associated with path/.git, or the empty 

5146 string if path/.git does not exist. If path is None, use the leo-editor 

5147 directory. 

5148 """ 

5149 branch, commit = g.gitInfo(path) 

5150 return commit 

5151#@+node:ekr.20200724132432.1: *3* g.gitInfoForFile 

5152def gitInfoForFile(filename: str) -> Tuple[str, str]: 

5153 """ 

5154 Return the git (branch, commit) info associated for the given file. 

5155 """ 

5156 # g.gitInfo and g.gitHeadPath now do all the work. 

5157 return g.gitInfo(filename) 

5158#@+node:ekr.20200724133754.1: *3* g.gitInfoForOutline 

5159def gitInfoForOutline(c: Cmdr) -> Tuple[str, str]: 

5160 """ 

5161 Return the git (branch, commit) info associated for commander c. 

5162 """ 

5163 return g.gitInfoForFile(c.fileName()) 

5164#@+node:maphew.20171112205129.1: *3* g.gitDescribe 

5165def gitDescribe(path: str=None) -> Tuple[str, str, str]: 

5166 """ 

5167 Return the Git tag, distance-from-tag, and commit hash for the 

5168 associated path. If path is None, use the leo-editor directory. 

5169 

5170 Given `git describe` cmd line output: `x-leo-v5.6-55-ge1129da\n` 

5171 This function returns ('x-leo-v5.6', '55', 'e1129da') 

5172 """ 

5173 describe = g.execGitCommand('git describe --tags --long', path) 

5174 # rsplit not split, as '-' might be in tag name. 

5175 tag, distance, commit = describe[0].rsplit('-', 2) 

5176 if 'g' in commit[0:]: 

5177 # leading 'g' isn't part of the commit hash. 

5178 commit = commit[1:] 

5179 commit = commit.rstrip() 

5180 return tag, distance, commit 

5181#@+node:ekr.20170414034616.6: *3* g.gitHeadPath 

5182def gitHeadPath(path_s: str) -> Optional[str]: 

5183 """ 

5184 Compute the path to .git/HEAD given the path. 

5185 """ 

5186 path = Path(path_s) 

5187 # #1780: Look up the directory tree, looking the .git directory. 

5188 while os.path.exists(path): 

5189 head = os.path.join(path, '.git', 'HEAD') 

5190 if os.path.exists(head): 

5191 return head 

5192 if path == path.parent: 

5193 break 

5194 path = path.parent 

5195 return None 

5196#@+node:ekr.20170414034616.3: *3* g.gitInfo 

5197def gitInfo(path: str=None) -> Tuple[str, str]: 

5198 """ 

5199 Path may be a directory or file. 

5200 

5201 Return the branch and commit number or ('', ''). 

5202 """ 

5203 branch, commit = '', '' # Set defaults. 

5204 if path is None: 

5205 # Default to leo/core. 

5206 path = os.path.dirname(__file__) 

5207 if not os.path.isdir(path): 

5208 path = os.path.dirname(path) 

5209 # Does path/../ref exist? 

5210 path = g.gitHeadPath(path) 

5211 if not path: 

5212 return branch, commit 

5213 try: 

5214 with open(path) as f: 

5215 s = f.read() 

5216 if not s.startswith('ref'): 

5217 branch = 'None' 

5218 commit = s[:7] 

5219 return branch, commit 

5220 # On a proper branch 

5221 pointer = s.split()[1] 

5222 dirs = pointer.split('/') 

5223 branch = dirs[-1] 

5224 except IOError: 

5225 g.trace('can not open:', path) 

5226 return branch, commit 

5227 # Try to get a better commit number. 

5228 git_dir = g.os_path_finalize_join(path, '..') 

5229 try: 

5230 path = g.os_path_finalize_join(git_dir, pointer) 

5231 with open(path) as f: # type:ignore 

5232 s = f.read() 

5233 commit = s.strip()[0:12] 

5234 # shorten the hash to a unique shortname 

5235 except IOError: 

5236 try: 

5237 path = g.os_path_finalize_join(git_dir, 'packed-refs') 

5238 with open(path) as f: # type:ignore 

5239 for line in f: 

5240 if line.strip().endswith(' ' + pointer): 

5241 commit = line.split()[0][0:12] 

5242 break 

5243 except IOError: 

5244 pass 

5245 return branch, commit 

5246#@+node:ekr.20031218072017.3139: ** g.Hooks & Plugins 

5247#@+node:ekr.20101028131948.5860: *3* g.act_on_node 

5248def dummy_act_on_node(c: Cmdr, p: Pos, event: Any) -> None: 

5249 pass 

5250 

5251# This dummy definition keeps pylint happy. 

5252# Plugins can change this. 

5253 

5254act_on_node = dummy_act_on_node 

5255#@+node:ville.20120502221057.7500: *3* g.childrenModifiedSet, g.contentModifiedSet 

5256childrenModifiedSet: Set["VNode"] = set() 

5257contentModifiedSet: Set["VNode"] = set() 

5258#@+node:ekr.20031218072017.1596: *3* g.doHook 

5259def doHook(tag: str, *args: Any, **keywords: Any) -> Any: 

5260 """ 

5261 This global function calls a hook routine. Hooks are identified by the 

5262 tag param. 

5263 

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

5265 an exception. 

5266 

5267 We look for a hook routine in three places: 

5268 1. c.hookFunction 

5269 2. app.hookFunction 

5270 3. leoPlugins.doPlugins() 

5271 

5272 Set app.hookError on all exceptions. 

5273 Scripts may reset app.hookError to try again. 

5274 """ 

5275 if g.app.killed or g.app.hookError: 

5276 return None 

5277 if args: 

5278 # A minor error in Leo's core. 

5279 g.pr(f"***ignoring args param. tag = {tag}") 

5280 if not g.app.config.use_plugins: 

5281 if tag in ('open0', 'start1'): 

5282 g.warning("Plugins disabled: use_plugins is 0 in a leoSettings.leo file.") 

5283 return None 

5284 # Get the hook handler function. Usually this is doPlugins. 

5285 c = keywords.get("c") 

5286 # pylint: disable=consider-using-ternary 

5287 f = (c and c.hookFunction) or g.app.hookFunction 

5288 if not f: 

5289 g.app.hookFunction = f = g.app.pluginsController.doPlugins 

5290 try: 

5291 # Pass the hook to the hook handler. 

5292 # g.pr('doHook',f.__name__,keywords.get('c')) 

5293 return f(tag, keywords) 

5294 except Exception: 

5295 g.es_exception() 

5296 g.app.hookError = True # Supress this function. 

5297 g.app.idle_time_hooks_enabled = False 

5298 return None 

5299#@+node:ekr.20100910075900.5950: *3* g.Wrappers for g.app.pluginController methods 

5300# Important: we can not define g.pc here! 

5301#@+node:ekr.20100910075900.5951: *4* g.Loading & registration 

5302def loadOnePlugin(pluginName: str, verbose: bool=False) -> Any: 

5303 pc = g.app.pluginsController 

5304 return pc.loadOnePlugin(pluginName, verbose=verbose) 

5305 

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

5307 pc = g.app.pluginsController 

5308 return pc.registerExclusiveHandler(tags, fn) 

5309 

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

5311 pc = g.app.pluginsController 

5312 return pc.registerHandler(tags, fn) 

5313 

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

5315 pc = g.app.pluginsController 

5316 return pc.plugin_signon(module_name, verbose) 

5317 

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

5319 pc = g.app.pluginsController 

5320 return pc.unloadOnePlugin(moduleOrFileName, verbose) 

5321 

5322def unregisterHandler(tags: Any, fn: Any) -> Any: 

5323 pc = g.app.pluginsController 

5324 return pc.unregisterHandler(tags, fn) 

5325#@+node:ekr.20100910075900.5952: *4* g.Information 

5326def getHandlersForTag(tags: List[str]) -> List: 

5327 pc = g.app.pluginsController 

5328 return pc.getHandlersForTag(tags) 

5329 

5330def getLoadedPlugins() -> List: 

5331 pc = g.app.pluginsController 

5332 return pc.getLoadedPlugins() 

5333 

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

5335 pc = g.app.pluginsController 

5336 return pc.getPluginModule(moduleName) 

5337 

5338def pluginIsLoaded(fn: str) -> bool: 

5339 pc = g.app.pluginsController 

5340 return pc.isLoaded(fn) 

5341#@+node:ekr.20031218072017.1315: ** g.Idle time functions 

5342#@+node:EKR.20040602125018.1: *3* g.disableIdleTimeHook 

5343def disableIdleTimeHook() -> None: 

5344 """Disable the global idle-time hook.""" 

5345 g.app.idle_time_hooks_enabled = False 

5346#@+node:EKR.20040602125018: *3* g.enableIdleTimeHook 

5347def enableIdleTimeHook(*args: Any, **keys: Any) -> None: 

5348 """Enable idle-time processing.""" 

5349 g.app.idle_time_hooks_enabled = True 

5350#@+node:ekr.20140825042850.18410: *3* g.IdleTime 

5351def IdleTime(handler: Any, delay: int=500, tag: str=None) -> Any: 

5352 """ 

5353 A thin wrapper for the LeoQtGui.IdleTime class. 

5354 

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

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

5357 

5358 def handler(timer): 

5359 '''IdleTime handler. timer is an IdleTime instance.''' 

5360 delta_t = timer.time-timer.starting_time 

5361 g.trace(timer.count, '%2.4f' % (delta_t)) 

5362 if timer.count >= 5: 

5363 g.trace('done') 

5364 timer.stop() 

5365 

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

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

5368 if timer: timer.start() 

5369 

5370 Timer instances are completely independent:: 

5371 

5372 def handler1(timer): 

5373 delta_t = timer.time-timer.starting_time 

5374 g.trace('%2s %2.4f' % (timer.count,delta_t)) 

5375 if timer.count >= 5: 

5376 g.trace('done') 

5377 timer.stop() 

5378 

5379 def handler2(timer): 

5380 delta_t = timer.time-timer.starting_time 

5381 g.trace('%2s %2.4f' % (timer.count,delta_t)) 

5382 if timer.count >= 10: 

5383 g.trace('done') 

5384 timer.stop() 

5385 

5386 timer1 = g.IdleTime(handler1, delay=500) 

5387 timer2 = g.IdleTime(handler2, delay=1000) 

5388 if timer1 and timer2: 

5389 timer1.start() 

5390 timer2.start() 

5391 """ 

5392 try: 

5393 return g.app.gui.idleTimeClass(handler, delay, tag) 

5394 except Exception: 

5395 return None 

5396#@+node:ekr.20161027205025.1: *3* g.idleTimeHookHandler (stub) 

5397def idleTimeHookHandler(timer: Any) -> None: 

5398 """This function exists for compatibility.""" 

5399 g.es_print('Replaced by IdleTimeManager.on_idle') 

5400 g.trace(g.callers()) 

5401#@+node:ekr.20041219095213: ** g.Importing 

5402#@+node:ekr.20040917061619: *3* g.cantImport 

5403def cantImport(moduleName: str, pluginName: str=None, verbose: bool=True) -> None: 

5404 """Print a "Can't Import" message and return None.""" 

5405 s = f"Can not import {moduleName}" 

5406 if pluginName: 

5407 s = s + f" from {pluginName}" 

5408 if not g.app or not g.app.gui: 

5409 print(s) 

5410 elif g.unitTesting: 

5411 return 

5412 else: 

5413 g.warning('', s) 

5414#@+node:ekr.20191220044128.1: *3* g.import_module 

5415def import_module(name: str, package: str=None) -> Any: 

5416 """ 

5417 A thin wrapper over importlib.import_module. 

5418 """ 

5419 trace = 'plugins' in g.app.debug and not g.unitTesting 

5420 exceptions = [] 

5421 try: 

5422 m = importlib.import_module(name, package=package) 

5423 except Exception as e: 

5424 m = None 

5425 if trace: 

5426 t, v, tb = sys.exc_info() 

5427 del tb # don't need the traceback 

5428 # In case v is empty, we'll at least have the execption type 

5429 v = v or str(t) # type:ignore 

5430 if v not in exceptions: 

5431 exceptions.append(v) 

5432 g.trace(f"Can not import {name}: {e}") 

5433 return m 

5434#@+node:ekr.20140711071454.17650: ** g.Indices, Strings, Unicode & Whitespace 

5435#@+node:ekr.20140711071454.17647: *3* g.Indices 

5436#@+node:ekr.20050314140957: *4* g.convertPythonIndexToRowCol 

5437def convertPythonIndexToRowCol(s: str, i: int) -> Tuple[int, int]: 

5438 """Convert index i into string s into zero-based row/col indices.""" 

5439 if not s or i <= 0: 

5440 return 0, 0 

5441 i = min(i, len(s)) 

5442 # works regardless of what s[i] is 

5443 row = s.count('\n', 0, i) # Don't include i 

5444 if row == 0: 

5445 return row, i 

5446 prevNL = s.rfind('\n', 0, i) # Don't include i 

5447 return row, i - prevNL - 1 

5448#@+node:ekr.20050315071727: *4* g.convertRowColToPythonIndex 

5449def convertRowColToPythonIndex(s: str, row: int, col: int, lines: List[str]=None) -> int: 

5450 """Convert zero-based row/col indices into a python index into string s.""" 

5451 if row < 0: 

5452 return 0 

5453 if lines is None: 

5454 lines = g.splitLines(s) 

5455 if row >= len(lines): 

5456 return len(s) 

5457 col = min(col, len(lines[row])) 

5458 # A big bottleneck 

5459 prev = 0 

5460 for line in lines[:row]: 

5461 prev += len(line) 

5462 return prev + col 

5463#@+node:ekr.20061031102333.2: *4* g.getWord & getLine 

5464def getWord(s: str, i: int) -> Tuple[int, int]: 

5465 """Return i,j such that s[i:j] is the word surrounding s[i].""" 

5466 if i >= len(s): 

5467 i = len(s) - 1 

5468 if i < 0: 

5469 i = 0 

5470 # Scan backwards. 

5471 while 0 <= i < len(s) and g.isWordChar(s[i]): 

5472 i -= 1 

5473 i += 1 

5474 # Scan forwards. 

5475 j = i 

5476 while 0 <= j < len(s) and g.isWordChar(s[j]): 

5477 j += 1 

5478 return i, j 

5479 

5480def getLine(s: str, i: int) -> Tuple[int, int]: 

5481 """ 

5482 Return i,j such that s[i:j] is the line surrounding s[i]. 

5483 s[i] is a newline only if the line is empty. 

5484 s[j] is a newline unless there is no trailing newline. 

5485 """ 

5486 if i > len(s): 

5487 i = len(s) - 1 

5488 if i < 0: 

5489 i = 0 

5490 # A newline *ends* the line, so look to the left of a newline. 

5491 j = s.rfind('\n', 0, i) 

5492 if j == -1: 

5493 j = 0 

5494 else: 

5495 j += 1 

5496 k = s.find('\n', i) 

5497 if k == -1: 

5498 k = len(s) 

5499 else: 

5500 k = k + 1 

5501 return j, k 

5502#@+node:ekr.20111114151846.9847: *4* g.toPythonIndex 

5503def toPythonIndex(s: str, index: int) -> int: 

5504 """ 

5505 Convert index to a Python int. 

5506 

5507 index may be a Tk index (x.y) or 'end'. 

5508 """ 

5509 if index is None: 

5510 return 0 

5511 if isinstance(index, int): 

5512 return index 

5513 if index == '1.0': 

5514 return 0 

5515 if index == 'end': 

5516 return len(s) 

5517 data = index.split('.') 

5518 if len(data) == 2: 

5519 row, col = data 

5520 row, col = int(row), int(col) 

5521 i = g.convertRowColToPythonIndex(s, row - 1, col) 

5522 return i 

5523 g.trace(f"bad string index: {index}") 

5524 return 0 

5525#@+node:ekr.20140526144610.17601: *3* g.Strings 

5526#@+node:ekr.20190503145501.1: *4* g.isascii 

5527def isascii(s: str) -> bool: 

5528 # s.isascii() is defined in Python 3.7. 

5529 return all(ord(ch) < 128 for ch in s) 

5530#@+node:ekr.20031218072017.3106: *4* g.angleBrackets & virtual_event_name 

5531def angleBrackets(s: str) -> str: 

5532 """Returns < < s > >""" 

5533 lt = "<<" 

5534 rt = ">>" 

5535 return lt + s + rt 

5536 

5537virtual_event_name = angleBrackets 

5538#@+node:ekr.20090516135452.5777: *4* g.ensureLeading/TrailingNewlines 

5539def ensureLeadingNewlines(s: str, n: int) -> str: 

5540 s = g.removeLeading(s, '\t\n\r ') 

5541 return ('\n' * n) + s 

5542 

5543def ensureTrailingNewlines(s: str, n: int) -> str: 

5544 s = g.removeTrailing(s, '\t\n\r ') 

5545 return s + '\n' * n 

5546#@+node:ekr.20050920084036.4: *4* g.longestCommonPrefix & g.itemsMatchingPrefixInList 

5547def longestCommonPrefix(s1: str, s2: str) -> str: 

5548 """Find the longest prefix common to strings s1 and s2.""" 

5549 prefix = '' 

5550 for ch in s1: 

5551 if s2.startswith(prefix + ch): 

5552 prefix = prefix + ch 

5553 else: 

5554 return prefix 

5555 return prefix 

5556 

5557def itemsMatchingPrefixInList(s: str, aList: List[str], matchEmptyPrefix: bool=False) -> Tuple[List, str]: 

5558 """This method returns a sorted list items of aList whose prefix is s. 

5559 

5560 It also returns the longest common prefix of all the matches. 

5561 """ 

5562 if s: 

5563 pmatches = [a for a in aList if a.startswith(s)] 

5564 elif matchEmptyPrefix: 

5565 pmatches = aList[:] 

5566 else: pmatches = [] 

5567 if pmatches: 

5568 pmatches.sort() 

5569 common_prefix = reduce(g.longestCommonPrefix, pmatches) 

5570 else: 

5571 common_prefix = '' 

5572 return pmatches, common_prefix 

5573#@+node:ekr.20090516135452.5776: *4* g.removeLeading/Trailing 

5574# Warning: g.removeTrailingWs already exists. 

5575# Do not change it! 

5576 

5577def removeLeading(s: str, chars: str) -> str: 

5578 """Remove all characters in chars from the front of s.""" 

5579 i = 0 

5580 while i < len(s) and s[i] in chars: 

5581 i += 1 

5582 return s[i:] 

5583 

5584def removeTrailing(s: str, chars: str) -> str: 

5585 """Remove all characters in chars from the end of s.""" 

5586 i = len(s) - 1 

5587 while i >= 0 and s[i] in chars: 

5588 i -= 1 

5589 i += 1 

5590 return s[:i] 

5591#@+node:ekr.20060410112600: *4* g.stripBrackets 

5592def stripBrackets(s: str) -> str: 

5593 """Strip leading and trailing angle brackets.""" 

5594 if s.startswith('<'): 

5595 s = s[1:] 

5596 if s.endswith('>'): 

5597 s = s[:-1] 

5598 return s 

5599#@+node:ekr.20170317101100.1: *4* g.unCamel 

5600def unCamel(s: str) -> List[str]: 

5601 """Return a list of sub-words in camelCased string s.""" 

5602 result: List[str] = [] 

5603 word: List[str] = [] 

5604 for ch in s: 

5605 if ch.isalpha() and ch.isupper(): 

5606 if word: 

5607 result.append(''.join(word)) 

5608 word = [ch] 

5609 elif ch.isalpha(): 

5610 word.append(ch) 

5611 elif word: 

5612 result.append(''.join(word)) 

5613 word = [] 

5614 if word: 

5615 result.append(''.join(word)) 

5616 return result 

5617#@+node:ekr.20031218072017.1498: *3* g.Unicode 

5618#@+node:ekr.20190505052756.1: *4* g.checkUnicode 

5619checkUnicode_dict: Dict[str, bool] = {} 

5620 

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

5622 """ 

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

5624 

5625 This method is meant to document defensive programming. We don't expect 

5626 these errors, but they might arise as the result of problems in 

5627 user-defined plugins or scripts. 

5628 """ 

5629 tag = 'g.checkUnicode' 

5630 if s is None and g.unitTesting: 

5631 return '' 

5632 if isinstance(s, str): 

5633 return s 

5634 if not isinstance(s, bytes): 

5635 g.error(f"{tag}: unexpected argument: {s!r}") 

5636 return '' 

5637 # 

5638 # Report the unexpected conversion. 

5639 callers = g.callers(1) 

5640 if callers not in checkUnicode_dict: 

5641 g.trace(g.callers()) 

5642 g.error(f"\n{tag}: expected unicode. got: {s!r}\n") 

5643 checkUnicode_dict[callers] = True 

5644 # 

5645 # Convert to unicode, reporting all errors. 

5646 if not encoding: 

5647 encoding = 'utf-8' 

5648 try: 

5649 s = s.decode(encoding, 'strict') 

5650 except(UnicodeDecodeError, UnicodeError): 

5651 # https://wiki.python.org/moin/UnicodeDecodeError 

5652 s = s.decode(encoding, 'replace') 

5653 g.trace(g.callers()) 

5654 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}") 

5655 except Exception: 

5656 g.trace(g.callers()) 

5657 g.es_excption() 

5658 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}") 

5659 return s 

5660#@+node:ekr.20100125073206.8709: *4* g.getPythonEncodingFromString 

5661def getPythonEncodingFromString(s: str) -> str: 

5662 """Return the encoding given by Python's encoding line. 

5663 s is the entire file. 

5664 """ 

5665 encoding = None 

5666 tag, tag2 = '# -*- coding:', '-*-' 

5667 n1, n2 = len(tag), len(tag2) 

5668 if s: 

5669 # For Python 3.x we must convert to unicode before calling startswith. 

5670 # The encoding doesn't matter: we only look at the first line, and if 

5671 # the first line is an encoding line, it will contain only ascii characters. 

5672 s = g.toUnicode(s, encoding='ascii', reportErrors=False) 

5673 lines = g.splitLines(s) 

5674 line1 = lines[0].strip() 

5675 if line1.startswith(tag) and line1.endswith(tag2): 

5676 e = line1[n1 : -n2].strip() 

5677 if e and g.isValidEncoding(e): 

5678 encoding = e 

5679 elif g.match_word(line1, 0, '@first'): # 2011/10/21. 

5680 line1 = line1[len('@first') :].strip() 

5681 if line1.startswith(tag) and line1.endswith(tag2): 

5682 e = line1[n1 : -n2].strip() 

5683 if e and g.isValidEncoding(e): 

5684 encoding = e 

5685 return encoding 

5686#@+node:ekr.20031218072017.1500: *4* g.isValidEncoding 

5687def isValidEncoding(encoding: str) -> bool: 

5688 """Return True if the encooding is valid.""" 

5689 if not encoding: 

5690 return False 

5691 if sys.platform == 'cli': 

5692 return True 

5693 try: 

5694 codecs.lookup(encoding) 

5695 return True 

5696 except LookupError: # Windows 

5697 return False 

5698 except AttributeError: # Linux 

5699 return False 

5700 except Exception: 

5701 # UnicodeEncodeError 

5702 g.es_print('Please report the following error') 

5703 g.es_exception() 

5704 return False 

5705#@+node:ekr.20061006152327: *4* g.isWordChar & g.isWordChar1 

5706def isWordChar(ch: str) -> bool: 

5707 """Return True if ch should be considered a letter.""" 

5708 return bool(ch and (ch.isalnum() or ch == '_')) 

5709 

5710def isWordChar1(ch: str) -> bool: 

5711 return bool(ch and (ch.isalpha() or ch == '_')) 

5712#@+node:ekr.20130910044521.11304: *4* g.stripBOM 

5713def stripBOM(s: str) -> Tuple[Optional[str], str]: 

5714 """ 

5715 If there is a BOM, return (e,s2) where e is the encoding 

5716 implied by the BOM and s2 is the s stripped of the BOM. 

5717 

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

5719 

5720 s must be the contents of a file (a string) read in binary mode. 

5721 """ 

5722 table = ( 

5723 # Important: test longer bom's first. 

5724 (4, 'utf-32', codecs.BOM_UTF32_BE), 

5725 (4, 'utf-32', codecs.BOM_UTF32_LE), 

5726 (3, 'utf-8', codecs.BOM_UTF8), 

5727 (2, 'utf-16', codecs.BOM_UTF16_BE), 

5728 (2, 'utf-16', codecs.BOM_UTF16_LE), 

5729 ) 

5730 if s: 

5731 for n, e, bom in table: 

5732 assert len(bom) == n 

5733 if bom == s[: len(bom)]: 

5734 return e, s[len(bom) :] 

5735 return None, s 

5736#@+node:ekr.20050208093800: *4* g.toEncodedString 

5737def toEncodedString(s: str, encoding: str='utf-8', reportErrors: bool=False) -> bytes: 

5738 """Convert unicode string to an encoded string.""" 

5739 if not isinstance(s, str): 

5740 return s 

5741 if not encoding: 

5742 encoding = 'utf-8' 

5743 # These are the only significant calls to s.encode in Leo. 

5744 try: 

5745 s = s.encode(encoding, "strict") # type:ignore 

5746 except UnicodeError: 

5747 s = s.encode(encoding, "replace") # type:ignore 

5748 if reportErrors: 

5749 g.error(f"Error converting {s} from unicode to {encoding} encoding") 

5750 # Tracing these calls directly yields thousands of calls. 

5751 return s # type:ignore 

5752#@+node:ekr.20050208093800.1: *4* g.toUnicode 

5753unicode_warnings: Dict[str, bool] = {} # Keys are g.callers. 

5754 

5755def toUnicode(s: Any, encoding: str=None, reportErrors: bool=False) -> str: 

5756 """Convert bytes to unicode if necessary.""" 

5757 if isinstance(s, str): 

5758 return s 

5759 tag = 'g.toUnicode' 

5760 if not isinstance(s, bytes): 

5761 if not isinstance(s, (NullObject, TracingNullObject)): 

5762 callers = g.callers() 

5763 if callers not in unicode_warnings: 

5764 unicode_warnings[callers] = True 

5765 g.error(f"{tag}: unexpected argument of type {s.__class__.__name__}") 

5766 g.trace(callers) 

5767 return '' 

5768 if not encoding: 

5769 encoding = 'utf-8' 

5770 try: 

5771 s = s.decode(encoding, 'strict') 

5772 except(UnicodeDecodeError, UnicodeError): 

5773 # https://wiki.python.org/moin/UnicodeDecodeError 

5774 s = s.decode(encoding, 'replace') 

5775 if reportErrors: 

5776 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}") 

5777 g.trace(g.callers()) 

5778 except Exception: 

5779 g.es_exception() 

5780 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}") 

5781 g.trace(g.callers()) 

5782 return s 

5783#@+node:ekr.20031218072017.3197: *3* g.Whitespace 

5784#@+node:ekr.20031218072017.3198: *4* g.computeLeadingWhitespace 

5785# Returns optimized whitespace corresponding to width with the indicated tab_width. 

5786 

5787def computeLeadingWhitespace(width: int, tab_width: int) -> str: 

5788 if width <= 0: 

5789 return "" 

5790 if tab_width > 1: 

5791 tabs = int(width / tab_width) 

5792 blanks = int(width % tab_width) 

5793 return ('\t' * tabs) + (' ' * blanks) 

5794 # Negative tab width always gets converted to blanks. 

5795 return ' ' * width 

5796#@+node:ekr.20120605172139.10263: *4* g.computeLeadingWhitespaceWidth 

5797# Returns optimized whitespace corresponding to width with the indicated tab_width. 

5798 

5799def computeLeadingWhitespaceWidth(s: str, tab_width: int) -> int: 

5800 w = 0 

5801 for ch in s: 

5802 if ch == ' ': 

5803 w += 1 

5804 elif ch == '\t': 

5805 w += (abs(tab_width) - (w % abs(tab_width))) 

5806 else: 

5807 break 

5808 return w 

5809#@+node:ekr.20031218072017.3199: *4* g.computeWidth 

5810# Returns the width of s, assuming s starts a line, with indicated tab_width. 

5811 

5812def computeWidth(s: str, tab_width: int) -> int: 

5813 w = 0 

5814 for ch in s: 

5815 if ch == '\t': 

5816 w += (abs(tab_width) - (w % abs(tab_width))) 

5817 elif ch == '\n': # Bug fix: 2012/06/05. 

5818 break 

5819 else: 

5820 w += 1 

5821 return w 

5822#@+node:ekr.20110727091744.15083: *4* g.wrap_lines (newer) 

5823#@@language rest 

5824#@+at 

5825# Important note: this routine need not deal with leading whitespace. 

5826# 

5827# Instead, the caller should simply reduce pageWidth by the width of 

5828# leading whitespace wanted, then add that whitespace to the lines 

5829# returned here. 

5830# 

5831# The key to this code is the invarient that line never ends in whitespace. 

5832#@@c 

5833#@@language python 

5834 

5835def wrap_lines(lines: List[str], pageWidth: int, firstLineWidth: int=None) -> List[str]: 

5836 """Returns a list of lines, consisting of the input lines wrapped to the given pageWidth.""" 

5837 if pageWidth < 10: 

5838 pageWidth = 10 

5839 # First line is special 

5840 if not firstLineWidth: 

5841 firstLineWidth = pageWidth 

5842 if firstLineWidth < 10: 

5843 firstLineWidth = 10 

5844 outputLineWidth = firstLineWidth 

5845 # Sentence spacing 

5846 # This should be determined by some setting, and can only be either 1 or 2 

5847 sentenceSpacingWidth = 1 

5848 assert 0 < sentenceSpacingWidth < 3 

5849 result = [] # The lines of the result. 

5850 line = "" # The line being formed. It never ends in whitespace. 

5851 for s in lines: 

5852 i = 0 

5853 while i < len(s): 

5854 assert len(line) <= outputLineWidth # DTHEIN 18-JAN-2004 

5855 j = g.skip_ws(s, i) 

5856 k = g.skip_non_ws(s, j) 

5857 word = s[j:k] 

5858 assert k > i 

5859 i = k 

5860 # DTHEIN 18-JAN-2004: wrap at exactly the text width, 

5861 # not one character less 

5862 # 

5863 wordLen = len(word) 

5864 if line.endswith('.') or line.endswith('?') or line.endswith('!'): 

5865 space = ' ' * sentenceSpacingWidth 

5866 else: 

5867 space = ' ' 

5868 if line and wordLen > 0: 

5869 wordLen += len(space) 

5870 if wordLen + len(line) <= outputLineWidth: 

5871 if wordLen > 0: 

5872 #@+<< place blank and word on the present line >> 

5873 #@+node:ekr.20110727091744.15084: *5* << place blank and word on the present line >> 

5874 if line: 

5875 # Add the word, preceeded by a blank. 

5876 line = space.join((line, word)) 

5877 else: 

5878 # Just add the word to the start of the line. 

5879 line = word 

5880 #@-<< place blank and word on the present line >> 

5881 else: pass # discard the trailing whitespace. 

5882 else: 

5883 #@+<< place word on a new line >> 

5884 #@+node:ekr.20110727091744.15085: *5* << place word on a new line >> 

5885 # End the previous line. 

5886 if line: 

5887 result.append(line) 

5888 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines 

5889 # Discard the whitespace and put the word on a new line. 

5890 line = word 

5891 # Careful: the word may be longer than pageWidth. 

5892 if len(line) > pageWidth: # DTHEIN 18-JAN-2004: line can equal pagewidth 

5893 result.append(line) 

5894 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines 

5895 line = "" 

5896 #@-<< place word on a new line >> 

5897 if line: 

5898 result.append(line) 

5899 return result 

5900#@+node:ekr.20031218072017.3200: *4* g.get_leading_ws 

5901def get_leading_ws(s: str) -> str: 

5902 """Returns the leading whitespace of 's'.""" 

5903 i = 0 

5904 n = len(s) 

5905 while i < n and s[i] in (' ', '\t'): 

5906 i += 1 

5907 return s[0:i] 

5908#@+node:ekr.20031218072017.3201: *4* g.optimizeLeadingWhitespace 

5909# Optimize leading whitespace in s with the given tab_width. 

5910 

5911def optimizeLeadingWhitespace(line: str, tab_width: int) -> str: 

5912 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width) 

5913 s = g.computeLeadingWhitespace(width, tab_width) + line[i:] 

5914 return s 

5915#@+node:ekr.20040723093558: *4* g.regularizeTrailingNewlines 

5916#@+at The caller should call g.stripBlankLines before calling this routine 

5917# if desired. 

5918# 

5919# This routine does _not_ simply call rstrip(): that would delete all 

5920# trailing whitespace-only lines, and in some cases that would change 

5921# the meaning of program or data. 

5922#@@c 

5923 

5924def regularizeTrailingNewlines(s: str, kind: str) -> None: 

5925 """Kind is 'asis', 'zero' or 'one'.""" 

5926 pass 

5927#@+node:ekr.20091229090857.11698: *4* g.removeBlankLines 

5928def removeBlankLines(s: str) -> str: 

5929 lines = g.splitLines(s) 

5930 lines = [z for z in lines if z.strip()] 

5931 return ''.join(lines) 

5932#@+node:ekr.20091229075924.6235: *4* g.removeLeadingBlankLines 

5933def removeLeadingBlankLines(s: str) -> str: 

5934 lines = g.splitLines(s) 

5935 result = [] 

5936 remove = True 

5937 for line in lines: 

5938 if remove and not line.strip(): 

5939 pass 

5940 else: 

5941 remove = False 

5942 result.append(line) 

5943 return ''.join(result) 

5944#@+node:ekr.20031218072017.3202: *4* g.removeLeadingWhitespace 

5945# Remove whitespace up to first_ws wide in s, given tab_width, the width of a tab. 

5946 

5947def removeLeadingWhitespace(s: str, first_ws: int, tab_width: int) -> str: 

5948 j = 0 

5949 ws = 0 

5950 first_ws = abs(first_ws) 

5951 for ch in s: 

5952 if ws >= first_ws: 

5953 break 

5954 elif ch == ' ': 

5955 j += 1 

5956 ws += 1 

5957 elif ch == '\t': 

5958 j += 1 

5959 ws += (abs(tab_width) - (ws % abs(tab_width))) 

5960 else: 

5961 break 

5962 if j > 0: 

5963 s = s[j:] 

5964 return s 

5965#@+node:ekr.20031218072017.3203: *4* g.removeTrailingWs 

5966# Warning: string.rstrip also removes newlines! 

5967 

5968def removeTrailingWs(s: str) -> str: 

5969 j = len(s) - 1 

5970 while j >= 0 and (s[j] == ' ' or s[j] == '\t'): 

5971 j -= 1 

5972 return s[: j + 1] 

5973#@+node:ekr.20031218072017.3204: *4* g.skip_leading_ws 

5974# Skips leading up to width leading whitespace. 

5975 

5976def skip_leading_ws(s: str, i: int, ws: int, tab_width: int) -> int: 

5977 count = 0 

5978 while count < ws and i < len(s): 

5979 ch = s[i] 

5980 if ch == ' ': 

5981 count += 1 

5982 i += 1 

5983 elif ch == '\t': 

5984 count += (abs(tab_width) - (count % abs(tab_width))) 

5985 i += 1 

5986 else: break 

5987 return i 

5988#@+node:ekr.20031218072017.3205: *4* g.skip_leading_ws_with_indent 

5989def skip_leading_ws_with_indent(s: str, i: int, tab_width: int) -> Tuple[int, int]: 

5990 """Skips leading whitespace and returns (i, indent), 

5991 

5992 - i points after the whitespace 

5993 - indent is the width of the whitespace, assuming tab_width wide tabs.""" 

5994 count = 0 

5995 n = len(s) 

5996 while i < n: 

5997 ch = s[i] 

5998 if ch == ' ': 

5999 count += 1 

6000 i += 1 

6001 elif ch == '\t': 

6002 count += (abs(tab_width) - (count % abs(tab_width))) 

6003 i += 1 

6004 else: break 

6005 return i, count 

6006#@+node:ekr.20040723093558.1: *4* g.stripBlankLines 

6007def stripBlankLines(s: str) -> str: 

6008 lines = g.splitLines(s) 

6009 for i, line in enumerate(lines): 

6010 j = g.skip_ws(line, 0) 

6011 if j >= len(line): 

6012 lines[i] = '' 

6013 elif line[j] == '\n': 

6014 lines[i] = '\n' 

6015 return ''.join(lines) 

6016#@+node:ekr.20031218072017.3108: ** g.Logging & Printing 

6017# g.es and related print to the Log window. 

6018# g.pr prints to the console. 

6019# g.es_print and related print to both the Log window and the console. 

6020#@+node:ekr.20080821073134.2: *3* g.doKeywordArgs 

6021def doKeywordArgs(keys: Dict, d: Dict=None) -> Dict: 

6022 """ 

6023 Return a result dict that is a copy of the keys dict 

6024 with missing items replaced by defaults in d dict. 

6025 """ 

6026 if d is None: 

6027 d = {} 

6028 result = {} 

6029 for key, default_val in d.items(): 

6030 isBool = default_val in (True, False) 

6031 val = keys.get(key) 

6032 if isBool and val in (True, 'True', 'true'): 

6033 result[key] = True 

6034 elif isBool and val in (False, 'False', 'false'): 

6035 result[key] = False 

6036 elif val is None: 

6037 result[key] = default_val 

6038 else: 

6039 result[key] = val 

6040 return result 

6041#@+node:ekr.20031218072017.1474: *3* g.enl, ecnl & ecnls 

6042def ecnl(tabName: str='Log') -> None: 

6043 g.ecnls(1, tabName) 

6044 

6045def ecnls(n: int, tabName: str='Log') -> None: 

6046 log = app.log 

6047 if log and not log.isNull: 

6048 while log.newlines < n: 

6049 g.enl(tabName) 

6050 

6051def enl(tabName: str='Log') -> None: 

6052 log = app.log 

6053 if log and not log.isNull: 

6054 log.newlines += 1 

6055 log.putnl(tabName) 

6056#@+node:ekr.20100914094836.5892: *3* g.error, g.note, g.warning, g.red, g.blue 

6057def blue(*args: Any, **keys: Any) -> None: 

6058 g.es_print(color='blue', *args, **keys) 

6059 

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

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

6062 

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

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

6065 

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

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

6068 

6069def warning(*args: Any, **keys: Any) -> None: 

6070 g.es_print(color='warning', *args, **keys) 

6071#@+node:ekr.20070626132332: *3* g.es 

6072def es(*args: Any, **keys: Any) -> None: 

6073 """Put all non-keyword args to the log pane. 

6074 The first, third, fifth, etc. arg translated by g.translateString. 

6075 Supports color, comma, newline, spaces and tabName keyword arguments. 

6076 """ 

6077 if not app or app.killed: 

6078 return 

6079 if app.gui and app.gui.consoleOnly: 

6080 return 

6081 log = app.log 

6082 # Compute the effective args. 

6083 d = { 

6084 'color': None, 

6085 'commas': False, 

6086 'newline': True, 

6087 'spaces': True, 

6088 'tabName': 'Log', 

6089 'nodeLink': None, 

6090 } 

6091 d = g.doKeywordArgs(keys, d) 

6092 color = d.get('color') 

6093 if color == 'suppress': 

6094 return # New in 4.3. 

6095 color = g.actualColor(color) 

6096 tabName = d.get('tabName') or 'Log' 

6097 newline = d.get('newline') 

6098 s = g.translateArgs(args, d) 

6099 # Do not call g.es, g.es_print, g.pr or g.trace here! 

6100 # sys.__stdout__.write('\n===== g.es: %r\n' % s) 

6101 if app.batchMode: 

6102 if app.log: 

6103 app.log.put(s) 

6104 elif g.unitTesting: 

6105 if log and not log.isNull: 

6106 # This makes the output of unit tests match the output of scripts. 

6107 g.pr(s, newline=newline) 

6108 elif log and app.logInited: 

6109 if newline: 

6110 s += '\n' 

6111 log.put(s, color=color, tabName=tabName, nodeLink=d['nodeLink']) 

6112 # Count the number of *trailing* newlines. 

6113 for ch in s: 

6114 if ch == '\n': 

6115 log.newlines += 1 

6116 else: 

6117 log.newlines = 0 

6118 else: 

6119 app.logWaiting.append((s, color, newline, d),) 

6120 

6121log = es 

6122#@+node:ekr.20060917120951: *3* g.es_dump 

6123def es_dump(s: str, n: int=30, title: str=None) -> None: 

6124 if title: 

6125 g.es_print('', title) 

6126 i = 0 

6127 while i < len(s): 

6128 aList = ''.join([f"{ord(ch):2x} " for ch in s[i : i + n]]) 

6129 g.es_print('', aList) 

6130 i += n 

6131#@+node:ekr.20031218072017.3110: *3* g.es_error & es_print_error 

6132def es_error(*args: Any, **keys: Any) -> None: 

6133 color = keys.get('color') 

6134 if color is None and g.app.config: 

6135 keys['color'] = g.app.config.getColor("log-error-color") or 'red' 

6136 g.es(*args, **keys) 

6137 

6138def es_print_error(*args: Any, **keys: Any) -> None: 

6139 color = keys.get('color') 

6140 if color is None and g.app.config: 

6141 keys['color'] = g.app.config.getColor("log-error-color") or 'red' 

6142 g.es_print(*args, **keys) 

6143#@+node:ekr.20031218072017.3111: *3* g.es_event_exception 

6144def es_event_exception(eventName: str, full: bool=False) -> None: 

6145 g.es("exception handling ", eventName, "event") 

6146 typ, val, tb = sys.exc_info() 

6147 if full: 

6148 errList = traceback.format_exception(typ, val, tb) 

6149 else: 

6150 errList = traceback.format_exception_only(typ, val) 

6151 for i in errList: 

6152 g.es('', i) 

6153 if not g.stdErrIsRedirected(): # 2/16/04 

6154 traceback.print_exc() 

6155#@+node:ekr.20031218072017.3112: *3* g.es_exception 

6156def es_exception(full: bool=True, c: Cmdr=None, color: str="red") -> Tuple[str, int]: 

6157 typ, val, tb = sys.exc_info() 

6158 # val is the second argument to the raise statement. 

6159 if full: 

6160 lines = traceback.format_exception(typ, val, tb) 

6161 else: 

6162 lines = traceback.format_exception_only(typ, val) 

6163 for line in lines: 

6164 g.es_print_error(line, color=color) 

6165 fileName, n = g.getLastTracebackFileAndLineNumber() 

6166 return fileName, n 

6167#@+node:ekr.20061015090538: *3* g.es_exception_type 

6168def es_exception_type(c: Cmdr=None, color: str="red") -> None: 

6169 # exctype is a Exception class object; value is the error message. 

6170 exctype, value = sys.exc_info()[:2] 

6171 g.es_print('', f"{exctype.__name__}, {value}", color=color) # type:ignore 

6172#@+node:ekr.20050707064040: *3* g.es_print 

6173# see: http://www.diveintopython.org/xml_processing/unicode.html 

6174 

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

6176 """ 

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

6178 

6179 The first, third, fifth, etc. arg translated by g.translateString. 

6180 Supports color, comma, newline, spaces and tabName keyword arguments. 

6181 """ 

6182 g.pr(*args, **keys) 

6183 if g.app and not g.unitTesting: 

6184 g.es(*args, **keys) 

6185#@+node:ekr.20111107181638.9741: *3* g.print_exception 

6186def print_exception(full: bool=True, c: Cmdr=None, flush: bool=False, color: str="red") -> Tuple[str, int]: 

6187 """Print exception info about the last exception.""" 

6188 # val is the second argument to the raise statement. 

6189 typ, val, tb = sys.exc_info() 

6190 if full: 

6191 lines = traceback.format_exception(typ, val, tb) 

6192 else: 

6193 lines = traceback.format_exception_only(typ, val) 

6194 print(''.join(lines), flush=flush) 

6195 try: 

6196 fileName, n = g.getLastTracebackFileAndLineNumber() 

6197 return fileName, n 

6198 except Exception: 

6199 return "<no file>", 0 

6200#@+node:ekr.20050707065530: *3* g.es_trace 

6201def es_trace(*args: Any, **keys: Any) -> None: 

6202 if args: 

6203 try: 

6204 s = args[0] 

6205 g.trace(g.toEncodedString(s, 'ascii')) 

6206 except Exception: 

6207 pass 

6208 g.es(*args, **keys) 

6209#@+node:ekr.20040731204831: *3* g.getLastTracebackFileAndLineNumber 

6210def getLastTracebackFileAndLineNumber() -> Tuple[str, int]: 

6211 typ, val, tb = sys.exc_info() 

6212 if typ == SyntaxError: 

6213 # IndentationError is a subclass of SyntaxError. 

6214 return val.filename, val.lineno 

6215 # 

6216 # Data is a list of tuples, one per stack entry. 

6217 # Tupls have the form (filename,lineNumber,functionName,text). 

6218 data = traceback.extract_tb(tb) 

6219 if data: 

6220 item = data[-1] # Get the item at the top of the stack. 

6221 filename, n, functionName, text = item 

6222 return filename, n 

6223 # Should never happen. 

6224 return '<string>', 0 

6225#@+node:ekr.20150621095017.1: *3* g.goto_last_exception 

6226def goto_last_exception(c: Cmdr) -> None: 

6227 """Go to the line given by sys.last_traceback.""" 

6228 typ, val, tb = sys.exc_info() 

6229 if tb: 

6230 file_name, line_number = g.getLastTracebackFileAndLineNumber() 

6231 line_number = max(0, line_number - 1) # Convert to zero-based. 

6232 if file_name.endswith('scriptFile.py'): 

6233 # A script. 

6234 c.goToScriptLineNumber(line_number, c.p) 

6235 else: 

6236 for p in c.all_nodes(): 

6237 if p.isAnyAtFileNode() and p.h.endswith(file_name): 

6238 c.goToLineNumber(line_number) 

6239 return 

6240 else: 

6241 g.trace('No previous exception') 

6242#@+node:ekr.20100126062623.6240: *3* g.internalError 

6243def internalError(*args: Any) -> None: 

6244 """Report a serious interal error in Leo.""" 

6245 callers = g.callers(20).split(',') 

6246 caller = callers[-1] 

6247 g.error('\nInternal Leo error in', caller) 

6248 g.es_print(*args) 

6249 g.es_print('Called from', ', '.join(callers[:-1])) 

6250 g.es_print('Please report this error to Leo\'s developers', color='red') 

6251#@+node:ekr.20150127060254.5: *3* g.log_to_file 

6252def log_to_file(s: str, fn: str=None) -> None: 

6253 """Write a message to ~/test/leo_log.txt.""" 

6254 if fn is None: 

6255 fn = g.os_path_expanduser('~/test/leo_log.txt') 

6256 if not s.endswith('\n'): 

6257 s = s + '\n' 

6258 try: 

6259 with open(fn, 'a') as f: 

6260 f.write(s) 

6261 except Exception: 

6262 g.es_exception() 

6263#@+node:ekr.20080710101653.1: *3* g.pr 

6264# see: http://www.diveintopython.org/xml_processing/unicode.html 

6265 

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

6267 """ 

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

6269 

6270 The first, third, fifth, etc. arg translated by g.translateString. 

6271 Supports color, comma, newline, spaces and tabName keyword arguments. 

6272 """ 

6273 # Compute the effective args. 

6274 d = {'commas': False, 'newline': True, 'spaces': True} 

6275 d = doKeywordArgs(keys, d) 

6276 newline = d.get('newline') 

6277 # Unit tests require sys.stdout. 

6278 stdout = sys.stdout if sys.stdout and g.unitTesting else sys.__stdout__ 

6279 if not stdout: 

6280 # #541. 

6281 return 

6282 if sys.platform.lower().startswith('win'): 

6283 encoding = 'ascii' # 2011/11/9. 

6284 elif getattr(stdout, 'encoding', None): 

6285 # sys.stdout is a TextIOWrapper with a particular encoding. 

6286 encoding = stdout.encoding 

6287 else: 

6288 encoding = 'utf-8' 

6289 s = translateArgs(args, d) # Translates everything to unicode. 

6290 s = g.toUnicode(s, encoding=encoding, reportErrors=False) 

6291 if newline: 

6292 s += '\n' 

6293 # Python's print statement *can* handle unicode, but 

6294 # sitecustomize.py must have sys.setdefaultencoding('utf-8') 

6295 try: 

6296 # #783: print-* commands fail under pythonw. 

6297 stdout.write(s) 

6298 except Exception: 

6299 pass 

6300#@+node:ekr.20060221083356: *3* g.prettyPrintType 

6301def prettyPrintType(obj: Any) -> str: 

6302 if isinstance(obj, str): # type:ignore 

6303 return 'string' 

6304 t: Any = type(obj) 

6305 if t in (types.BuiltinFunctionType, types.FunctionType): 

6306 return 'function' 

6307 if t == types.ModuleType: 

6308 return 'module' 

6309 if t in [types.MethodType, types.BuiltinMethodType]: 

6310 return 'method' 

6311 # Fall back to a hack. 

6312 t = str(type(obj)) # type:ignore 

6313 if t.startswith("<type '"): 

6314 t = t[7:] 

6315 if t.endswith("'>"): 

6316 t = t[:-2] 

6317 return t 

6318#@+node:ekr.20031218072017.3113: *3* g.printBindings 

6319def print_bindings(name: str, window: Any) -> None: 

6320 bindings = window.bind() 

6321 g.pr("\nBindings for", name) 

6322 for b in bindings: 

6323 g.pr(b) 

6324#@+node:ekr.20070510074941: *3* g.printEntireTree 

6325def printEntireTree(c: Cmdr, tag: str='') -> None: 

6326 g.pr('printEntireTree', '=' * 50) 

6327 g.pr('printEntireTree', tag, 'root', c.rootPosition()) 

6328 for p in c.all_positions(): 

6329 g.pr('..' * p.level(), p.v) 

6330#@+node:ekr.20031218072017.3114: *3* g.printGlobals 

6331def printGlobals(message: str=None) -> None: 

6332 # Get the list of globals. 

6333 globs = list(globals()) 

6334 globs.sort() 

6335 # Print the list. 

6336 if message: 

6337 leader = "-" * 10 

6338 g.pr(leader, ' ', message, ' ', leader) 

6339 for name in globs: 

6340 g.pr(name) 

6341#@+node:ekr.20031218072017.3115: *3* g.printLeoModules 

6342def printLeoModules(message: str=None) -> None: 

6343 # Create the list. 

6344 mods = [] 

6345 for name in sys.modules: 

6346 if name and name[0:3] == "leo": 

6347 mods.append(name) 

6348 # Print the list. 

6349 if message: 

6350 leader = "-" * 10 

6351 g.pr(leader, ' ', message, ' ', leader) 

6352 mods.sort() 

6353 for m in mods: 

6354 g.pr(m, newline=False) 

6355 g.pr('') 

6356#@+node:ekr.20041122153823: *3* g.printStack 

6357def printStack() -> None: 

6358 traceback.print_stack() 

6359#@+node:ekr.20031218072017.2317: *3* g.trace 

6360def trace(*args: Any, **keys: Any) -> None: 

6361 """Print a tracing message.""" 

6362 # Don't use g here: in standalone mode g is a NullObject! 

6363 # Compute the effective args. 

6364 d: Dict[str, Any] = {'align': 0, 'before': '', 'newline': True, 'caller_level': 1, 'noname': False} 

6365 d = doKeywordArgs(keys, d) 

6366 newline = d.get('newline') 

6367 align = d.get('align', 0) 

6368 caller_level = d.get('caller_level', 1) 

6369 noname = d.get('noname') 

6370 # Compute the caller name. 

6371 if noname: 

6372 name = '' 

6373 else: 

6374 try: # get the function name from the call stack. 

6375 f1 = sys._getframe(caller_level) # The stack frame, one level up. 

6376 code1 = f1.f_code # The code object 

6377 name = code1.co_name # The code name 

6378 except Exception: 

6379 name = g.shortFileName(__file__) 

6380 if name == '<module>': 

6381 name = g.shortFileName(__file__) 

6382 if name.endswith('.pyc'): 

6383 name = name[:-1] 

6384 # Pad the caller name. 

6385 if align != 0 and len(name) < abs(align): 

6386 pad = ' ' * (abs(align) - len(name)) 

6387 if align > 0: 

6388 name = name + pad 

6389 else: 

6390 name = pad + name 

6391 # Munge *args into s. 

6392 result = [name] if name else [] 

6393 # 

6394 # Put leading newlines into the prefix. 

6395 if isinstance(args, tuple): 

6396 args = list(args) # type:ignore 

6397 if args and isinstance(args[0], str): 

6398 prefix = '' 

6399 while args[0].startswith('\n'): 

6400 prefix += '\n' 

6401 args[0] = args[0][1:] # type:ignore 

6402 else: 

6403 prefix = '' 

6404 for arg in args: 

6405 if isinstance(arg, str): 

6406 pass 

6407 elif isinstance(arg, bytes): 

6408 arg = toUnicode(arg) 

6409 else: 

6410 arg = repr(arg) 

6411 if result: 

6412 result.append(" " + arg) 

6413 else: 

6414 result.append(arg) 

6415 s = d.get('before') + ''.join(result) 

6416 if prefix: 

6417 prefix = prefix[1:] # One less newline. 

6418 pr(prefix) 

6419 pr(s, newline=newline) 

6420#@+node:ekr.20080220111323: *3* g.translateArgs 

6421console_encoding = None 

6422 

6423def translateArgs(args: Iterable[Any], d: Dict[str, Any]) -> str: 

6424 """ 

6425 Return the concatenation of s and all args, with odd args translated. 

6426 """ 

6427 global console_encoding 

6428 if not console_encoding: 

6429 e = sys.getdefaultencoding() 

6430 console_encoding = e if isValidEncoding(e) else 'utf-8' 

6431 # print 'translateArgs',console_encoding 

6432 result: List[str] = [] 

6433 n, spaces = 0, d.get('spaces') 

6434 for arg in args: 

6435 n += 1 

6436 # First, convert to unicode. 

6437 if isinstance(arg, str): 

6438 arg = toUnicode(arg, console_encoding) 

6439 # Now translate. 

6440 if not isinstance(arg, str): 

6441 arg = repr(arg) 

6442 elif (n % 2) == 1: 

6443 arg = translateString(arg) 

6444 else: 

6445 pass # The arg is an untranslated string. 

6446 if arg: 

6447 if result and spaces: 

6448 result.append(' ') 

6449 result.append(arg) 

6450 return ''.join(result) 

6451#@+node:ekr.20060810095921: *3* g.translateString & tr 

6452def translateString(s: str) -> str: 

6453 """Return the translated text of s.""" 

6454 # pylint: disable=undefined-loop-variable 

6455 # looks like a pylint bug 

6456 upper = app and getattr(app, 'translateToUpperCase', None) 

6457 if not isinstance(s, str): 

6458 s = str(s, 'utf-8') 

6459 if upper: 

6460 s = s.upper() 

6461 else: 

6462 s = gettext.gettext(s) 

6463 return s 

6464 

6465tr = translateString 

6466#@+node:EKR.20040612114220: ** g.Miscellaneous 

6467#@+node:ekr.20120928142052.10116: *3* g.actualColor 

6468def actualColor(color: str) -> str: 

6469 """Return the actual color corresponding to the requested color.""" 

6470 c = g.app.log and g.app.log.c 

6471 # Careful: c.config may not yet exist. 

6472 if not c or not c.config: 

6473 return color 

6474 # Don't change absolute colors. 

6475 if color and color.startswith('#'): 

6476 return color 

6477 # #788: Translate colors to theme-defined colors. 

6478 if color is None: 

6479 # Prefer text_foreground_color' 

6480 color2 = c.config.getColor('log-text-foreground-color') 

6481 if color2: 

6482 return color2 

6483 # Fall back to log_black_color. 

6484 color2 = c.config.getColor('log-black-color') 

6485 return color2 or 'black' 

6486 if color == 'black': 

6487 # Prefer log_black_color. 

6488 color2 = c.config.getColor('log-black-color') 

6489 if color2: 

6490 return color2 

6491 # Fall back to log_text_foreground_color. 

6492 color2 = c.config.getColor('log-text-foreground-color') 

6493 return color2 or 'black' 

6494 color2 = c.config.getColor(f"log_{color}_color") 

6495 return color2 or color 

6496#@+node:ekr.20060921100435: *3* g.CheckVersion & helpers 

6497# Simplified version by EKR: stringCompare not used. 

6498 

6499def CheckVersion( 

6500 s1: str, 

6501 s2: str, 

6502 condition: str=">=", 

6503 stringCompare: bool=None, 

6504 delimiter: str='.', 

6505 trace: bool=False, 

6506) -> bool: 

6507 # CheckVersion is called early in the startup process. 

6508 vals1 = [g.CheckVersionToInt(s) for s in s1.split(delimiter)] 

6509 n1 = len(vals1) 

6510 vals2 = [g.CheckVersionToInt(s) for s in s2.split(delimiter)] 

6511 n2 = len(vals2) 

6512 n = max(n1, n2) 

6513 if n1 < n: 

6514 vals1.extend([0 for i in range(n - n1)]) 

6515 if n2 < n: 

6516 vals2.extend([0 for i in range(n - n2)]) 

6517 for cond, val in ( 

6518 ('==', vals1 == vals2), ('!=', vals1 != vals2), 

6519 ('<', vals1 < vals2), ('<=', vals1 <= vals2), 

6520 ('>', vals1 > vals2), ('>=', vals1 >= vals2), 

6521 ): 

6522 if condition == cond: 

6523 result = val 

6524 break 

6525 else: 

6526 raise EnvironmentError( 

6527 "condition must be one of '>=', '>', '==', '!=', '<', or '<='.") 

6528 return result 

6529#@+node:ekr.20070120123930: *4* g.CheckVersionToInt 

6530def CheckVersionToInt(s: str) -> int: 

6531 try: 

6532 return int(s) 

6533 except ValueError: 

6534 aList = [] 

6535 for ch in s: 

6536 if ch.isdigit(): 

6537 aList.append(ch) 

6538 else: 

6539 break 

6540 if aList: 

6541 s = ''.join(aList) 

6542 return int(s) 

6543 return 0 

6544#@+node:ekr.20111103205308.9657: *3* g.cls 

6545@command('cls') 

6546def cls(event: Any=None) -> None: 

6547 """Clear the screen.""" 

6548 if sys.platform.lower().startswith('win'): 

6549 os.system('cls') 

6550#@+node:ekr.20131114124839.16665: *3* g.createScratchCommander 

6551def createScratchCommander(fileName: str=None) -> None: 

6552 c = g.app.newCommander(fileName) 

6553 frame = c.frame 

6554 frame.createFirstTreeNode() 

6555 assert c.rootPosition() 

6556 frame.setInitialWindowGeometry() 

6557 frame.resizePanesToRatio(frame.ratio, frame.secondary_ratio) 

6558#@+node:ekr.20031218072017.3126: *3* g.funcToMethod (Python Cookbook) 

6559def funcToMethod(f: Any, theClass: Any, name: str=None) -> None: 

6560 """ 

6561 From the Python Cookbook... 

6562 

6563 The following method allows you to add a function as a method of 

6564 any class. That is, it converts the function to a method of the 

6565 class. The method just added is available instantly to all 

6566 existing instances of the class, and to all instances created in 

6567 the future. 

6568 

6569 The function's first argument should be self. 

6570 

6571 The newly created method has the same name as the function unless 

6572 the optional name argument is supplied, in which case that name is 

6573 used as the method name. 

6574 """ 

6575 setattr(theClass, name or f.__name__, f) 

6576#@+node:ekr.20060913090832.1: *3* g.init_zodb 

6577init_zodb_import_failed = False 

6578init_zodb_failed: Dict[str, bool] = {} # Keys are paths, values are True. 

6579init_zodb_db: Dict[str, Any] = {} # Keys are paths, values are ZODB.DB instances. 

6580 

6581def init_zodb(pathToZodbStorage: str, verbose: bool=True) -> Any: 

6582 """ 

6583 Return an ZODB.DB instance from the given path. 

6584 return None on any error. 

6585 """ 

6586 global init_zodb_db, init_zodb_failed, init_zodb_import_failed 

6587 db = init_zodb_db.get(pathToZodbStorage) 

6588 if db: 

6589 return db 

6590 if init_zodb_import_failed: 

6591 return None 

6592 failed = init_zodb_failed.get(pathToZodbStorage) 

6593 if failed: 

6594 return None 

6595 try: 

6596 import ZODB # type:ignore 

6597 except ImportError: 

6598 if verbose: 

6599 g.es('g.init_zodb: can not import ZODB') 

6600 g.es_exception() 

6601 init_zodb_import_failed = True 

6602 return None 

6603 try: 

6604 storage = ZODB.FileStorage.FileStorage(pathToZodbStorage) 

6605 init_zodb_db[pathToZodbStorage] = db = ZODB.DB(storage) 

6606 return db 

6607 except Exception: 

6608 if verbose: 

6609 g.es('g.init_zodb: exception creating ZODB.DB instance') 

6610 g.es_exception() 

6611 init_zodb_failed[pathToZodbStorage] = True 

6612 return None 

6613#@+node:ekr.20170206080908.1: *3* g.input_ 

6614def input_(message: str='', c: Cmdr=None) -> str: 

6615 """ 

6616 Safely execute python's input statement. 

6617 

6618 c.executeScriptHelper binds 'input' to be a wrapper that calls g.input_ 

6619 with c and handler bound properly. 

6620 """ 

6621 if app.gui.isNullGui: 

6622 return '' 

6623 # Prompt for input from the console, assuming there is one. 

6624 # pylint: disable=no-member 

6625 from leo.core.leoQt import QtCore 

6626 QtCore.pyqtRemoveInputHook() 

6627 return input(message) 

6628#@+node:ekr.20110609125359.16493: *3* g.isMacOS 

6629def isMacOS() -> bool: 

6630 return sys.platform == 'darwin' 

6631#@+node:ekr.20181027133311.1: *3* g.issueSecurityWarning 

6632def issueSecurityWarning(setting: str) -> None: 

6633 g.es('Security warning! Ignoring...', color='red') 

6634 g.es(setting, color='red') 

6635 g.es('This setting can be set only in') 

6636 g.es('leoSettings.leo or myLeoSettings.leo') 

6637#@+node:ekr.20031218072017.3144: *3* g.makeDict (Python Cookbook) 

6638# From the Python cookbook. 

6639 

6640def makeDict(**keys: Any) -> Dict: 

6641 """Returns a Python dictionary from using the optional keyword arguments.""" 

6642 return keys 

6643#@+node:ekr.20140528065727.17963: *3* g.pep8_class_name 

6644def pep8_class_name(s: str) -> str: 

6645 """Return the proper class name for s.""" 

6646 # Warning: s.capitalize() does not work. 

6647 # It lower cases all but the first letter! 

6648 return ''.join([z[0].upper() + z[1:] for z in s.split('_') if z]) 

6649 

6650if 0: # Testing: 

6651 cls() 

6652 aList = ( 

6653 '_', 

6654 '__', 

6655 '_abc', 

6656 'abc_', 

6657 'abc', 

6658 'abc_xyz', 

6659 'AbcPdQ', 

6660 ) 

6661 for s in aList: 

6662 print(pep8_class_name(s)) 

6663#@+node:ekr.20160417174224.1: *3* g.plural 

6664def plural(obj: Any) -> str: 

6665 """Return "s" or "" depending on n.""" 

6666 if isinstance(obj, (list, tuple, str)): 

6667 n = len(obj) 

6668 else: 

6669 n = obj 

6670 return '' if n == 1 else 's' 

6671#@+node:ekr.20160331194701.1: *3* g.truncate 

6672def truncate(s: str, n: int) -> str: 

6673 """Return s truncated to n characters.""" 

6674 if len(s) <= n: 

6675 return s 

6676 # Fail: weird ws. 

6677 s2 = s[: n - 3] + f"...({len(s)})" 

6678 if s.endswith('\n'): 

6679 return s2 + '\n' 

6680 return s2 

6681#@+node:ekr.20031218072017.3150: *3* g.windows 

6682def windows() -> Optional[List]: 

6683 return app and app.windowList 

6684#@+node:ekr.20031218072017.2145: ** g.os_path_ Wrappers 

6685#@+at Note: all these methods return Unicode strings. It is up to the user to 

6686# convert to an encoded string as needed, say when opening a file. 

6687#@+node:ekr.20180314120442.1: *3* g.glob_glob 

6688def glob_glob(pattern: str) -> List: 

6689 """Return the regularized glob.glob(pattern)""" 

6690 aList = glob.glob(pattern) 

6691 # os.path.normpath does the *reverse* of what we want. 

6692 if g.isWindows: 

6693 aList = [z.replace('\\', '/') for z in aList] 

6694 return aList 

6695#@+node:ekr.20031218072017.2146: *3* g.os_path_abspath 

6696def os_path_abspath(path: str) -> str: 

6697 """Convert a path to an absolute path.""" 

6698 if not path: 

6699 return '' 

6700 if '\x00' in path: 

6701 g.trace('NULL in', repr(path), g.callers()) 

6702 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6703 path = os.path.abspath(path) 

6704 # os.path.normpath does the *reverse* of what we want. 

6705 if g.isWindows: 

6706 path = path.replace('\\', '/') 

6707 return path 

6708#@+node:ekr.20031218072017.2147: *3* g.os_path_basename 

6709def os_path_basename(path: str) -> str: 

6710 """Return the second half of the pair returned by split(path).""" 

6711 if not path: 

6712 return '' 

6713 path = os.path.basename(path) 

6714 # os.path.normpath does the *reverse* of what we want. 

6715 if g.isWindows: 

6716 path = path.replace('\\', '/') 

6717 return path 

6718#@+node:ekr.20031218072017.2148: *3* g.os_path_dirname 

6719def os_path_dirname(path: str) -> str: 

6720 """Return the first half of the pair returned by split(path).""" 

6721 if not path: 

6722 return '' 

6723 path = os.path.dirname(path) 

6724 # os.path.normpath does the *reverse* of what we want. 

6725 if g.isWindows: 

6726 path = path.replace('\\', '/') 

6727 return path 

6728#@+node:ekr.20031218072017.2149: *3* g.os_path_exists 

6729def os_path_exists(path: str) -> bool: 

6730 """Return True if path exists.""" 

6731 if not path: 

6732 return False 

6733 if '\x00' in path: 

6734 g.trace('NULL in', repr(path), g.callers()) 

6735 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6736 return os.path.exists(path) 

6737#@+node:ekr.20080921060401.13: *3* g.os_path_expanduser 

6738def os_path_expanduser(path: str) -> str: 

6739 """wrap os.path.expanduser""" 

6740 if not path: 

6741 return '' 

6742 result = os.path.normpath(os.path.expanduser(path)) 

6743 # os.path.normpath does the *reverse* of what we want. 

6744 if g.isWindows: 

6745 path = path.replace('\\', '/') 

6746 return result 

6747#@+node:ekr.20080921060401.14: *3* g.os_path_finalize 

6748def os_path_finalize(path: str) -> str: 

6749 """ 

6750 Expand '~', then return os.path.normpath, os.path.abspath of the path. 

6751 There is no corresponding os.path method 

6752 """ 

6753 if '\x00' in path: 

6754 g.trace('NULL in', repr(path), g.callers()) 

6755 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6756 path = os.path.expanduser(path) # #1383. 

6757 path = os.path.abspath(path) 

6758 path = os.path.normpath(path) 

6759 # os.path.normpath does the *reverse* of what we want. 

6760 if g.isWindows: 

6761 path = path.replace('\\', '/') 

6762 # calling os.path.realpath here would cause problems in some situations. 

6763 return path 

6764#@+node:ekr.20140917154740.19483: *3* g.os_path_finalize_join 

6765def os_path_finalize_join(*args: Any, **keys: Any) -> str: 

6766 """ 

6767 Join and finalize. 

6768 

6769 **keys may contain a 'c' kwarg, used by g.os_path_join. 

6770 """ 

6771 path = g.os_path_join(*args, **keys) 

6772 path = g.os_path_finalize(path) 

6773 return path 

6774#@+node:ekr.20031218072017.2150: *3* g.os_path_getmtime 

6775def os_path_getmtime(path: str) -> float: 

6776 """Return the modification time of path.""" 

6777 if not path: 

6778 return 0 

6779 try: 

6780 return os.path.getmtime(path) 

6781 except Exception: 

6782 return 0 

6783#@+node:ekr.20080729142651.2: *3* g.os_path_getsize 

6784def os_path_getsize(path: str) -> int: 

6785 """Return the size of path.""" 

6786 return os.path.getsize(path) if path else 0 

6787#@+node:ekr.20031218072017.2151: *3* g.os_path_isabs 

6788def os_path_isabs(path: str) -> bool: 

6789 """Return True if path is an absolute path.""" 

6790 return os.path.isabs(path) if path else False 

6791#@+node:ekr.20031218072017.2152: *3* g.os_path_isdir 

6792def os_path_isdir(path: str) -> bool: 

6793 """Return True if the path is a directory.""" 

6794 return os.path.isdir(path) if path else False 

6795#@+node:ekr.20031218072017.2153: *3* g.os_path_isfile 

6796def os_path_isfile(path: str) -> bool: 

6797 """Return True if path is a file.""" 

6798 return os.path.isfile(path) if path else False 

6799#@+node:ekr.20031218072017.2154: *3* g.os_path_join 

6800def os_path_join(*args: Any, **keys: Any) -> str: 

6801 """ 

6802 Join paths, like os.path.join, with enhancements: 

6803 

6804 A '!!' arg prepends g.app.loadDir to the list of paths. 

6805 A '.' arg prepends c.openDirectory to the list of paths, 

6806 provided there is a 'c' kwarg. 

6807 """ 

6808 c = keys.get('c') 

6809 uargs = [z for z in args if z] 

6810 if not uargs: 

6811 return '' 

6812 # Note: This is exactly the same convention as used by getBaseDirectory. 

6813 if uargs[0] == '!!': 

6814 uargs[0] = g.app.loadDir 

6815 elif uargs[0] == '.': 

6816 c = keys.get('c') 

6817 if c and c.openDirectory: 

6818 uargs[0] = c.openDirectory 

6819 try: 

6820 path = os.path.join(*uargs) 

6821 except TypeError: 

6822 g.trace(uargs, args, keys, g.callers()) 

6823 raise 

6824 # May not be needed on some Pythons. 

6825 if '\x00' in path: 

6826 g.trace('NULL in', repr(path), g.callers()) 

6827 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6828 # os.path.normpath does the *reverse* of what we want. 

6829 if g.isWindows: 

6830 path = path.replace('\\', '/') 

6831 return path 

6832#@+node:ekr.20031218072017.2156: *3* g.os_path_normcase 

6833def os_path_normcase(path: str) -> str: 

6834 """Normalize the path's case.""" 

6835 if not path: 

6836 return '' 

6837 path = os.path.normcase(path) 

6838 if g.isWindows: 

6839 path = path.replace('\\', '/') 

6840 return path 

6841#@+node:ekr.20031218072017.2157: *3* g.os_path_normpath 

6842def os_path_normpath(path: str) -> str: 

6843 """Normalize the path.""" 

6844 if not path: 

6845 return '' 

6846 path = os.path.normpath(path) 

6847 # os.path.normpath does the *reverse* of what we want. 

6848 if g.isWindows: 

6849 path = path.replace('\\', '/').lower() # #2049: ignore case! 

6850 return path 

6851#@+node:ekr.20180314081254.1: *3* g.os_path_normslashes 

6852def os_path_normslashes(path: str) -> str: 

6853 

6854 # os.path.normpath does the *reverse* of what we want. 

6855 if g.isWindows and path: 

6856 path = path.replace('\\', '/') 

6857 return path 

6858#@+node:ekr.20080605064555.2: *3* g.os_path_realpath 

6859def os_path_realpath(path: str) -> str: 

6860 """Return the canonical path of the specified filename, eliminating any 

6861 symbolic links encountered in the path (if they are supported by the 

6862 operating system). 

6863 """ 

6864 if not path: 

6865 return '' 

6866 path = os.path.realpath(path) 

6867 # os.path.normpath does the *reverse* of what we want. 

6868 if g.isWindows: 

6869 path = path.replace('\\', '/') 

6870 return path 

6871#@+node:ekr.20031218072017.2158: *3* g.os_path_split 

6872def os_path_split(path: str) -> Tuple[str, str]: 

6873 if not path: 

6874 return '', '' 

6875 head, tail = os.path.split(path) 

6876 return head, tail 

6877#@+node:ekr.20031218072017.2159: *3* g.os_path_splitext 

6878def os_path_splitext(path: str) -> Tuple[str, str]: 

6879 

6880 if not path: 

6881 return '', '' 

6882 head, tail = os.path.splitext(path) 

6883 return head, tail 

6884#@+node:ekr.20090829140232.6036: *3* g.os_startfile 

6885def os_startfile(fname: str) -> None: 

6886 #@+others 

6887 #@+node:bob.20170516112250.1: *4* stderr2log() 

6888 def stderr2log(g: Any, ree: Any, fname: str) -> None: 

6889 """ Display stderr output in the Leo-Editor log pane 

6890 

6891 Arguments: 

6892 g: Leo-Editor globals 

6893 ree: Read file descriptor for stderr 

6894 fname: file pathname 

6895 

6896 Returns: 

6897 None 

6898 """ 

6899 

6900 while True: 

6901 emsg = ree.read().decode('utf-8') 

6902 if emsg: 

6903 g.es_print_error(f"xdg-open {fname} caused output to stderr:\n{emsg}") 

6904 else: 

6905 break 

6906 #@+node:bob.20170516112304.1: *4* itPoll() 

6907 def itPoll(fname: str, ree: Any, subPopen: Any, g: Any, ito: Any) -> None: 

6908 """ Poll for subprocess done 

6909 

6910 Arguments: 

6911 fname: File name 

6912 ree: stderr read file descriptor 

6913 subPopen: URL open subprocess object 

6914 g: Leo-Editor globals 

6915 ito: Idle time object for itPoll() 

6916 

6917 Returns: 

6918 None 

6919 """ 

6920 

6921 stderr2log(g, ree, fname) 

6922 rc = subPopen.poll() 

6923 if not rc is None: 

6924 ito.stop() 

6925 ito.destroy_self() 

6926 if rc != 0: 

6927 g.es_print(f"xdg-open {fname} failed with exit code {rc}") 

6928 stderr2log(g, ree, fname) 

6929 ree.close() 

6930 #@-others 

6931 if fname.find('"') > -1: 

6932 quoted_fname = f"'{fname}'" 

6933 else: 

6934 quoted_fname = f'"{fname}"' 

6935 if sys.platform.startswith('win'): 

6936 # pylint: disable=no-member 

6937 os.startfile(quoted_fname) 

6938 # Exists only on Windows. 

6939 elif sys.platform == 'darwin': 

6940 # From Marc-Antoine Parent. 

6941 try: 

6942 # Fix bug 1226358: File URL's are broken on MacOS: 

6943 # use fname, not quoted_fname, as the argument to subprocess.call. 

6944 subprocess.call(['open', fname]) 

6945 except OSError: 

6946 pass # There may be a spurious "Interrupted system call" 

6947 except ImportError: 

6948 os.system(f"open {quoted_fname}") 

6949 else: 

6950 try: 

6951 ree = None 

6952 wre = tempfile.NamedTemporaryFile() 

6953 ree = io.open(wre.name, 'rb', buffering=0) 

6954 except IOError: 

6955 g.trace(f"error opening temp file for {fname!r}") 

6956 if ree: 

6957 ree.close() 

6958 return 

6959 try: 

6960 subPopen = subprocess.Popen(['xdg-open', fname], stderr=wre, shell=False) 

6961 except Exception: 

6962 g.es_print(f"error opening {fname!r}") 

6963 g.es_exception() 

6964 try: 

6965 itoPoll = g.IdleTime( 

6966 (lambda ito: itPoll(fname, ree, subPopen, g, ito)), 

6967 delay=1000, 

6968 ) 

6969 itoPoll.start() 

6970 # Let the Leo-Editor process run 

6971 # so that Leo-Editor is usable while the file is open. 

6972 except Exception: 

6973 g.es_exception(f"exception executing g.startfile for {fname!r}") 

6974#@+node:ekr.20111115155710.9859: ** g.Parsing & Tokenizing 

6975#@+node:ekr.20031218072017.822: *3* g.createTopologyList 

6976def createTopologyList(c: Cmdr, root: Pos=None, useHeadlines: bool=False) -> List: 

6977 """Creates a list describing a node and all its descendents""" 

6978 if not root: 

6979 root = c.rootPosition() 

6980 v = root 

6981 if useHeadlines: 

6982 aList = [(v.numberOfChildren(), v.headString()),] # type: ignore 

6983 else: 

6984 aList = [v.numberOfChildren()] # type: ignore 

6985 child = v.firstChild() 

6986 while child: 

6987 aList.append(g.createTopologyList(c, child, useHeadlines)) # type: ignore 

6988 child = child.next() 

6989 return aList 

6990#@+node:ekr.20111017204736.15898: *3* g.getDocString 

6991def getDocString(s: str) -> str: 

6992 """Return the text of the first docstring found in s.""" 

6993 tags = ('"""', "'''") 

6994 tag1, tag2 = tags 

6995 i1, i2 = s.find(tag1), s.find(tag2) 

6996 if i1 == -1 and i2 == -1: 

6997 return '' 

6998 if i1 > -1 and i2 > -1: 

6999 i = min(i1, i2) 

7000 else: 

7001 i = max(i1, i2) 

7002 tag = s[i : i + 3] 

7003 assert tag in tags 

7004 j = s.find(tag, i + 3) 

7005 if j > -1: 

7006 return s[i + 3 : j] 

7007 return '' 

7008#@+node:ekr.20111017211256.15905: *3* g.getDocStringForFunction 

7009def getDocStringForFunction(func: Any) -> str: 

7010 """Return the docstring for a function that creates a Leo command.""" 

7011 

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

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

7014 

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

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

7017 return defaults[i] 

7018 

7019 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252 

7020 # Minibuffer commands created by mod_scripting.py have no docstrings. 

7021 # Do special cases first. 

7022 

7023 s = '' 

7024 if name(func) == 'minibufferCallback': 

7025 func = get_defaults(func, 0) 

7026 if hasattr(func, 'func.__doc__') and func.__doc__.strip(): 

7027 s = func.__doc__ 

7028 if not s and name(func) == 'commonCommandCallback': 

7029 script = get_defaults(func, 1) 

7030 s = g.getDocString(script) # Do a text scan for the function. 

7031 # Now the general cases. Prefer __doc__ to docstring() 

7032 if not s and hasattr(func, '__doc__'): 

7033 s = func.__doc__ 

7034 if not s and hasattr(func, 'docstring'): 

7035 s = func.docstring 

7036 return s 

7037#@+node:ekr.20111115155710.9814: *3* g.python_tokenize (not used) 

7038def python_tokenize(s: str) -> List: 

7039 """ 

7040 Tokenize string s and return a list of tokens (kind, value, line_number) 

7041 

7042 where kind is in ('comment,'id','nl','other','string','ws'). 

7043 """ 

7044 result: List[Tuple[str, str, int]] = [] 

7045 i, line_number = 0, 0 

7046 while i < len(s): 

7047 progress = j = i 

7048 ch = s[i] 

7049 if ch == '\n': 

7050 kind, i = 'nl', i + 1 

7051 elif ch in ' \t': 

7052 kind = 'ws' 

7053 while i < len(s) and s[i] in ' \t': 

7054 i += 1 

7055 elif ch == '#': 

7056 kind, i = 'comment', g.skip_to_end_of_line(s, i) 

7057 elif ch in '"\'': 

7058 kind, i = 'string', g.skip_python_string(s, i) 

7059 elif ch == '_' or ch.isalpha(): 

7060 kind, i = 'id', g.skip_id(s, i) 

7061 else: 

7062 kind, i = 'other', i + 1 

7063 assert progress < i and j == progress 

7064 val = s[j:i] 

7065 assert val 

7066 line_number += val.count('\n') # A comment. 

7067 result.append((kind, val, line_number),) 

7068 return result 

7069#@+node:ekr.20040327103735.2: ** g.Scripting 

7070#@+node:ekr.20161223090721.1: *3* g.exec_file 

7071def exec_file(path: str, d: Dict[str, str], script: str=None) -> None: 

7072 """Simulate python's execfile statement for python 3.""" 

7073 if script is None: 

7074 with open(path) as f: 

7075 script = f.read() 

7076 exec(compile(script, path, 'exec'), d) 

7077#@+node:ekr.20131016032805.16721: *3* g.execute_shell_commands 

7078def execute_shell_commands(commands: Any, trace: bool=False) -> None: 

7079 """ 

7080 Execute each shell command in a separate process. 

7081 Wait for each command to complete, except those starting with '&' 

7082 """ 

7083 if isinstance(commands, str): 

7084 commands = [commands] 

7085 for command in commands: 

7086 wait = not command.startswith('&') 

7087 if trace: 

7088 g.trace(command) 

7089 if command.startswith('&'): 

7090 command = command[1:].strip() 

7091 proc = subprocess.Popen(command, shell=True) 

7092 if wait: 

7093 proc.communicate() 

7094 else: 

7095 if trace: 

7096 print('Start:', proc) 

7097 # #1489: call proc.poll at idle time. 

7098 

7099 def proc_poller(timer: Any, proc: Any=proc) -> None: 

7100 val = proc.poll() 

7101 if val is not None: 

7102 # This trace can be disruptive. 

7103 if trace: 

7104 print(' End:', proc, val) 

7105 timer.stop() 

7106 

7107 g.IdleTime(proc_poller, delay=0).start() 

7108#@+node:ekr.20180217113719.1: *3* g.execute_shell_commands_with_options & helpers 

7109def execute_shell_commands_with_options( 

7110 base_dir: str=None, 

7111 c: Cmdr=None, 

7112 command_setting: str=None, 

7113 commands: List=None, 

7114 path_setting: str=None, 

7115 trace: bool=False, 

7116 warning: str=None, 

7117) -> None: 

7118 """ 

7119 A helper for prototype commands or any other code that 

7120 runs programs in a separate process. 

7121 

7122 base_dir: Base directory to use if no config path given. 

7123 commands: A list of commands, for g.execute_shell_commands. 

7124 commands_setting: Name of @data setting for commands. 

7125 path_setting: Name of @string setting for the base directory. 

7126 warning: A warning to be printed before executing the commands. 

7127 """ 

7128 base_dir = g.computeBaseDir(c, base_dir, path_setting, trace) 

7129 if not base_dir: 

7130 return 

7131 commands = g.computeCommands(c, commands, command_setting, trace) 

7132 if not commands: 

7133 return 

7134 if warning: 

7135 g.es_print(warning) 

7136 os.chdir(base_dir) # Can't do this in the commands list. 

7137 g.execute_shell_commands(commands) 

7138#@+node:ekr.20180217152624.1: *4* g.computeBaseDir 

7139def computeBaseDir(c: Cmdr, base_dir: str, path_setting: str, trace: bool=False) -> Optional[str]: 

7140 """ 

7141 Compute a base_directory. 

7142 If given, @string path_setting takes precedence. 

7143 """ 

7144 # Prefer the path setting to the base_dir argument. 

7145 if path_setting: 

7146 if not c: 

7147 g.es_print('@string path_setting requires valid c arg') 

7148 return None 

7149 # It's not an error for the setting to be empty. 

7150 base_dir2 = c.config.getString(path_setting) 

7151 if base_dir2: 

7152 base_dir2 = base_dir2.replace('\\', '/') 

7153 if g.os_path_exists(base_dir2): 

7154 return base_dir2 

7155 g.es_print(f"@string {path_setting} not found: {base_dir2!r}") 

7156 return None 

7157 # Fall back to given base_dir. 

7158 if base_dir: 

7159 base_dir = base_dir.replace('\\', '/') 

7160 if g.os_path_exists(base_dir): 

7161 return base_dir 

7162 g.es_print(f"base_dir not found: {base_dir!r}") 

7163 return None 

7164 g.es_print(f"Please use @string {path_setting}") 

7165 return None 

7166#@+node:ekr.20180217153459.1: *4* g.computeCommands 

7167def computeCommands(c: Cmdr, commands: List[str], command_setting: str, trace: bool=False) -> List[str]: 

7168 """ 

7169 Get the list of commands. 

7170 If given, @data command_setting takes precedence. 

7171 """ 

7172 if not commands and not command_setting: 

7173 g.es_print('Please use commands, command_setting or both') 

7174 return [] 

7175 # Prefer the setting to the static commands. 

7176 if command_setting: 

7177 if c: 

7178 aList = c.config.getData(command_setting) 

7179 # It's not an error for the setting to be empty. 

7180 # Fall back to the commands. 

7181 return aList or commands 

7182 g.es_print('@data command_setting requires valid c arg') 

7183 return [] 

7184 return commands 

7185#@+node:ekr.20050503112513.7: *3* g.executeFile 

7186def executeFile(filename: str, options: str='') -> None: 

7187 if not os.access(filename, os.R_OK): 

7188 return 

7189 fdir, fname = g.os_path_split(filename) 

7190 # New in Leo 4.10: alway use subprocess. 

7191 

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

7193 

7194 p = subprocess.Popen(cmdlst, cwd=fdir, 

7195 universal_newlines=True, 

7196 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 

7197 stdo, stde = p.communicate() 

7198 return p.wait(), stdo, stde 

7199 

7200 rc, so, se = subprocess_wrapper(f"{sys.executable} {fname} {options}") 

7201 if rc: 

7202 g.pr('return code', rc) 

7203 g.pr(so, se) 

7204#@+node:ekr.20040321065415: *3* g.find*Node* 

7205#@+others 

7206#@+node:ekr.20210303123423.3: *4* findNodeAnywhere 

7207def findNodeAnywhere(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]: 

7208 h = headline.strip() 

7209 for p in c.all_unique_positions(copy=False): 

7210 if p.h.strip() == h: 

7211 return p.copy() 

7212 if not exact: 

7213 for p in c.all_unique_positions(copy=False): 

7214 if p.h.strip().startswith(h): 

7215 return p.copy() 

7216 return None 

7217#@+node:ekr.20210303123525.1: *4* findNodeByPath 

7218def findNodeByPath(c: Cmdr, path: str) -> Optional[Pos]: 

7219 """Return the first @<file> node in Cmdr c whose path is given.""" 

7220 if not os.path.isabs(path): # #2049. Only absolute paths could possibly work. 

7221 g.trace(f"path not absolute: {path}") 

7222 return None 

7223 path = g.os_path_normpath(path) # #2049. Do *not* use os.path.normpath. 

7224 for p in c.all_positions(): 

7225 if p.isAnyAtFileNode(): 

7226 if path == g.os_path_normpath(g.fullPath(c, p)): # #2049. Do *not* use os.path.normpath. 

7227 return p 

7228 return None 

7229#@+node:ekr.20210303123423.1: *4* findNodeInChildren 

7230def findNodeInChildren(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]: 

7231 """Search for a node in v's tree matching the given headline.""" 

7232 p1 = p.copy() 

7233 h = headline.strip() 

7234 for p in p1.children(): 

7235 if p.h.strip() == h: 

7236 return p.copy() 

7237 if not exact: 

7238 for p in p1.children(): 

7239 if p.h.strip().startswith(h): 

7240 return p.copy() 

7241 return None 

7242#@+node:ekr.20210303123423.2: *4* findNodeInTree 

7243def findNodeInTree(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]: 

7244 """Search for a node in v's tree matching the given headline.""" 

7245 h = headline.strip() 

7246 p1 = p.copy() 

7247 for p in p1.subtree(): 

7248 if p.h.strip() == h: 

7249 return p.copy() 

7250 if not exact: 

7251 for p in p1.subtree(): 

7252 if p.h.strip().startswith(h): 

7253 return p.copy() 

7254 return None 

7255#@+node:ekr.20210303123423.4: *4* findTopLevelNode 

7256def findTopLevelNode(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]: 

7257 h = headline.strip() 

7258 for p in c.rootPosition().self_and_siblings(copy=False): 

7259 if p.h.strip() == h: 

7260 return p.copy() 

7261 if not exact: 

7262 for p in c.rootPosition().self_and_siblings(copy=False): 

7263 if p.h.strip().startswith(h): 

7264 return p.copy() 

7265 return None 

7266#@-others 

7267#@+node:EKR.20040614071102.1: *3* g.getScript & helpers 

7268def getScript( 

7269 c: Cmdr, 

7270 p: Pos, 

7271 useSelectedText: bool=True, 

7272 forcePythonSentinels: bool=True, 

7273 useSentinels: bool=True, 

7274) -> str: 

7275 """ 

7276 Return the expansion of the selected text of node p. 

7277 Return the expansion of all of node p's body text if 

7278 p is not the current node or if there is no text selection. 

7279 """ 

7280 w = c.frame.body.wrapper 

7281 if not p: 

7282 p = c.p 

7283 try: 

7284 if g.app.inBridge: 

7285 s = p.b 

7286 elif w and p == c.p and useSelectedText and w.hasSelection(): 

7287 s = w.getSelectedText() 

7288 else: 

7289 s = p.b 

7290 # Remove extra leading whitespace so the user may execute indented code. 

7291 s = textwrap.dedent(s) 

7292 s = g.extractExecutableString(c, p, s) 

7293 script = g.composeScript(c, p, s, 

7294 forcePythonSentinels=forcePythonSentinels, 

7295 useSentinels=useSentinels) 

7296 except Exception: 

7297 g.es_print("unexpected exception in g.getScript") 

7298 g.es_exception() 

7299 script = '' 

7300 return script 

7301#@+node:ekr.20170228082641.1: *4* g.composeScript 

7302def composeScript( 

7303 c: Cmdr, 

7304 p: Pos, 

7305 s: str, 

7306 forcePythonSentinels: bool=True, 

7307 useSentinels: bool=True, 

7308) -> str: 

7309 """Compose a script from p.b.""" 

7310 # This causes too many special cases. 

7311 # if not g.unitTesting and forceEncoding: 

7312 # aList = g.get_directives_dict_list(p) 

7313 # encoding = scanAtEncodingDirectives(aList) or 'utf-8' 

7314 # s = g.insertCodingLine(encoding,s) 

7315 if not s.strip(): 

7316 return '' 

7317 at = c.atFileCommands # type:ignore 

7318 old_in_script = g.app.inScript 

7319 try: 

7320 # #1297: set inScript flags. 

7321 g.app.inScript = g.inScript = True 

7322 g.app.scriptDict["script1"] = s 

7323 # Important: converts unicode to utf-8 encoded strings. 

7324 script = at.stringToString(p.copy(), s, 

7325 forcePythonSentinels=forcePythonSentinels, 

7326 sentinels=useSentinels) 

7327 # Important, the script is an **encoded string**, not a unicode string. 

7328 script = script.replace("\r\n", "\n") # Use brute force. 

7329 g.app.scriptDict["script2"] = script 

7330 finally: 

7331 g.app.inScript = g.inScript = old_in_script 

7332 return script 

7333#@+node:ekr.20170123074946.1: *4* g.extractExecutableString 

7334def extractExecutableString(c: Cmdr, p: Pos, s: str) -> str: 

7335 """ 

7336 Return all lines for the given @language directive. 

7337 

7338 Ignore all lines under control of any other @language directive. 

7339 """ 

7340 # 

7341 # Rewritten to fix #1071. 

7342 if g.unitTesting: 

7343 return s # Regretable, but necessary. 

7344 # 

7345 # Return s if no @language in effect. Should never happen. 

7346 language = g.scanForAtLanguage(c, p) 

7347 if not language: 

7348 return s 

7349 # 

7350 # Return s if @language is unambiguous. 

7351 pattern = r'^@language\s+(\w+)' 

7352 matches = list(re.finditer(pattern, s, re.MULTILINE)) 

7353 if len(matches) < 2: 

7354 return s 

7355 # 

7356 # Scan the lines, extracting only the valid lines. 

7357 extracting, result = False, [] 

7358 for i, line in enumerate(g.splitLines(s)): 

7359 m = re.match(pattern, line) 

7360 if m: 

7361 # g.trace(language, m.group(1)) 

7362 extracting = m.group(1) == language 

7363 elif extracting: 

7364 result.append(line) 

7365 return ''.join(result) 

7366#@+node:ekr.20060624085200: *3* g.handleScriptException 

7367def handleScriptException(c: Cmdr, p: Pos, script: str, script1: str) -> None: 

7368 g.warning("exception executing script") 

7369 full = c.config.getBool('show-full-tracebacks-in-scripts') 

7370 fileName, n = g.es_exception(full=full) 

7371 # Careful: this test is no longer guaranteed. 

7372 if p.v.context == c: 

7373 try: 

7374 c.goToScriptLineNumber(n, p) 

7375 #@+<< dump the lines near the error >> 

7376 #@+node:EKR.20040612215018: *4* << dump the lines near the error >> 

7377 if g.os_path_exists(fileName): 

7378 with open(fileName) as f: 

7379 lines = f.readlines() 

7380 else: 

7381 lines = g.splitLines(script) 

7382 s = '-' * 20 

7383 g.es_print('', s) 

7384 # Print surrounding lines. 

7385 i = max(0, n - 2) 

7386 j = min(n + 2, len(lines)) 

7387 while i < j: 

7388 ch = '*' if i == n - 1 else ' ' 

7389 s = f"{ch} line {i+1:d}: {lines[i]}" 

7390 g.es('', s, newline=False) 

7391 i += 1 

7392 #@-<< dump the lines near the error >> 

7393 except Exception: 

7394 g.es_print('Unexpected exception in g.handleScriptException') 

7395 g.es_exception() 

7396#@+node:ekr.20140209065845.16767: *3* g.insertCodingLine 

7397def insertCodingLine(encoding: str, script: str) -> str: 

7398 """ 

7399 Insert a coding line at the start of script s if no such line exists. 

7400 The coding line must start with @first because it will be passed to 

7401 at.stringToString. 

7402 """ 

7403 if script: 

7404 tag = '@first # -*- coding:' 

7405 lines = g.splitLines(script) 

7406 for s in lines: 

7407 if s.startswith(tag): 

7408 break 

7409 else: 

7410 lines.insert(0, f"{tag} {encoding} -*-\n") 

7411 script = ''.join(lines) 

7412 return script 

7413#@+node:ekr.20070524083513: ** g.Unit Tests 

7414#@+node:ekr.20210901071523.1: *3* g.run_coverage_tests 

7415def run_coverage_tests(module: str='', filename: str='') -> None: 

7416 """ 

7417 Run the coverage tests given by the module and filename strings. 

7418 """ 

7419 unittests_dir = g.os_path_finalize_join(g.app.loadDir, '..', 'unittests') 

7420 assert os.path.exists(unittests_dir) 

7421 os.chdir(unittests_dir) 

7422 prefix = r"python -m pytest --cov-report html --cov-report term-missing --cov " 

7423 command = f"{prefix} {module} {filename}" 

7424 g.execute_shell_commands(command, trace=False) 

7425#@+node:ekr.20200221050038.1: *3* g.run_unit_test_in_separate_process 

7426def run_unit_test_in_separate_process(command: str) -> None: 

7427 """ 

7428 A script to be run from unitTest.leo. 

7429 

7430 Run the unit testing command (say `python -m leo.core.leoAst`) in a separate process. 

7431 """ 

7432 leo_editor_dir = os.path.join(g.app.loadDir, '..', '..') 

7433 os.chdir(leo_editor_dir) 

7434 p = subprocess.Popen( 

7435 shlex.split(command), 

7436 stdout=subprocess.PIPE, 

7437 stderr=subprocess.PIPE, 

7438 shell=sys.platform.startswith('win'), 

7439 ) 

7440 out, err = p.communicate() 

7441 err = g.toUnicode(err) 

7442 out = g.toUnicode(out) 

7443 print('') 

7444 print(command) 

7445 if out.strip(): 

7446 # print('traces...') 

7447 print(out.rstrip()) 

7448 print(err.rstrip()) 

7449 # There may be skipped tests... 

7450 err_lines = g.splitLines(err.rstrip()) 

7451 if not err_lines[-1].startswith('OK'): 

7452 g.trace('Test failed') 

7453 g.printObj(err_lines, tag='err_lines') 

7454 assert False 

7455#@+node:ekr.20210901065224.1: *3* g.run_unit_tests 

7456def run_unit_tests(tests: str=None, verbose: bool=False) -> None: 

7457 """ 

7458 Run the unit tests given by the "tests" string. 

7459 

7460 Run *all* unit tests if "tests" is not given. 

7461 """ 

7462 leo_editor_dir = g.os_path_finalize_join(g.app.loadDir, '..', '..') 

7463 os.chdir(leo_editor_dir) 

7464 verbosity = '-v' if verbose else '' 

7465 command = f"python -m unittest {verbosity} {tests or ''} " 

7466 # pytest reports too many errors. 

7467 # command = f"python -m pytest --pdb {tests or ''}" 

7468 g.execute_shell_commands(command, trace=False) 

7469#@+node:ekr.20120311151914.9916: ** g.Urls & UNLs 

7470unl_regex = re.compile(r'\bunl:.*$') 

7471 

7472kinds = '(file|ftp|gopher|http|https|mailto|news|nntp|prospero|telnet|wais)' 

7473url_regex = re.compile(fr"""{kinds}://[^\s'"]+[\w=/]""") 

7474#@+node:ekr.20120320053907.9776: *3* g.computeFileUrl 

7475def computeFileUrl(fn: str, c: Cmdr=None, p: Pos=None) -> str: 

7476 """ 

7477 Compute finalized url for filename fn. 

7478 """ 

7479 # First, replace special characters (especially %20, by their equivalent). 

7480 url = urllib.parse.unquote(fn) 

7481 # Finalize the path *before* parsing the url. 

7482 i = url.find('~') 

7483 if i > -1: 

7484 # Expand '~'. 

7485 path = url[i:] 

7486 path = g.os_path_expanduser(path) 

7487 # #1338: This is way too dangerous, and a serious security violation. 

7488 # path = c.os_path_expandExpression(path) 

7489 path = g.os_path_finalize(path) 

7490 url = url[:i] + path 

7491 else: 

7492 tag = 'file://' 

7493 tag2 = 'file:///' 

7494 if sys.platform.startswith('win') and url.startswith(tag2): 

7495 path = url[len(tag2) :].lstrip() 

7496 elif url.startswith(tag): 

7497 path = url[len(tag) :].lstrip() 

7498 else: 

7499 path = url 

7500 # #1338: This is way too dangerous, and a serious security violation. 

7501 # path = c.os_path_expandExpression(path) 

7502 # Handle ancestor @path directives. 

7503 if c and c.openDirectory: 

7504 base = c.getNodePath(p) 

7505 path = g.os_path_finalize_join(c.openDirectory, base, path) 

7506 else: 

7507 path = g.os_path_finalize(path) 

7508 url = f"{tag}{path}" 

7509 return url 

7510#@+node:ekr.20190608090856.1: *3* g.es_clickable_link 

7511def es_clickable_link(c: Cmdr, p: Pos, line_number: int, message: str) -> None: 

7512 """ 

7513 Write a clickable message to the given line number of p.b. 

7514 

7515 Negative line numbers indicate global lines. 

7516 

7517 """ 

7518 unl = p.get_UNL() 

7519 c.frame.log.put(message.strip() + '\n', nodeLink=f"{unl}::{line_number}") 

7520#@+node:tbrown.20140311095634.15188: *3* g.findUNL & helpers 

7521def findUNL(unlList1: List[str], c: Cmdr) -> Optional[Pos]: 

7522 """ 

7523 Find and move to the unl given by the unlList in the commander c. 

7524 Return the found position, or None. 

7525 """ 

7526 # Define the unl patterns. 

7527 old_pat = re.compile(r'^(.*):(\d+),?(\d+)?,?([-\d]+)?,?(\d+)?$') # ':' is the separator. 

7528 new_pat = re.compile(r'^(.*?)(::)([-\d]+)?$') # '::' is the separator. 

7529 

7530 #@+others # Define helper functions 

7531 #@+node:ekr.20220213142925.1: *4* function: convert_unl_list 

7532 def convert_unl_list(aList: List[str]) -> List[str]: 

7533 """ 

7534 Convert old-style UNLs to new UNLs, retaining line numbers if possible. 

7535 """ 

7536 result = [] 

7537 for s in aList: 

7538 # Try to get the line number. 

7539 for m, line_group in ( 

7540 (old_pat.match(s), 4), 

7541 (new_pat.match(s), 3), 

7542 ): 

7543 if m: 

7544 try: 

7545 n = int(m.group(line_group)) 

7546 result.append(f"{m.group(1)}::{n}") 

7547 continue 

7548 except Exception: 

7549 pass 

7550 # Finally, just add the whole UNL. 

7551 result.append(s) 

7552 return result 

7553 #@+node:ekr.20220213142735.1: *4* function: full_match 

7554 def full_match(p: Pos) -> bool: 

7555 """Return True if the headlines of p and all p's parents match unlList.""" 

7556 # Careful: make copies. 

7557 aList, p1 = unlList[:], p.copy() 

7558 while aList and p1: 

7559 m = new_pat.match(aList[-1]) 

7560 if m and m.group(1).strip() != p1.h.strip(): 

7561 return False 

7562 if not m and aList[-1].strip() != p1.h.strip(): 

7563 return False 

7564 aList.pop() 

7565 p1.moveToParent() 

7566 return not aList 

7567 #@-others 

7568 

7569 unlList = convert_unl_list(unlList1) 

7570 if not unlList: 

7571 return None 

7572 # Find all target headlines. 

7573 targets = [] 

7574 m = new_pat.match(unlList[-1]) 

7575 target = m and m.group(1) or unlList[-1] 

7576 targets.append(target) 

7577 targets.extend(unlList[:-1]) 

7578 # Find all target positions. Prefer later positions. 

7579 positions = list(reversed(list(z for z in c.all_positions() if z.h.strip() in targets))) 

7580 while unlList: 

7581 for p in positions: 

7582 p1 = p.copy() 

7583 if full_match(p): 

7584 assert p == p1, (p, p1) 

7585 n = 0 # The default line number. 

7586 # Parse the last target. 

7587 m = new_pat.match(unlList[-1]) 

7588 if m: 

7589 line = m.group(3) 

7590 try: 

7591 n = int(line) 

7592 except(TypeError, ValueError): 

7593 g.trace('bad line number', line) 

7594 if n == 0: 

7595 c.redraw(p) 

7596 elif n < 0: 

7597 p, offset, ok = c.gotoCommands.find_file_line(-n, p) # Calls c.redraw(). 

7598 return p if ok else None 

7599 elif n > 0: 

7600 insert_point = sum(len(i) + 1 for i in p.b.split('\n')[: n - 1]) 

7601 c.redraw(p) 

7602 c.frame.body.wrapper.setInsertPoint(insert_point) 

7603 c.frame.bringToFront() 

7604 c.bodyWantsFocusNow() 

7605 return p 

7606 # Not found. Pop the first parent from unlList. 

7607 unlList.pop(0) 

7608 return None 

7609#@+node:ekr.20120311151914.9917: *3* g.getUrlFromNode 

7610def getUrlFromNode(p: Pos) -> Optional[str]: 

7611 """ 

7612 Get an url from node p: 

7613 1. Use the headline if it contains a valid url. 

7614 2. Otherwise, look *only* at the first line of the body. 

7615 """ 

7616 if not p: 

7617 return None 

7618 c = p.v.context 

7619 assert c 

7620 table = [p.h, g.splitLines(p.b)[0] if p.b else ''] 

7621 table = [s[4:] if g.match_word(s, 0, '@url') else s for s in table] 

7622 table = [s.strip() for s in table if s.strip()] 

7623 # First, check for url's with an explicit scheme. 

7624 for s in table: 

7625 if g.isValidUrl(s): 

7626 return s 

7627 # Next check for existing file and add a file:// scheme. 

7628 for s in table: 

7629 tag = 'file://' 

7630 url = computeFileUrl(s, c=c, p=p) 

7631 if url.startswith(tag): 

7632 fn = url[len(tag) :].lstrip() 

7633 fn = fn.split('#', 1)[0] 

7634 if g.os_path_isfile(fn): 

7635 # Return the *original* url, with a file:// scheme. 

7636 # g.handleUrl will call computeFileUrl again. 

7637 return 'file://' + s 

7638 # Finally, check for local url's. 

7639 for s in table: 

7640 if s.startswith("#"): 

7641 return s 

7642 return None 

7643#@+node:ekr.20170221063527.1: *3* g.handleUnl 

7644def handleUnl(unl: str, c: Cmdr) -> Any: 

7645 """ 

7646 Handle a Leo UNL. This must *never* open a browser. 

7647 

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

7649  

7650 Redraw the commander if the UNL is found. 

7651 """ 

7652 if not unl: 

7653 return None 

7654 unll = unl.lower() 

7655 if unll.startswith('unl://'): 

7656 unl = unl[6:] 

7657 elif unll.startswith('file://'): 

7658 unl = unl[7:] 

7659 unl = unl.strip() 

7660 if not unl: 

7661 return None 

7662 unl = g.unquoteUrl(unl) 

7663 # Compute path and unl. 

7664 if '#' not in unl and '-->' not in unl: 

7665 # The path is the entire unl. 

7666 path, unl = unl, None 

7667 elif '#' not in unl: 

7668 # The path is empty. 

7669 # Move to the unl in *this* commander. 

7670 p = g.findUNL(unl.split("-->"), c) 

7671 if p: 

7672 c.redraw(p) 

7673 return c 

7674 else: 

7675 path, unl = unl.split('#', 1) 

7676 if unl and not path: # #2407 

7677 # Move to the unl in *this* commander. 

7678 p = g.findUNL(unl.split("-->"), c) 

7679 if p: 

7680 c.redraw(p) 

7681 return c 

7682 if c: 

7683 base = g.os_path_dirname(c.fileName()) 

7684 c_path = g.os_path_finalize_join(base, path) 

7685 else: 

7686 c_path = None 

7687 # Look for the file in various places. 

7688 table = ( 

7689 c_path, 

7690 g.os_path_finalize_join(g.app.loadDir, '..', path), 

7691 g.os_path_finalize_join(g.app.loadDir, '..', '..', path), 

7692 g.os_path_finalize_join(g.app.loadDir, '..', 'core', path), 

7693 g.os_path_finalize_join(g.app.loadDir, '..', 'config', path), 

7694 g.os_path_finalize_join(g.app.loadDir, '..', 'dist', path), 

7695 g.os_path_finalize_join(g.app.loadDir, '..', 'doc', path), 

7696 g.os_path_finalize_join(g.app.loadDir, '..', 'test', path), 

7697 g.app.loadDir, 

7698 g.app.homeDir, 

7699 ) 

7700 for path2 in table: 

7701 if path2 and path2.lower().endswith('.leo') and os.path.exists(path2): 

7702 path = path2 

7703 break 

7704 else: 

7705 g.es_print('path not found', repr(path)) 

7706 return None 

7707 # End editing in *this* outline, so typing in the new outline works. 

7708 c.endEditing() 

7709 c.redraw() 

7710 # Open the path. 

7711 c2 = g.openWithFileName(path, old_c=c) 

7712 if not c2: 

7713 return None 

7714 # Find and redraw. 

7715 # #2445: Default to c2.rootPosition(). 

7716 p = g.findUNL(unl.split("-->"), c2) or c2.rootPosition() 

7717 c2.redraw(p) 

7718 c2.bringToFront() 

7719 c2.bodyWantsFocusNow() 

7720 return c2 

7721#@+node:tbrown.20090219095555.63: *3* g.handleUrl & helpers 

7722def handleUrl(url: str, c: Cmdr=None, p: Pos=None) -> Any: 

7723 """Open a url or a unl.""" 

7724 if c and not p: 

7725 p = c.p 

7726 urll = url.lower() 

7727 if urll.startswith('@url'): 

7728 url = url[4:].lstrip() 

7729 if ( 

7730 urll.startswith('unl://') or 

7731 urll.startswith('file://') and url.find('-->') > -1 or 

7732 urll.startswith('#') 

7733 ): 

7734 return g.handleUnl(url, c) 

7735 try: 

7736 return g.handleUrlHelper(url, c, p) 

7737 except Exception: 

7738 g.es_print("g.handleUrl: exception opening", repr(url)) 

7739 g.es_exception() 

7740 return None 

7741#@+node:ekr.20170226054459.1: *4* g.handleUrlHelper 

7742def handleUrlHelper(url: str, c: Cmdr, p: Pos) -> None: 

7743 """Open a url. Most browsers should handle: 

7744 ftp://ftp.uu.net/public/whatever 

7745 http://localhost/MySiteUnderDevelopment/index.html 

7746 file:///home/me/todolist.html 

7747 """ 

7748 tag = 'file://' 

7749 original_url = url 

7750 if url.startswith(tag) and not url.startswith(tag + '#'): 

7751 # Finalize the path *before* parsing the url. 

7752 url = g.computeFileUrl(url, c=c, p=p) 

7753 parsed = urlparse.urlparse(url) 

7754 if parsed.netloc: 

7755 leo_path = os.path.join(parsed.netloc, parsed.path) 

7756 # "readme.txt" gets parsed into .netloc... 

7757 else: 

7758 leo_path = parsed.path 

7759 if leo_path.endswith('\\'): 

7760 leo_path = leo_path[:-1] 

7761 if leo_path.endswith('/'): 

7762 leo_path = leo_path[:-1] 

7763 if parsed.scheme == 'file' and leo_path.endswith('.leo'): 

7764 g.handleUnl(original_url, c) 

7765 elif parsed.scheme in ('', 'file'): 

7766 unquote_path = g.unquoteUrl(leo_path) 

7767 if g.unitTesting: 

7768 pass 

7769 elif g.os_path_exists(leo_path): 

7770 g.os_startfile(unquote_path) 

7771 else: 

7772 g.es(f"File '{leo_path}' does not exist") 

7773 else: 

7774 if g.unitTesting: 

7775 pass 

7776 else: 

7777 # Mozilla throws a weird exception, then opens the file! 

7778 try: 

7779 webbrowser.open(url) 

7780 except Exception: 

7781 pass 

7782#@+node:ekr.20170226060816.1: *4* g.traceUrl 

7783def traceUrl(c: Cmdr, path: str, parsed: Any, url: str) -> None: 

7784 

7785 print() 

7786 g.trace('url ', url) 

7787 g.trace('c.frame.title', c.frame.title) 

7788 g.trace('path ', path) 

7789 g.trace('parsed.fragment', parsed.fragment) 

7790 g.trace('parsed.netloc', parsed.netloc) 

7791 g.trace('parsed.path ', parsed.path) 

7792 g.trace('parsed.scheme', repr(parsed.scheme)) 

7793#@+node:ekr.20120311151914.9918: *3* g.isValidUrl 

7794def isValidUrl(url: str) -> bool: 

7795 """Return true if url *looks* like a valid url.""" 

7796 table = ( 

7797 'file', 'ftp', 'gopher', 'hdl', 'http', 'https', 'imap', 

7798 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu', 

7799 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', 'wais', 

7800 ) 

7801 if url.lower().startswith('unl://') or url.startswith('#'): 

7802 # All Leo UNL's. 

7803 return True 

7804 if url.startswith('@'): 

7805 return False 

7806 parsed = urlparse.urlparse(url) 

7807 scheme = parsed.scheme 

7808 for s in table: 

7809 if scheme.startswith(s): 

7810 return True 

7811 return False 

7812#@+node:ekr.20120315062642.9744: *3* g.openUrl 

7813def openUrl(p: Pos) -> None: 

7814 """ 

7815 Open the url of node p. 

7816 Use the headline if it contains a valid url. 

7817 Otherwise, look *only* at the first line of the body. 

7818 """ 

7819 if p: 

7820 url = g.getUrlFromNode(p) 

7821 if url: 

7822 c = p.v.context 

7823 if not g.doHook("@url1", c=c, p=p, url=url): 

7824 g.handleUrl(url, c=c, p=p) 

7825 g.doHook("@url2", c=c, p=p, url=url) 

7826#@+node:ekr.20110605121601.18135: *3* g.openUrlOnClick (open-url-under-cursor) 

7827def openUrlOnClick(event: Any, url: str=None) -> Optional[str]: 

7828 """Open the URL under the cursor. Return it for unit testing.""" 

7829 # This can be called outside Leo's command logic, so catch all exceptions. 

7830 try: 

7831 return openUrlHelper(event, url) 

7832 except Exception: 

7833 g.es_exception() 

7834 return None 

7835#@+node:ekr.20170216091704.1: *4* g.openUrlHelper 

7836def openUrlHelper(event: Any, url: str=None) -> Optional[str]: 

7837 """Open the UNL or URL under the cursor. Return it for unit testing.""" 

7838 c = getattr(event, 'c', None) 

7839 if not c: 

7840 return None 

7841 w = getattr(event, 'w', c.frame.body.wrapper) 

7842 if not g.app.gui.isTextWrapper(w): 

7843 g.internalError('must be a text wrapper', w) 

7844 return None 

7845 setattr(event, 'widget', w) 

7846 # Part 1: get the url. 

7847 if url is None: 

7848 s = w.getAllText() 

7849 ins = w.getInsertPoint() 

7850 i, j = w.getSelectionRange() 

7851 if i != j: 

7852 return None # So find doesn't open the url. 

7853 row, col = g.convertPythonIndexToRowCol(s, ins) 

7854 i, j = g.getLine(s, ins) 

7855 line = s[i:j] 

7856 

7857 # Navigate to section reference if one was clickedon 

7858 l_ = line.strip() 

7859 if l_.startswith('<<') and l_.endswith('>>'): 

7860 p = c.p 

7861 px = None 

7862 for p1 in p.subtree(): 

7863 if p1.h.strip() == l_: 

7864 px = p1 

7865 break 

7866 if px: 

7867 c.selectPosition(px) 

7868 c.redraw() 

7869 

7870 # Find the url on the line. 

7871 for match in g.url_regex.finditer(line): 

7872 # Don't open if we click after the url. 

7873 if match.start() <= col < match.end(): 

7874 url = match.group() 

7875 if g.isValidUrl(url): 

7876 break 

7877 else: 

7878 # Look for the unl: 

7879 for match in g.unl_regex.finditer(line): 

7880 # Don't open if we click after the unl. 

7881 if match.start() <= col < match.end(): 

7882 unl = match.group() 

7883 g.handleUnl(unl, c) 

7884 return None 

7885 elif not isinstance(url, str): 

7886 url = url.toString() 

7887 url = g.toUnicode(url) # #571 

7888 if url and g.isValidUrl(url): 

7889 # Part 2: handle the url 

7890 p = c.p 

7891 if not g.doHook("@url1", c=c, p=p, url=url): 

7892 g.handleUrl(url, c=c, p=p) 

7893 g.doHook("@url2", c=c, p=p) 

7894 return url 

7895 # Part 3: call find-def. 

7896 if not w.hasSelection(): 

7897 c.editCommands.extendToWord(event, select=True) 

7898 word = w.getSelectedText().strip() 

7899 if word: 

7900 c.findCommands.find_def_strict(event) 

7901 return None 

7902#@+node:ekr.20170226093349.1: *3* g.unquoteUrl 

7903def unquoteUrl(url: str) -> str: 

7904 """Replace special characters (especially %20, by their equivalent).""" 

7905 return urllib.parse.unquote(url) 

7906#@-others 

7907# set g when the import is about to complete. 

7908g: Any = sys.modules.get('leo.core.leoGlobals') 

7909assert g, sorted(sys.modules.keys()) 

7910if __name__ == '__main__': 

7911 unittest.main() 

7912 

7913#@@language python 

7914#@@tabwidth -4 

7915#@@pagewidth 70 

7916#@-leo