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 

44# 

45# Leo never imports any other Leo module. 

46if TYPE_CHECKING: # Always False at runtime. 

47 from leo.core.leoCommands import Commands as Cmdr 

48 from leo.core.leoNodes import Position as Pos 

49 from leo.core.leoNodes import VNode 

50else: 

51 Cmdr = Pos = VNode = Any 

52# 

53# Abbreviations... 

54StringIO = io.StringIO 

55#@-<< imports >> 

56in_bridge = False 

57 # Set to True in leoBridge.py just before importing leo.core.leoApp. 

58 # This tells leoApp to load a null Gui. 

59in_vs_code = False # #2098. 

60minimum_python_version = '3.6' # #1215. 

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

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

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

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

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

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

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

68globalDirectiveList = [ 

69 # Order does not matter. 

70 'all', 

71 'beautify', 

72 'colorcache', 'code', 'color', 'comment', 'c', 

73 'delims', 'doc', 

74 'encoding', 

75 # 'end_raw', # #2276. 

76 'first', 'header', 'ignore', 

77 'killbeautify', 'killcolor', 

78 'language', 'last', 'lineending', 

79 'markup', 

80 'nobeautify', 

81 'nocolor-node', 'nocolor', 'noheader', 'nowrap', 

82 'nopyflakes', # Leo 6.1. 

83 'nosearch', # Leo 5.3. 

84 'others', 'pagewidth', 'path', 'quiet', 

85 # 'raw', # #2276. 

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

87 'silent', 

88 'tabwidth', 'terse', 

89 'unit', 'verbose', 'wrap', 

90] 

91 

92directives_pat = None # Set below. 

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

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

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

96#@@nobeautify 

97#@@language rest 

98#@+at 

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

100# following appears in leo.commands. 

101# 

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

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

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

105# 

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

107#@@c 

108#@@language python 

109 

110global_commands_dict = {} 

111 

112cmd_instance_dict = { 

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

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

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

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

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

118 'Commands': ['c'], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

138} 

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

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

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

142#@+others 

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

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

145 """ 

146 Check g.check_cmd_instance_dict. 

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

148 """ 

149 d = cmd_instance_dict 

150 for key in d: 

151 ivars = d.get(key) 

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

153 # Produces warnings. 

154 if obj: 

155 name = obj.__class__.__name__ 

156 if name != key: 

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

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

159class Command: 

160 """ 

161 A global decorator for creating commands. 

162 

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

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

165 

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

167 def A_Command(event): 

168 c = event.get('c') 

169 ... 

170 

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

172 """ 

173 

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

175 """Ctor for command decorator class.""" 

176 self.name = name 

177 

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

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

180 global_commands_dict[self.name] = func 

181 if app: 

182 for c in app.commanders(): 

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

184 # Inject ivars for plugins_menu.py. 

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

186 func.is_command = True 

187 func.command_name = self.name 

188 return func 

189 

190command = Command 

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

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

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

194 from leo.core import leoCommands 

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

196 funcToMethod(func, leoCommands.Commands, alias) 

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

198class CommanderCommand: 

199 """ 

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

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

202 

203 Usage: 

204 

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

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

207 ... 

208 

209 The decorator injects command_name into the Commander class and calls 

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

211 

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

213 """ 

214 

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

216 """Ctor for command decorator class.""" 

217 self.name = name 

218 

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

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

221 

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

223 c = event.get('c') 

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

225 method(event=event) 

226 

227 # Inject ivars for plugins_menu.py. 

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

229 commander_command_wrapper.__name__ = self.name 

230 commander_command_wrapper.__doc__ = func.__doc__ 

231 global_commands_dict[self.name] = commander_command_wrapper 

232 if app: 

233 from leo.core import leoCommands 

234 funcToMethod(func, leoCommands.Commands) 

235 for c in app.commanders(): 

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

237 # Inject ivars for plugins_menu.py. 

238 func.is_command = True 

239 func.command_name = self.name 

240 return func 

241 

242commander_command = CommanderCommand 

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

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

245 """ 

246 Return the instance of c given by ivars. 

247 ivars is a list of strings. 

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

249 """ 

250 if not ivars: 

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

252 return None 

253 ivar = ivars[0] 

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

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

256 return None 

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

258 for ivar in ivars[1:]: 

259 obj = getattr(obj, ivar, None) 

260 if not obj: 

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

262 break 

263 return obj 

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

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

266 """ 

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

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

269 

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

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

272 """ 

273 

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

275 

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

277 if isinstance(event, dict): 

278 c = event.get('c') 

279 else: 

280 c = event.c 

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

282 try: 

283 func(self, event=event) 

284 # Don't use a keyword for self. 

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

286 except Exception: 

287 g.es_exception() 

288 

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

290 new_cmd_wrapper.__name__ = name 

291 new_cmd_wrapper.__doc__ = func.__doc__ 

292 global_commands_dict[name] = new_cmd_wrapper 

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

294 return func 

295 # The decorator must return the func itself. 

296 

297 return _decorator 

298#@-others 

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

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

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

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

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

304# 

305# Patterns used only in this module... 

306 

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

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

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

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

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

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

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

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

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

316user_dict: Dict[Any, Any] = {} 

317 # Non-persistent dictionary for free use by scripts and plugins. 

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

319# Global status vars. 

320inScript = False # A synonym for app.inScript 

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

322#@+others 

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

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

325def standard_timestamp() -> str: 

326 """Return a reasonable timestamp.""" 

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

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

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

330 """ 

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

332 

333 The main backup directory is computed as follows: 

334 

335 1. os.environ['LEO_BACKUP'] 

336 2. ~/Backup 

337 """ 

338 # Compute the main backup directory. 

339 # First, try the LEO_BACKUP directory. 

340 backup = None 

341 try: 

342 backup = os.environ['LEO_BACKUP'] 

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

344 backup = None 

345 except KeyError: 

346 pass 

347 except Exception: 

348 g.es_exception() 

349 # Second, try ~/Backup. 

350 if not backup: 

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

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

353 backup = None 

354 if not backup: 

355 return None 

356 # Compute the path to backup/sub_directory 

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

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

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

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

361class BindingInfo: 

362 """ 

363 A class representing any kind of key binding line. 

364 

365 This includes other information besides just the KeyStroke. 

366 """ 

367 # Important: The startup code uses this class, 

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

369 #@+others 

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

371 def __init__( 

372 self, 

373 kind: str, 

374 commandName: str='', 

375 func: Any=None, 

376 nextMode: Any=None, 

377 pane: Any=None, 

378 stroke: Any=None, 

379 ) -> None: 

380 if not g.isStrokeOrNone(stroke): 

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

382 self.kind = kind 

383 self.commandName = commandName 

384 self.func = func 

385 self.nextMode = nextMode 

386 self.pane = pane 

387 self.stroke = stroke 

388 # 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 """A class that creates an tkinter dialog with a single OK button.""" 

480 #@+others 

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

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

483 """Constructor for the leoTkinterDialog class.""" 

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

485 self.title = title 

486 self.message = message 

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

488 self.defaultButtonCommand = None 

489 # Command to call when user closes the window 

490 # by clicking the close box. 

491 self.frame = None # The outermost frame. 

492 self.root = None # Created in createTopFrame. 

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

494 self.createTopFrame() 

495 buttons = [{ 

496 "text": "OK", 

497 "command": self.okButton, 

498 "default": True, 

499 }] 

500 self.createButtons(buttons) 

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

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

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

504 """Create a row of buttons. 

505 

506 buttons is a list of dictionaries containing 

507 the properties of each button. 

508 """ 

509 import tkinter as Tk 

510 assert self.frame 

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

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

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

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

515 buttonList = [] 

516 for d in buttons: 

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

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

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

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

521 bd = 4 if isDefault else 2 

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

523 underline=underline, command=command) 

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

525 buttonList.append(b) 

526 if isDefault and command: 

527 self.defaultButtonCommand = command 

528 return buttonList 

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

530 def createTopFrame(self) -> None: 

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

532 import tkinter as Tk 

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

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

535 self.top.title(self.title) 

536 self.root.withdraw() 

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

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

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

540 label.pack(pady=10) 

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

542 def okButton(self) -> None: 

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

544 self.top.destroy() 

545 self.top = None 

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

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

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

549 self.okButton() 

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

551 def run(self) -> None: 

552 """Run the modal emergency dialog.""" 

553 # Suppress f-stringify. 

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

555 self.top.lift() 

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

557 self.root.wait_window(self.top) 

558 #@-others 

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

560# Important: The startup code uses this class, 

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

562 

563 

564class GeneralSetting: 

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

566 

567 def __init__( 

568 self, 

569 kind: str, 

570 encoding: str=None, 

571 ivar: str=None, 

572 setting: str=None, 

573 val: Any=None, 

574 path: str=None, 

575 tag: str='setting', 

576 unl: str=None, 

577 ) -> None: 

578 self.encoding = encoding 

579 self.ivar = ivar 

580 self.kind = kind 

581 self.path = path 

582 self.unl = unl 

583 self.setting = setting 

584 self.val = val 

585 self.tag = tag 

586 

587 def __repr__(self) -> str: 

588 # Better for g.printObj. 

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

590 return ( 

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

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

593 

594 dump = __repr__ 

595 __str__ = __repr__ 

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

597class KeyStroke: 

598 """ 

599 A class that represent any key stroke or binding. 

600 

601 stroke.s is the "canonicalized" stroke. 

602 """ 

603 #@+others 

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

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

606 

607 if binding: 

608 self.s = self.finalize_binding(binding) 

609 else: 

610 self.s = None # type:ignore 

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

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

613 # for key in sorted(d) 

614 # where the keys of d are KeyStroke objects. 

615 #@@c 

616 

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

618 if not other: 

619 return False 

620 if hasattr(other, 's'): 

621 return self.s == other.s 

622 return self.s == other 

623 

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

625 if not other: 

626 return False 

627 if hasattr(other, 's'): 

628 return self.s < other.s 

629 return self.s < other 

630 

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

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

633 

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

635 return not self.__eq__(other) 

636 

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

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

639 

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

641 return not self.__lt__(other) 

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

643 # Allow KeyStroke objects to be keys in dictionaries. 

644 

645 def __hash__(self) -> Any: 

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

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

648 def __repr__(self) -> str: 

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

650 

651 def __str__(self) -> str: 

652 return repr(self.s) 

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

654 def dump(self) -> None: 

655 """Show results of printable chars.""" 

656 for i in range(128): 

657 s = chr(i) 

658 stroke = g.KeyStroke(s) 

659 if stroke.s != s: 

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

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

662 stroke = g.KeyStroke(ch) 

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

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

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

666 

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

668 # This trace is good for devs only. 

669 self.mods = self.find_mods(binding) 

670 s = self.strip_mods(binding) 

671 s = self.finalize_char(s) 

672 # May change self.mods. 

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

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

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

676 return mods + s 

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

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

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

680 # 

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

682 shift_d = { 

683 'bksp': 'BackSpace', 

684 'backspace': 'BackSpace', 

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

686 'linefeed': 'Return', 

687 '\r': 'Return', 

688 'return': 'Return', 

689 'tab': 'Tab', 

690 } 

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

692 # Returning '' breaks existing code. 

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

694 # 

695 # Make all other translations... 

696 # 

697 # This dict ensures proper capitalization. 

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

699 translate_d = { 

700 # 

701 # The gang of four... 

702 'bksp': 'BackSpace', 

703 'backspace': 'BackSpace', 

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

705 'linefeed': '\n', 

706 '\r': '\n', 

707 'return': '\n', 

708 'tab': 'Tab', 

709 # 

710 # Special chars... 

711 'delete': 'Delete', 

712 'down': 'Down', 

713 'end': 'End', 

714 'enter': 'Enter', 

715 'escape': 'Escape', 

716 'home': 'Home', 

717 'insert': 'Insert', 

718 'left': 'Left', 

719 'next': 'Next', 

720 'prior': 'Prior', 

721 'right': 'Right', 

722 'up': 'Up', 

723 # 

724 # Qt key names... 

725 'del': 'Delete', 

726 'dnarrow': 'Down', 

727 'esc': 'Escape', 

728 'ins': 'Insert', 

729 'ltarrow': 'Left', 

730 'pagedn': 'Next', 

731 'pageup': 'Prior', 

732 'pgdown': 'Next', 

733 'pgup': 'Prior', 

734 'rtarrow': 'Right', 

735 'uparrow': 'Up', 

736 # 

737 # Legacy Tk binding names... 

738 "ampersand": "&", 

739 "asciicircum": "^", 

740 "asciitilde": "~", 

741 "asterisk": "*", 

742 "at": "@", 

743 "backslash": "\\", 

744 "bar": "|", 

745 "braceleft": "{", 

746 "braceright": "}", 

747 "bracketleft": "[", 

748 "bracketright": "]", 

749 "colon": ":", 

750 "comma": ",", 

751 "dollar": "$", 

752 "equal": "=", 

753 "exclam": "!", 

754 "greater": ">", 

755 "less": "<", 

756 "minus": "-", 

757 "numbersign": "#", 

758 "quotedbl": '"', 

759 "quoteright": "'", 

760 "parenleft": "(", 

761 "parenright": ")", 

762 "percent": "%", 

763 "period": ".", 

764 "plus": "+", 

765 "question": "?", 

766 "quoteleft": "`", 

767 "semicolon": ";", 

768 "slash": "/", 

769 "space": " ", 

770 "underscore": "_", 

771 } 

772 # 

773 # pylint: disable=undefined-loop-variable 

774 # Looks like a pylint bug. 

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

776 return 'None' 

777 if s.lower() in translate_d: 

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

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

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

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

782 return '' 

783 if s.isalpha(): 

784 if len(s) == 1: 

785 if 'shift' in self.mods: 

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

787 self.mods.remove('shift') 

788 s = s.upper() 

789 else: 

790 s = s.lower() 

791 elif self.mods: 

792 s = s.lower() 

793 else: 

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

795 s = '' 

796 if 0: 

797 # Make sure all special chars are in translate_d. 

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

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

800 s = s.capitalize() 

801 return s 

802 # 

803 # Translate shifted keys to their appropriate alternatives. 

804 return self.strip_shift(s) 

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

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

807 """ 

808 Handle supposedly shifted keys. 

809 

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

811 

812 The legacy Tk binding names have already been translated, 

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

814 """ 

815 # 

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

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

818 shift_d = { 

819 # Top row of keyboard. 

820 "`": "~", "~": "~", 

821 "1": "!", "!": "!", 

822 "2": "@", "@": "@", 

823 "3": "#", "#": "#", 

824 "4": "$", "$": "$", 

825 "5": "%", "%": "%", 

826 "6": "^", "^": "^", 

827 "7": "&", "&": "&", 

828 "8": "*", "*": "*", 

829 "9": "(", "(": "(", 

830 "0": ")", ")": ")", 

831 "-": "_", "_": "_", 

832 "=": "+", "+": "+", 

833 # Second row of keyboard. 

834 "[": "{", "{": "{", 

835 "]": "}", "}": "}", 

836 "\\": '|', "|": "|", 

837 # Third row of keyboard. 

838 ";": ":", ":": ":", 

839 "'": '"', '"': '"', 

840 # Fourth row of keyboard. 

841 ".": "<", "<": "<", 

842 ",": ">", ">": ">", 

843 "//": "?", "?": "?", 

844 } 

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

846 self.mods.remove('shift') 

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

848 return s 

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

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

851 

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

853 return self.s.find(pattern) 

854 

855 def lower(self) -> str: 

856 return self.s.lower() 

857 

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

859 return self.s.startswith(s) 

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

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

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

863 s = s.lower() 

864 table = ( 

865 ['alt',], 

866 ['command', 'cmd',], 

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

868 ['meta',], 

869 ['shift', 'shft',], 

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

871 # 868: Allow alternative spellings. 

872 ) 

873 result = [] 

874 for aList in table: 

875 kind = aList[0] 

876 for mod in aList: 

877 for suffix in '+-': 

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

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

880 result.append(kind) 

881 break 

882 return result 

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

884 def isAltCtrl(self) -> bool: 

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

886 mods = self.find_mods(self.s) 

887 return 'alt' in mods and 'ctrl' in mods 

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

889 def isFKey(self) -> bool: 

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

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

892 def isPlainKey(self) -> bool: 

893 """ 

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

895 

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

897 

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

899 """ 

900 s = self.s 

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

902 # For unit tests. 

903 return False 

904 # #868: 

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

906 # Enable bindings. 

907 return False 

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

909 return False 

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

911 return False 

912 if s == 'BackSpace': 

913 return False 

914 return True 

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

916 def isNumPadKey(self) -> bool: 

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

918 

919 def isPlainNumPad(self) -> bool: 

920 return ( 

921 self.isNumPadKey() and 

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

923 ) 

924 

925 def removeNumPadModifier(self) -> None: 

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

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

928 def prettyPrint(self) -> str: 

929 

930 s = self.s 

931 if not s: 

932 return '<None>' 

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

934 ch = s[-1] 

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

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

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

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

939 table = ( 

940 'alt', 

941 'cmd', 'command', 

942 'control', 'ctrl', 

943 'keypad', 'key_pad', # 868: 

944 'meta', 

945 'shift', 'shft', 

946 ) 

947 for mod in table: 

948 for suffix in '+-': 

949 target = mod + suffix 

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

951 if i > -1: 

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

953 break 

954 return s 

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

956 def toGuiChar(self) -> str: 

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

958 s = self.s.lower() 

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

960 s = '\n' 

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

962 s = '\t' 

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

964 s = '\b' 

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

966 s = '.' 

967 return s 

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

969 def toInsertableChar(self) -> str: 

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

971 # pylint: disable=len-as-condition 

972 s = self.s 

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

974 return '' 

975 # Handle the "Gang of Four" 

976 d = { 

977 'BackSpace': '\b', 

978 'LineFeed': '\n', 

979 # 'Insert': '\n', 

980 'Return': '\n', 

981 'Tab': '\t', 

982 } 

983 if s in d: 

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

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

986 #@-others 

987 

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

989 return isinstance(obj, KeyStroke) 

990 

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

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

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

994class MatchBrackets: 

995 """ 

996 A class implementing the match-brackets command. 

997 

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

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

1000 javascript) regex. 

1001 """ 

1002 #@+others 

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

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

1005 """Ctor for MatchBrackets class.""" 

1006 self.c = c 

1007 self.p = p.copy() 

1008 self.language = language 

1009 # Constants. 

1010 self.close_brackets = ")]}>" 

1011 self.open_brackets = "([{<" 

1012 self.brackets = self.open_brackets + self.close_brackets 

1013 self.matching_brackets = self.close_brackets + self.open_brackets 

1014 # Language dependent. 

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

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

1017 # to track expanding selection 

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

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

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

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

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

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

1024 assert s[i] == '/' 

1025 offset = 1 if self.forward else -1 

1026 i += offset 

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

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

1029 return True 

1030 i += offset 

1031 return False 

1032 return False 

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

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

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

1036 assert s[i] == '/' 

1037 offset = 1 if self.forward else -1 

1038 i1 = i 

1039 i += offset 

1040 found: Union[int, bool] = False 

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

1042 ch = s[i] 

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

1044 i += offset 

1045 if ch == '/': 

1046 # Count the preceding backslashes. 

1047 n = 0 

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

1049 n += 1 

1050 i2 -= 1 

1051 if (n % 2) == 0: 

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

1053 found = i 

1054 else: 

1055 found = i 

1056 break 

1057 if found is None: 

1058 self.oops('unmatched regex delim') 

1059 return i1 + offset 

1060 return found 

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

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

1063 """ 

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

1065 Return the index of the next character. 

1066 """ 

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

1068 delim = s[i] 

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

1070 offset = 1 if self.forward else -1 

1071 i += offset 

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

1073 ch = s[i] 

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

1075 i += offset 

1076 if ch == delim: 

1077 # Count the preceding backslashes. 

1078 n = 0 

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

1080 n += 1 

1081 i2 -= 1 

1082 if (n % 2) == 0: 

1083 return i 

1084 # Annoying when matching brackets on the fly. 

1085 # self.oops('unmatched string') 

1086 return i + offset 

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

1088 def expand_range( 

1089 self, 

1090 s: str, 

1091 left: int, 

1092 right: int, 

1093 max_right: int, 

1094 expand: bool=False, 

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

1096 """ 

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

1098 

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

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

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

1102 or s[right] being a bracket. 

1103 

1104 Returns 

1105 new_left, new_right, bracket_char, index_of_bracket_char 

1106 if expansion succeeds, otherwise 

1107 None, None, None, None 

1108 

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

1110 bracket, but index_of_bracket_char will definitely be a bracket. 

1111 """ 

1112 expanded: Union[bool, str] = False 

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

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

1115 orig_left = left 

1116 orig_right = right 

1117 while ( 

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

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

1120 and (left > 0 or right < max_right) 

1121 ): 

1122 expanded = False 

1123 if left > 0: 

1124 left -= 1 

1125 if s[left] in self.brackets: 

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

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

1128 expanded = 'left' 

1129 if right < max_right: 

1130 right += 1 

1131 if s[right] in self.brackets: 

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

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

1134 expanded = 'right' 

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

1136 return left, right, s[left], left 

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

1138 return left, right, s[right], right 

1139 return None, None, None, None 

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

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

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

1143 self.forward = ch1 in self.open_brackets 

1144 # Find the character matching the initial bracket. 

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

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

1147 target = self.matching_brackets[n] 

1148 break 

1149 else: 

1150 return None 

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

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

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

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

1155 """Scan forward for target.""" 

1156 level = 0 

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

1158 progress = i 

1159 ch = s[i] 

1160 if ch in '"\'': 

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

1162 i = self.scan_string(s, i) 

1163 elif self.starts_comment(s, i): 

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

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

1166 i = self.scan_regex(s, i) 

1167 elif ch == ch1: 

1168 level += 1 

1169 i += 1 

1170 elif ch == target: 

1171 level -= 1 

1172 if level <= 0: 

1173 return i 

1174 i += 1 

1175 else: 

1176 i += 1 

1177 assert i > progress 

1178 # Not found 

1179 return None 

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

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

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

1183 i1 = i 

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

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

1186 offset = 1 if self.forward else -1 

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

1188 if not self.forward: 

1189 i1 += len(end) 

1190 i += offset 

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

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

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

1194 return i 

1195 i += offset 

1196 self.oops('unmatched multiline comment') 

1197 elif self.forward: 

1198 # Scan to the newline. 

1199 target = '\n' 

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

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

1202 i += 1 

1203 return i 

1204 i += 1 

1205 else: 

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

1207 target = self.single_comment 

1208 found = None 

1209 i -= 1 

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

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

1212 found = i 

1213 i -= 1 

1214 if found is None: 

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

1216 found = 0 

1217 return found 

1218 return i 

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

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

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

1222 assert 0 <= i < len(s) 

1223 if self.forward: 

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

1225 return True 

1226 return ( 

1227 self.start_comment and self.end_comment and 

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

1229 ) 

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

1231 if self.single_comment: 

1232 # Scan backward for any single-comment delim. 

1233 i -= 1 

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

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

1236 return True 

1237 i -= 1 

1238 return False 

1239 return ( 

1240 self.start_comment and self.end_comment and 

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

1242 ) 

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

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

1245 """Scan backwards for delim.""" 

1246 level = 0 

1247 while i >= 0: 

1248 progress = i 

1249 ch = s[i] 

1250 if self.ends_comment(s, i): 

1251 i = self.back_scan_comment(s, i) 

1252 elif ch in '"\'': 

1253 # Scan to the beginning of the string. 

1254 i = self.scan_string(s, i) 

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

1256 i = self.scan_regex(s, i) 

1257 elif ch == ch1: 

1258 level += 1 

1259 i -= 1 

1260 elif ch == target: 

1261 level -= 1 

1262 if level <= 0: 

1263 return i 

1264 i -= 1 

1265 else: 

1266 i -= 1 

1267 assert i < progress 

1268 # Not found 

1269 return None 

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

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

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

1273 i1 = i 

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

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

1276 i -= 1 

1277 while i >= 0: 

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

1279 i -= 1 

1280 return i 

1281 i -= 1 

1282 self.oops('unmatched multiline comment') 

1283 return i 

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

1285 found = None 

1286 i -= 1 

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

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

1289 found = i - 1 

1290 i -= 1 

1291 if found is None: 

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

1293 found = 0 

1294 return found 

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

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

1297 """ 

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

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

1300 """ 

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

1302 # This is the hard (dubious) case. 

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

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

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

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

1307 # 

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

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

1310 if self.single_comment: 

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

1312 quote = None 

1313 i -= 1 

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

1315 progress = i 

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

1317 quote = None 

1318 i -= 1 

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

1320 if not quote: 

1321 quote = s[i] 

1322 i -= 1 

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

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

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

1326 if quote: 

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

1328 if s[i] == quote: 

1329 return False 

1330 i -= 1 

1331 return True 

1332 else: 

1333 i -= 1 

1334 assert progress > i 

1335 return False 

1336 return ( 

1337 self.start_comment and 

1338 self.end_comment and 

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

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

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

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

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

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

1345 #@@nobeautify 

1346 

1347 def run(self) -> None: 

1348 """The driver for the MatchBrackets class. 

1349 

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

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

1352 

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

1354 range. The second time, select enclosing range. 

1355 """ 

1356 # 

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

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

1359 s = w.getAllText() 

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

1361 sel_range = w.getSelectionRange() 

1362 if not w.hasSelection(): 

1363 _mb['count'] = 1 

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

1365 # haven't been to other end yet 

1366 _mb['count'] += 1 

1367 # move insert point to other end of selection 

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

1369 w.setSelectionRange( 

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

1371 return 

1372 

1373 # Find the bracket nearest the cursor. 

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

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

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

1377 if left is None: 

1378 g.es("Bracket not found") 

1379 return 

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

1381 if index2 is None: 

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

1383 return 

1384 

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

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

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

1388 # to any enclosing brackets 

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

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

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

1392 _mb['count'] += 1 

1393 else: 

1394 _mb['count'] = 1 

1395 _mb['range'] = minmax 

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

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

1398 s, 

1399 max(minmax[0], 0), 

1400 min(minmax[1], max_right), 

1401 max_right, expand=True 

1402 ) 

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

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

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

1406 index, index2 = index3, index4 

1407 _mb['count'] = 1 

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

1409 

1410 if index2 is not None: 

1411 if index2 < index: 

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

1413 else: 

1414 w.setSelectionRange( 

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

1416 w.see(index2) 

1417 else: 

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

1419 #@-others 

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

1421class PosList(list): 

1422 #@+<< docstring for PosList >> 

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

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

1425 

1426 This is deprecated, use leoNodes.PosList instead! 

1427 

1428 aList = g.PosList(c) 

1429 # Creates a PosList containing all positions in c. 

1430 

1431 aList = g.PosList(c,aList2) 

1432 # Creates a PosList from aList2. 

1433 

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

1435 # Creates a PosList containing all positions p in aList 

1436 # such that p.h matches the pattern. 

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

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

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

1440 

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

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

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

1444 """ 

1445 #@-<< docstring for PosList >> 

1446 #@+others 

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

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

1449 self.c = c 

1450 super().__init__() 

1451 if aList is None: 

1452 for p in c.all_positions(): 

1453 self.append(p.copy()) 

1454 else: 

1455 for p in aList: 

1456 self.append(p.copy()) 

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

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

1459 if verbose: 

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

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

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

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

1464 """ 

1465 Return a new PosList containing all positions 

1466 in self that match the given pattern. 

1467 """ 

1468 c = self.c 

1469 

1470 aList = [] 

1471 if regex: 

1472 for p in self: 

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

1474 aList.append(p.copy()) 

1475 else: 

1476 for p in self: 

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

1478 aList.append(p.copy()) 

1479 if removeClones: 

1480 aList = self.removeClones(aList) 

1481 return PosList(c, aList) 

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

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

1484 seen = {} 

1485 aList2: List[Pos] = [] 

1486 for p in aList: 

1487 if p.v not in seen: 

1488 seen[p.v] = p.v 

1489 aList2.append(p) 

1490 return aList2 

1491 #@-others 

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

1493class ReadLinesClass: 

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

1495 

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

1497 self.lines = g.splitLines(s) 

1498 self.i = 0 

1499 

1500 def next(self) -> str: 

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

1502 line = self.lines[self.i] 

1503 self.i += 1 

1504 else: 

1505 line = '' 

1506 return line 

1507 

1508 __next__ = next 

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

1510class RedirectClass: 

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

1512 #@+<< RedirectClass methods >> 

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

1514 #@+others 

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

1516 def __init__(self) -> None: 

1517 self.old = None 

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

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

1520 def isRedirected(self) -> bool: 

1521 return self.old is not None 

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

1523 # For LeoN: just for compatibility. 

1524 

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

1526 return 

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

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

1529 if self.old: 

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

1531 else: 

1532 g.pr(s) 

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

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

1535 if g.app.batchMode: 

1536 # Redirection is futile in batch mode. 

1537 return 

1538 if not self.old: 

1539 if stdout: 

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

1541 else: 

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

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

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

1545 if self.old: 

1546 if stdout: 

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

1548 else: 

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

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

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

1552 

1553 if self.old: 

1554 if app.log: 

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

1556 else: 

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

1558 else: 

1559 # Can happen when g.batchMode is True. 

1560 g.pr(s) 

1561 #@-others 

1562 #@-<< RedirectClass methods >> 

1563 

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

1565 

1566redirectStdErrObj = RedirectClass() 

1567redirectStdOutObj = RedirectClass() 

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

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

1570#@+others 

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

1572# Redirect streams to the current log window. 

1573 

1574def redirectStderr() -> None: 

1575 global redirectStdErrObj 

1576 redirectStdErrObj.redirect(stdout=False) 

1577 

1578def redirectStdout() -> None: 

1579 global redirectStdOutObj 

1580 redirectStdOutObj.redirect() 

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

1582# Restore standard streams. 

1583 

1584def restoreStderr() -> None: 

1585 global redirectStdErrObj 

1586 redirectStdErrObj.undirect(stdout=False) 

1587 

1588def restoreStdout() -> None: 

1589 global redirectStdOutObj 

1590 redirectStdOutObj.undirect() 

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

1592def stdErrIsRedirected() -> bool: 

1593 global redirectStdErrObj 

1594 return redirectStdErrObj.isRedirected() 

1595 

1596def stdOutIsRedirected() -> bool: 

1597 global redirectStdOutObj 

1598 return redirectStdOutObj.isRedirected() 

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

1600# Send output to original stdout. 

1601 

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

1603 global redirectStdOutObj 

1604 redirectStdOutObj.rawPrint(s) 

1605#@-others 

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

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

1608class SherlockTracer: 

1609 """ 

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

1611 

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

1613 

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

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

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

1617 

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

1619 matches the regular expression x. 

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

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

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

1623 

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

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

1626 

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

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

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

1630 

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

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

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

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

1635 

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

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

1638 code needs to be inserted anywhere. 

1639 

1640 Usage: 

1641 

1642 g.SherlockTracer(patterns).run() 

1643 """ 

1644 #@+others 

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

1646 def __init__( 

1647 self, 

1648 patterns: List[Any], 

1649 dots: bool=True, 

1650 show_args: bool=True, 

1651 show_return: bool=True, 

1652 verbose: bool=True, 

1653 ) -> None: 

1654 """SherlockTracer ctor.""" 

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

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

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

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

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

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

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

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

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

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

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

1666 self.set_patterns(patterns) 

1667 from leo.core.leoQt import QtCore 

1668 if QtCore: 

1669 # pylint: disable=no-member 

1670 QtCore.pyqtRemoveInputHook() 

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

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

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

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

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

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

1677 """Report a bad Sherlock pattern.""" 

1678 if pattern not in self.bad_patterns: 

1679 self.bad_patterns.append(pattern) 

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

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

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

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

1684 try: 

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

1686 if pattern.startswith(prefix): 

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

1688 return True 

1689 self.bad_pattern(pattern) 

1690 return False 

1691 except Exception: 

1692 self.bad_pattern(pattern) 

1693 return False 

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

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

1696 """The dispatch method.""" 

1697 if event == 'call': 

1698 self.do_call(frame, arg) 

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

1700 self.do_return(frame, arg) 

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

1702 self.do_line(frame, arg) 

1703 # Queue the SherlockTracer instance again. 

1704 return self 

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

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

1707 """Trace through a function call.""" 

1708 frame1 = frame 

1709 code = frame.f_code 

1710 file_name = code.co_filename 

1711 locals_ = frame.f_locals 

1712 function_name = code.co_name 

1713 try: 

1714 full_name = self.get_full_name(locals_, function_name) 

1715 except Exception: 

1716 full_name = function_name 

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

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

1719 return 

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

1721 while frame: 

1722 frame = frame.f_back 

1723 n += 1 

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

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

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

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

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

1729 # Always update stats. 

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

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

1732 self.stats[file_name] = d 

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

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

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

1736 code = frame.f_code 

1737 locals_ = frame.f_locals 

1738 name = code.co_name 

1739 n = code.co_argcount 

1740 if code.co_flags & 4: 

1741 n = n + 1 

1742 if code.co_flags & 8: 

1743 n = n + 1 

1744 result = [] 

1745 for i in range(n): 

1746 name = code.co_varnames[i] 

1747 if name != 'self': 

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

1749 if arg: 

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

1751 # Clearer w/o f-string 

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

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

1754 else: 

1755 val = self.show(arg) 

1756 if val: 

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

1758 return ','.join(result) 

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

1760 bad_fns: List[str] = [] 

1761 

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

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

1764 if 1: 

1765 return 

1766 code = frame.f_code 

1767 file_name = code.co_filename 

1768 locals_ = frame.f_locals 

1769 name = code.co_name 

1770 full_name = self.get_full_name(locals_, name) 

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

1772 return 

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

1774 d = self.contents_d 

1775 lines = d.get(file_name) 

1776 if not lines: 

1777 print(file_name) 

1778 try: 

1779 with open(file_name) as f: 

1780 s = f.read() 

1781 except Exception: 

1782 if file_name not in self.bad_fns: 

1783 self.bad_fns.append(file_name) 

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

1785 return 

1786 lines = g.splitLines(s) 

1787 d[file_name] = lines 

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

1789 if 0: 

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

1791 else: 

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

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

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

1795 """Trace a return statement.""" 

1796 code = frame.f_code 

1797 fn = code.co_filename 

1798 locals_ = frame.f_locals 

1799 name = code.co_name 

1800 full_name = self.get_full_name(locals_, name) 

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

1802 n = 0 

1803 while frame: 

1804 frame = frame.f_back 

1805 n += 1 

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

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

1808 if name and name == '__init__': 

1809 try: 

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

1811 ret = self.format_ret(ret1) 

1812 except NameError: 

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

1814 else: 

1815 ret = self.format_ret(arg) 

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

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

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

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

1820 try: 

1821 if isinstance(arg, types.GeneratorType): 

1822 ret = '<generator>' 

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

1824 # Clearer w/o f-string. 

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

1826 if len(ret) > 40: 

1827 # Clearer w/o f-string. 

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

1829 elif arg: 

1830 ret = self.show(arg) 

1831 if len(ret) > 40: 

1832 ret = f"\n {ret}" 

1833 else: 

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

1835 except Exception: 

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

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

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

1839 return f" -> {ret}" 

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

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

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

1843 if func in self.ignored_functions: 

1844 return False 

1845 

1846 def ignore_function() -> None: 

1847 if func not in self.ignored_functions: 

1848 self.ignored_functions.append(func) 

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

1850 # 

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

1852 table = ( 

1853 '_deepcopy.*', 

1854 # Unicode primitives. 

1855 'encode\b', 'decode\b', 

1856 # System functions 

1857 '.*__next\b', 

1858 '<frozen>', '<genexpr>', '<listcomp>', 

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

1860 'get\b', 

1861 # String primitives. 

1862 'append\b', 'split\b', 'join\b', 

1863 # File primitives... 

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

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

1866 ) 

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

1868 for z in table: 

1869 if re.match(z, func): 

1870 ignore_function() 

1871 return False 

1872 # 

1873 # Legacy code. 

1874 try: 

1875 enabled, pattern = False, None 

1876 for pattern in patterns: 

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

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

1879 enabled = True 

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

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

1882 enabled = False 

1883 return enabled 

1884 except Exception: 

1885 self.bad_pattern(pattern) 

1886 return False 

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

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

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

1890 full_name = name 

1891 try: 

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

1893 if user_self: 

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

1895 except Exception: 

1896 pass 

1897 return full_name 

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

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

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

1901 

1902 def is_enabled( 

1903 self, 

1904 file_name: str, 

1905 function_name: str, 

1906 patterns: List[str]=None, 

1907 ) -> bool: 

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

1909 # 

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

1911 if not os: 

1912 return False # Shutting down. 

1913 base_name = os.path.basename(file_name) 

1914 if base_name in self.ignored_files: 

1915 return False 

1916 

1917 def ignore_file() -> None: 

1918 if not base_name in self.ignored_files: 

1919 self.ignored_files.append(base_name) 

1920 

1921 def ignore_function() -> None: 

1922 if function_name not in self.ignored_functions: 

1923 self.ignored_functions.append(function_name) 

1924 

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

1926 ignore_file() 

1927 return False 

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

1929 ignore_file() 

1930 return False 

1931 # 

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

1933 table = ( 

1934 '_deepcopy.*', 

1935 # Unicode primitives. 

1936 'encode\b', 'decode\b', 

1937 # System functions 

1938 '.*__next\b', 

1939 '<frozen>', '<genexpr>', '<listcomp>', 

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

1941 'get\b', 

1942 # String primitives. 

1943 'append\b', 'split\b', 'join\b', 

1944 # File primitives... 

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

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

1947 ) 

1948 for z in table: 

1949 if re.match(z, function_name): 

1950 ignore_function() 

1951 return False 

1952 # 

1953 # Legacy code. 

1954 enabled = False 

1955 if patterns is None: 

1956 patterns = self.patterns 

1957 for pattern in patterns: 

1958 try: 

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

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

1961 enabled = True 

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

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

1964 enabled = False 

1965 elif pattern.startswith('+'): 

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

1967 enabled = True 

1968 elif pattern.startswith('-'): 

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

1970 enabled = False 

1971 else: 

1972 self.bad_pattern(pattern) 

1973 except Exception: 

1974 self.bad_pattern(pattern) 

1975 return enabled 

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

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

1978 """Print all accumulated statisitics.""" 

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

1980 if not patterns: 

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

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

1983 d = self.stats.get(fn) 

1984 if self.fn_is_enabled(fn, patterns): 

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

1986 else: 

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

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

1989 if result: 

1990 print('') 

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

1992 parts = fn.split('/') 

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

1994 for key in result: 

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

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

1997 # Modified from pdb.Pdb.set_trace. 

1998 

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

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

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

2002 if frame is None: 

2003 frame = sys._getframe().f_back 

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

2005 self.n = 0 

2006 while frame: 

2007 frame = frame.f_back 

2008 self.n += 1 

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

2010 sys.settrace(self) 

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

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

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

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

2015 self.set_patterns(patterns) 

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

2017 

2018 def pop(self) -> None: 

2019 """Restore the pushed patterns.""" 

2020 if self.pattern_stack: 

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

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

2023 else: 

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

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

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

2027 """Set the patterns in effect.""" 

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

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

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

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

2032 if not item: 

2033 return repr(item) 

2034 if isinstance(item, dict): 

2035 return 'dict' 

2036 if isinstance(item, str): 

2037 s = repr(item) 

2038 if len(s) <= 20: 

2039 return s 

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

2041 return repr(item) 

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

2043 def stop(self) -> None: 

2044 """Stop all tracing.""" 

2045 sys.settrace(None) 

2046 #@-others 

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

2048class TkIDDialog(EmergencyDialog): 

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

2050 

2051 message = ( 

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

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

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

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

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

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

2058 

2059 title = 'Enter Leo id' 

2060 

2061 def __init__(self) -> None: 

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

2063 self.val = '' 

2064 

2065 #@+others 

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

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

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

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

2070 self.okButton() 

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

2072 def createTopFrame(self) -> None: 

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

2074 import tkinter as Tk 

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

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

2077 self.top.title(self.title) 

2078 self.root.withdraw() 

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

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

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

2082 label.pack(pady=10) 

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

2084 self.entry.pack() 

2085 self.entry.focus_set() 

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

2087 def okButton(self) -> None: 

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

2089 self.val = self.entry.get() 

2090 # Return is not possible. 

2091 self.top.destroy() 

2092 self.top = None 

2093 #@-others 

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

2095class Tracer: 

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

2097 

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

2099 

2100 g.startTracer() 

2101 """ 

2102 #@+others 

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

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

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

2106 # Keys are function names. 

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

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

2109 # Keys are function names. 

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

2111 self.count = 0 

2112 self.inited = False 

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

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

2115 self.trace = trace 

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

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

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

2119 if not frame: 

2120 return '' 

2121 code = frame.f_code 

2122 result = [] 

2123 module = inspect.getmodule(code) 

2124 if module: 

2125 module_name = module.__name__ 

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

2127 result.append('g') 

2128 else: 

2129 tag = 'leo.core.' 

2130 if module_name.startswith(tag): 

2131 module_name = module_name[len(tag) :] 

2132 result.append(module_name) 

2133 try: 

2134 # This can fail during startup. 

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

2136 if self_obj: 

2137 result.append(self_obj.__class__.__name__) 

2138 except Exception: 

2139 pass 

2140 result.append(code.co_name) 

2141 return '.'.join(result) 

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

2143 def report(self) -> None: 

2144 if 0: 

2145 g.pr('\nstack') 

2146 for z in self.stack: 

2147 g.pr(z) 

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

2149 for key in sorted(self.callDict): 

2150 # Print the calling function. 

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

2152 # Print the called functions. 

2153 d = self.callDict.get(key) 

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

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

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

2157 def stop(self) -> None: 

2158 sys.settrace(None) 

2159 self.report() 

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

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

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

2163 n = len(self.stack) 

2164 if event == 'return': 

2165 n = max(0, n - 1) 

2166 pad = '.' * n 

2167 if event == 'call': 

2168 if not self.inited: 

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

2170 self.inited = True 

2171 name = self.computeName(frame.f_back) 

2172 self.updateStats(name) 

2173 self.stack.append(name) 

2174 name = self.computeName(frame) 

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

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

2177 self.updateStats(name) 

2178 self.stack.append(name) 

2179 return self.tracer 

2180 if event == 'return': 

2181 if self.stack: 

2182 name = self.stack.pop() 

2183 if ( 

2184 self.trace and 

2185 self.verbose and 

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

2187 ): 

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

2189 else: 

2190 g.trace('return underflow') 

2191 self.stop() 

2192 return None 

2193 if self.stack: 

2194 return self.tracer 

2195 self.stop() 

2196 return None 

2197 return self.tracer 

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

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

2200 if not self.stack: 

2201 return 

2202 caller = self.stack[-1] 

2203 # d is a dict reprenting the called functions. 

2204 # Keys are called functions, values are counts. 

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

2206 d[name] = 1 + d.get(name, 0) 

2207 self.callDict[caller] = d 

2208 # Update the total counts. 

2209 self.calledDict[name] = 1 + self.calledDict.get(name, 0) 

2210 #@-others 

2211 

2212def startTracer(limit: int=0, trace: bool=False, verbose: bool=False) -> Callable: 

2213 t = g.Tracer(limit=limit, trace=trace, verbose=verbose) 

2214 sys.settrace(t.tracer) 

2215 return t 

2216#@+node:ekr.20031219074948.1: *3* class g.Tracing/NullObject & helpers 

2217#@@nobeautify 

2218 

2219tracing_tags: Dict[int, str] = {} # Keys are id's, values are tags. 

2220tracing_vars: Dict[int, List] = {} # Keys are id's, values are names of ivars. 

2221# Keys are signatures: '%s.%s:%s' % (tag, attr, callers). Values not important. 

2222tracing_signatures: Dict[str, Any] = {} 

2223 

2224class NullObject: 

2225 """An object that does nothing, and does it very well.""" 

2226 def __init__(self, ivars: List[str]=None, *args: Any, **kwargs: Any) -> None: 

2227 if isinstance(ivars, str): 

2228 ivars = [ivars] 

2229 tracing_vars [id(self)] = ivars or [] 

2230 def __call__(self, *args: Any, **keys: Any) -> "NullObject": 

2231 return self 

2232 def __repr__(self) -> str: 

2233 return "NullObject" 

2234 def __str__(self) -> str: 

2235 return "NullObject" 

2236 # Attribute access... 

2237 def __delattr__(self, attr: str) -> None: 

2238 return None 

2239 def __getattr__(self, attr: str) -> Any: 

2240 if attr in tracing_vars.get(id(self), []): 

2241 return getattr(self, attr, None) 

2242 return self # Required. 

2243 def __setattr__(self, attr: str, val: Any) -> None: 

2244 if attr in tracing_vars.get(id(self), []): 

2245 object.__setattr__(self, attr, val) 

2246 # Container methods.. 

2247 def __bool__(self) -> bool: 

2248 return False 

2249 def __contains__(self, item: Any) -> bool: 

2250 return False 

2251 def __getitem__(self, key: str) -> None: 

2252 raise KeyError 

2253 def __setitem__(self, key: str, val: Any) -> None: 

2254 pass 

2255 def __iter__(self) -> "NullObject": 

2256 return self 

2257 def __len__(self) -> int: 

2258 return 0 

2259 # Iteration methods: 

2260 def __next__(self) -> None: 

2261 raise StopIteration 

2262 

2263 

2264class TracingNullObject: 

2265 """Tracing NullObject.""" 

2266 def __init__(self, tag: str, ivars: List[Any]=None, *args: Any, **kwargs: Any) -> None: 

2267 tracing_tags [id(self)] = tag 

2268 if isinstance(ivars, str): 

2269 ivars = [ivars] 

2270 tracing_vars [id(self)] = ivars or [] 

2271 def __call__(self, *args: Any, **kwargs: Any) -> "TracingNullObject": 

2272 return self 

2273 def __repr__(self) -> str: 

2274 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}' 

2275 def __str__(self) -> str: 

2276 return f'TracingNullObject: {tracing_tags.get(id(self), "<NO TAG>")}' 

2277 # 

2278 # Attribute access... 

2279 def __delattr__(self, attr: str) -> None: 

2280 return None 

2281 def __getattr__(self, attr: str) -> "TracingNullObject": 

2282 null_object_print_attr(id(self), attr) 

2283 if attr in tracing_vars.get(id(self), []): 

2284 return getattr(self, attr, None) 

2285 return self # Required. 

2286 def __setattr__(self, attr: str, val: Any) -> None: 

2287 g.null_object_print(id(self), '__setattr__', attr, val) 

2288 if attr in tracing_vars.get(id(self), []): 

2289 object.__setattr__(self, attr, val) 

2290 # 

2291 # All other methods... 

2292 def __bool__(self) -> bool: 

2293 if 0: # To do: print only once. 

2294 suppress = ('getShortcut','on_idle', 'setItemText') 

2295 callers = g.callers(2) 

2296 if not callers.endswith(suppress): 

2297 g.null_object_print(id(self), '__bool__') 

2298 return False 

2299 def __contains__(self, item: Any) -> bool: 

2300 g.null_object_print(id(self), '__contains__') 

2301 return False 

2302 def __getitem__(self, key: str) -> None: 

2303 g.null_object_print(id(self), '__getitem__') 

2304 # pylint doesn't like trailing return None. 

2305 def __iter__(self) -> "TracingNullObject": 

2306 g.null_object_print(id(self), '__iter__') 

2307 return self 

2308 def __len__(self) -> int: 

2309 # g.null_object_print(id(self), '__len__') 

2310 return 0 

2311 def __next__(self) -> None: 

2312 g.null_object_print(id(self), '__next__') 

2313 raise StopIteration 

2314 def __setitem__(self, key: str, val: Any) -> None: 

2315 g.null_object_print(id(self), '__setitem__') 

2316 # pylint doesn't like trailing return None. 

2317#@+node:ekr.20190330062625.1: *4* g.null_object_print_attr 

2318def null_object_print_attr(id_: int, attr: str) -> None: 

2319 suppress = True 

2320 suppress_callers: List[str] = [] 

2321 suppress_attrs: List[str] = [] 

2322 if suppress: 

2323 #@+<< define suppression lists >> 

2324 #@+node:ekr.20190330072026.1: *5* << define suppression lists >> 

2325 suppress_callers = [ 

2326 'drawNode', 'drawTopTree', 'drawTree', 

2327 'contractItem', 'getCurrentItem', 

2328 'declutter_node', 

2329 'finishCreate', 

2330 'initAfterLoad', 

2331 'show_tips', 

2332 'writeWaitingLog', 

2333 # 'set_focus', 'show_tips', 

2334 ] 

2335 suppress_attrs = [ 

2336 # Leo... 

2337 'c.frame.body.wrapper', 

2338 'c.frame.getIconBar.add', 

2339 'c.frame.log.createTab', 

2340 'c.frame.log.enable', 

2341 'c.frame.log.finishCreate', 

2342 'c.frame.menu.createMenuBar', 

2343 'c.frame.menu.finishCreate', 

2344 # 'c.frame.menu.getMenu', 

2345 'currentItem', 

2346 'dw.leo_master.windowTitle', 

2347 # Pyzo... 

2348 'pyzo.keyMapper.connect', 

2349 'pyzo.keyMapper.keyMappingChanged', 

2350 'pyzo.keyMapper.setShortcut', 

2351 ] 

2352 #@-<< define suppression lists >> 

2353 tag = tracing_tags.get(id_, "<NO TAG>") 

2354 callers = g.callers(3).split(',') 

2355 callers = ','.join(callers[:-1]) 

2356 in_callers = any(z in callers for z in suppress_callers) 

2357 s = f"{tag}.{attr}" 

2358 if suppress: 

2359 # Filter traces. 

2360 if not in_callers and s not in suppress_attrs: 

2361 g.pr(f"{s:40} {callers}") 

2362 else: 

2363 # Print each signature once. No need to filter! 

2364 signature = f"{tag}.{attr}:{callers}" 

2365 if signature not in tracing_signatures: 

2366 tracing_signatures[signature] = True 

2367 g.pr(f"{s:40} {callers}") 

2368#@+node:ekr.20190330072832.1: *4* g.null_object_print 

2369def null_object_print(id_: int, kind: Any, *args: Any) -> None: 

2370 tag = tracing_tags.get(id_, "<NO TAG>") 

2371 callers = g.callers(3).split(',') 

2372 callers = ','.join(callers[:-1]) 

2373 s = f"{kind}.{tag}" 

2374 signature = f"{s}:{callers}" 

2375 if 1: 

2376 # Always print: 

2377 if args: 

2378 args_s = ', '.join([repr(z) for z in args]) 

2379 g.pr(f"{s:40} {callers}\n\t\t\targs: {args_s}") 

2380 else: 

2381 g.pr(f"{s:40} {callers}") 

2382 elif signature not in tracing_signatures: 

2383 # Print each signature once. 

2384 tracing_signatures[signature] = True 

2385 g.pr(f"{s:40} {callers}") 

2386#@+node:ekr.20120129181245.10220: *3* class g.TypedDict 

2387class TypedDict: 

2388 """ 

2389 A class providing additional dictionary-related methods: 

2390 

2391 __init__: Specifies types and the dict's name. 

2392 __repr__: Compatible with g.printObj, based on g.objToString. 

2393 __setitem__: Type checks its arguments. 

2394 __str__: A concise summary of the inner dict. 

2395 add_to_list: A convenience method that adds a value to its key's list. 

2396 name: The dict's name. 

2397 setName: Sets the dict's name, for use by __repr__. 

2398 

2399 Overrides the following standard methods: 

2400 

2401 copy: A thin wrapper for copy.deepcopy. 

2402 get: Returns self.d.get 

2403 items: Returns self.d.items 

2404 keys: Returns self.d.keys 

2405 update: Updates self.d from either a dict or a TypedDict. 

2406 """ 

2407 

2408 def __init__(self, name: str, keyType: Any, valType: Any) -> None: 

2409 self.d: Dict[str, Any] = {} 

2410 self._name = name # For __repr__ only. 

2411 self.keyType = keyType 

2412 self.valType = valType 

2413 #@+others 

2414 #@+node:ekr.20120205022040.17770: *4* td.__repr__ & __str__ 

2415 def __str__(self) -> str: 

2416 """Concise: used by repr.""" 

2417 return ( 

2418 f"<TypedDict name:{self._name} " 

2419 f"keys:{self.keyType.__name__} " 

2420 f"values:{self.valType.__name__} " 

2421 f"len(keys): {len(list(self.keys()))}>" 

2422 ) 

2423 

2424 def __repr__(self) -> str: 

2425 """Suitable for g.printObj""" 

2426 return f"{g.dictToString(self.d)}\n{str(self)}\n" 

2427 #@+node:ekr.20120205022040.17774: *4* td.__setitem__ 

2428 def __setitem__(self, key: Any, val: Any) -> None: 

2429 """Allow d[key] = val""" 

2430 if key is None: 

2431 g.trace('TypeDict: None is not a valid key', g.callers()) 

2432 return 

2433 self._checkKeyType(key) 

2434 try: 

2435 for z in val: 

2436 self._checkValType(z) 

2437 except TypeError: 

2438 self._checkValType(val) # val is not iterable. 

2439 self.d[key] = val 

2440 #@+node:ekr.20190904052828.1: *4* td.add_to_list 

2441 def add_to_list(self, key: Any, val: Any) -> None: 

2442 """Update the *list*, self.d [key]""" 

2443 if key is None: 

2444 g.trace('TypeDict: None is not a valid key', g.callers()) 

2445 return 

2446 self._checkKeyType(key) 

2447 self._checkValType(val) 

2448 aList = self.d.get(key, []) 

2449 if val not in aList: 

2450 aList.append(val) 

2451 self.d[key] = aList 

2452 #@+node:ekr.20120206134955.10150: *4* td.checking 

2453 def _checkKeyType(self, key: str) -> None: 

2454 if key and key.__class__ != self.keyType: 

2455 self._reportTypeError(key, self.keyType) 

2456 

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

2458 if val.__class__ != self.valType: 

2459 self._reportTypeError(val, self.valType) 

2460 

2461 def _reportTypeError(self, obj: Any, objType: Any) -> str: 

2462 return ( 

2463 f"{self._name}\n" 

2464 f"expected: {obj.__class__.__name__}\n" 

2465 f" got: {objType.__name__}") 

2466 #@+node:ekr.20120223062418.10422: *4* td.copy 

2467 def copy(self, name: str=None) -> Any: 

2468 """Return a new dict with the same contents.""" 

2469 import copy 

2470 return copy.deepcopy(self) 

2471 #@+node:ekr.20120205022040.17771: *4* td.get & keys & values 

2472 def get(self, key: Any, default: Any=None) -> Any: 

2473 return self.d.get(key, default) 

2474 

2475 def items(self) -> Any: 

2476 return self.d.items() 

2477 

2478 def keys(self) -> Any: 

2479 return self.d.keys() 

2480 

2481 def values(self) -> Any: 

2482 return self.d.values() 

2483 #@+node:ekr.20190903181030.1: *4* td.get_setting & get_string_setting 

2484 def get_setting(self, key: str) -> Any: 

2485 key = key.replace('-', '').replace('_', '') 

2486 gs = self.get(key) 

2487 val = gs and gs.val 

2488 return val 

2489 

2490 def get_string_setting(self, key: str) -> Optional[str]: 

2491 val = self.get_setting(key) 

2492 return val if val and isinstance(val, str) else None 

2493 #@+node:ekr.20190904103552.1: *4* td.name & setName 

2494 def name(self) -> str: 

2495 return self._name 

2496 

2497 def setName(self, name: str) -> None: 

2498 self._name = name 

2499 #@+node:ekr.20120205022040.17807: *4* td.update 

2500 def update(self, d: Dict[Any, Any]) -> None: 

2501 """Update self.d from a the appropriate dict.""" 

2502 if isinstance(d, TypedDict): 

2503 self.d.update(d.d) 

2504 else: 

2505 self.d.update(d) 

2506 #@-others 

2507#@+node:ville.20090827174345.9963: *3* class g.UiTypeException & g.assertui 

2508class UiTypeException(Exception): 

2509 pass 

2510 

2511def assertUi(uitype: Any) -> None: 

2512 if not g.app.gui.guiName() == uitype: 

2513 raise UiTypeException 

2514#@+node:ekr.20200219071828.1: *3* class TestLeoGlobals (leoGlobals.py) 

2515class TestLeoGlobals(unittest.TestCase): 

2516 """Tests for leoGlobals.py.""" 

2517 #@+others 

2518 #@+node:ekr.20200219071958.1: *4* test_comment_delims_from_extension 

2519 def test_comment_delims_from_extension(self) -> None: 

2520 

2521 # pylint: disable=import-self 

2522 from leo.core import leoGlobals as leo_g 

2523 from leo.core import leoApp 

2524 leo_g.app = leoApp.LeoApp() 

2525 assert leo_g.comment_delims_from_extension(".py") == ('#', '', '') 

2526 assert leo_g.comment_delims_from_extension(".c") == ('//', '/*', '*/') 

2527 assert leo_g.comment_delims_from_extension(".html") == ('', '<!--', '-->') 

2528 #@+node:ekr.20200219072957.1: *4* test_is_sentinel 

2529 def test_is_sentinel(self) -> None: 

2530 

2531 # pylint: disable=import-self 

2532 from leo.core import leoGlobals as leo_g 

2533 # Python. 

2534 py_delims = leo_g.comment_delims_from_extension('.py') 

2535 assert leo_g.is_sentinel("#@+node", py_delims) 

2536 assert not leo_g.is_sentinel("#comment", py_delims) 

2537 # C. 

2538 c_delims = leo_g.comment_delims_from_extension('.c') 

2539 assert leo_g.is_sentinel("//@+node", c_delims) 

2540 assert not g.is_sentinel("//comment", c_delims) 

2541 # Html. 

2542 html_delims = leo_g.comment_delims_from_extension('.html') 

2543 assert leo_g.is_sentinel("<!--@+node-->", html_delims) 

2544 assert not leo_g.is_sentinel("<!--comment-->", html_delims) 

2545 #@-others 

2546#@+node:ekr.20140904112935.18526: *3* g.isTextWrapper & isTextWidget 

2547def isTextWidget(w: Any) -> bool: 

2548 return g.app.gui.isTextWidget(w) 

2549 

2550def isTextWrapper(w: Any) -> bool: 

2551 return g.app.gui.isTextWrapper(w) 

2552#@+node:ekr.20160518074224.1: *3* class g.LinterTable 

2553class LinterTable(): 

2554 """A class to encapsulate lists of leo modules under test.""" 

2555 

2556 def __init__(self) -> None: 

2557 """Ctor for LinterTable class.""" 

2558 # Define self. relative to leo.core.leoGlobals 

2559 self.loadDir = g.os_path_finalize_join(g.__file__, '..', '..') 

2560 #@+others 

2561 #@+node:ekr.20160518074545.2: *4* commands 

2562 def commands(self) -> List: 

2563 """Return list of all command modules in leo/commands.""" 

2564 pattern = g.os_path_finalize_join(self.loadDir, 'commands', '*.py') 

2565 return self.get_files(pattern) 

2566 #@+node:ekr.20160518074545.3: *4* core 

2567 def core(self) -> List: 

2568 """Return list of all of Leo's core files.""" 

2569 pattern = g.os_path_finalize_join(self.loadDir, 'core', 'leo*.py') 

2570 aList = self.get_files(pattern) 

2571 for fn in ['runLeo.py',]: 

2572 aList.append(g.os_path_finalize_join(self.loadDir, 'core', fn)) 

2573 return sorted(aList) 

2574 #@+node:ekr.20160518074545.4: *4* external 

2575 def external(self) -> List: 

2576 """Return list of files in leo/external""" 

2577 pattern = g.os_path_finalize_join(self.loadDir, 'external', 'leo*.py') 

2578 aList = self.get_files(pattern) 

2579 remove = [ 

2580 'leoSAGlobals.py', 

2581 'leoftsindex.py', 

2582 ] 

2583 remove = [g.os_path_finalize_join(self.loadDir, 'external', fn) for fn in remove] 

2584 return sorted([z for z in aList if z not in remove]) 

2585 #@+node:ekr.20160520093506.1: *4* get_files (LinterTable) 

2586 def get_files(self, pattern: str) -> List: 

2587 """Return the list of absolute file names matching the pattern.""" 

2588 aList = sorted([ 

2589 fn for fn in g.glob_glob(pattern) 

2590 if g.os_path_isfile(fn) and g.shortFileName(fn) != '__init__.py']) 

2591 return aList 

2592 #@+node:ekr.20160518074545.9: *4* get_files_for_scope 

2593 def get_files_for_scope(self, scope: str, fn: str) -> List: 

2594 """Return a list of absolute filenames for external linters.""" 

2595 d = { 

2596 'all': [self.core, self.commands, self.external, self.plugins], 

2597 'commands': [self.commands], 

2598 'core': [self.core, self.commands, self.external, self.gui_plugins], 

2599 'external': [self.external], 

2600 'file': [fn], 

2601 'gui': [self.gui_plugins], 

2602 'modes': [self.modes], 

2603 'plugins': [self.plugins], 

2604 'tests': [self.tests], 

2605 } 

2606 suppress_list = ['freewin.py',] 

2607 functions = d.get(scope) 

2608 paths = [] 

2609 if functions: 

2610 for func in functions: 

2611 files = [func] if isinstance(func, str) else func() 

2612 # Bug fix: 2016/10/15 

2613 for fn in files: 

2614 fn = g.os_path_abspath(fn) 

2615 if g.shortFileName(fn) in suppress_list: 

2616 print(f"\npylint-leo: skip {fn}") 

2617 continue 

2618 if g.os_path_exists(fn): 

2619 if g.os_path_isfile(fn): 

2620 paths.append(fn) 

2621 else: 

2622 print(f"does not exist: {fn}") 

2623 paths = sorted(set(paths)) 

2624 return paths 

2625 print('LinterTable.get_table: bad scope', scope) 

2626 return [] 

2627 #@+node:ekr.20160518074545.5: *4* gui_plugins 

2628 def gui_plugins(self) -> List: 

2629 """Return list of all of Leo's gui-related files.""" 

2630 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', 'qt_*.py') 

2631 aList = self.get_files(pattern) 

2632 # These are not included, because they don't start with 'qt_': 

2633 add = ['free_layout.py', 'nested_splitter.py',] 

2634 remove = [ 

2635 'qt_main.py', # auto-generated file. 

2636 ] 

2637 for fn in add: 

2638 aList.append(g.os_path_finalize_join(self.loadDir, 'plugins', fn)) 

2639 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove] 

2640 return sorted(set([z for z in aList if z not in remove])) 

2641 #@+node:ekr.20160518074545.6: *4* modes 

2642 def modes(self) -> List: 

2643 """Return list of all files in leo/modes""" 

2644 pattern = g.os_path_finalize_join(self.loadDir, 'modes', '*.py') 

2645 return self.get_files(pattern) 

2646 #@+node:ekr.20160518074545.8: *4* plugins (LinterTable) 

2647 def plugins(self) -> List: 

2648 """Return a list of all important plugins.""" 

2649 aList = [] 

2650 for theDir in ('', 'importers', 'writers'): 

2651 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', theDir, '*.py') 

2652 aList.extend(self.get_files(pattern)) 

2653 # Don't use get_files here. 

2654 # for fn in g.glob_glob(pattern): 

2655 # sfn = g.shortFileName(fn) 

2656 # if sfn != '__init__.py': 

2657 # sfn = os.sep.join([theDir, sfn]) if theDir else sfn 

2658 # aList.append(sfn) 

2659 remove = [ 

2660 # 2016/05/20: *do* include gui-related plugins. 

2661 # This allows the -a option not to doubly-include gui-related plugins. 

2662 # 'free_layout.py', # Gui-related. 

2663 # 'nested_splitter.py', # Gui-related. 

2664 'gtkDialogs.py', # Many errors, not important. 

2665 'leofts.py', # Not (yet) in leoPlugins.leo. 

2666 'qtGui.py', # Dummy file 

2667 'qt_main.py', # Created automatically. 

2668 'viewrendered2.py', # To be removed. 

2669 'rst3.py', # Obsolete 

2670 ] 

2671 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove] 

2672 aList = sorted([z for z in aList if z not in remove]) 

2673 return sorted(set(aList)) 

2674 #@+node:ekr.20211115103929.1: *4* tests (LinterTable) 

2675 def tests(self) -> List: 

2676 """Return list of files in leo/unittests""" 

2677 aList = [] 

2678 for theDir in ('', 'commands', 'core', 'plugins'): 

2679 pattern = g.os_path_finalize_join(self.loadDir, 'unittests', theDir, '*.py') 

2680 aList.extend(self.get_files(pattern)) 

2681 remove = [ 

2682 'py3_test_grammar.py', 

2683 ] 

2684 remove = [g.os_path_finalize_join(self.loadDir, 'unittests', fn) for fn in remove] 

2685 return sorted([z for z in aList if z not in remove]) 

2686 #@-others 

2687#@+node:ekr.20140711071454.17649: ** g.Debugging, GC, Stats & Timing 

2688#@+node:ekr.20031218072017.3104: *3* g.Debugging 

2689#@+node:ekr.20180415144534.1: *4* g.assert_is 

2690def assert_is(obj: Any, list_or_class: Any, warn: bool=True) -> bool: 

2691 

2692 if warn: 

2693 ok = isinstance(obj, list_or_class) 

2694 if not ok: 

2695 g.es_print( 

2696 f"can not happen. {obj !r}: " 

2697 f"expected {list_or_class}, " 

2698 f"got: {obj.__class__.__name__}") 

2699 g.es_print(g.callers()) 

2700 return ok 

2701 ok = isinstance(obj, list_or_class) 

2702 assert ok, (obj, obj.__class__.__name__, g.callers()) 

2703 return ok 

2704#@+node:ekr.20180420081530.1: *4* g._assert 

2705def _assert(condition: Any, show_callers: bool=True) -> bool: 

2706 """A safer alternative to a bare assert.""" 

2707 if g.unitTesting: 

2708 assert condition 

2709 return True 

2710 ok = bool(condition) 

2711 if ok: 

2712 return True 

2713 g.es_print('\n===== g._assert failed =====\n') 

2714 if show_callers: 

2715 g.es_print(g.callers()) 

2716 return False 

2717#@+node:ekr.20051023083258: *4* g.callers & g.caller & _callerName 

2718def callers(n: int=4, count: int=0, excludeCaller: bool=True, verbose: bool=False) -> str: 

2719 """ 

2720 Return a string containing a comma-separated list of the callers 

2721 of the function that called g.callerList. 

2722 

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

2724 

2725 If the `verbose` keyword is True, return a list separated by newlines. 

2726 """ 

2727 # Be careful to call g._callerName with smaller values of i first: 

2728 # sys._getframe throws ValueError if there are less than i entries. 

2729 result = [] 

2730 i = 3 if excludeCaller else 2 

2731 while 1: 

2732 s = _callerName(n=i, verbose=verbose) 

2733 if s: 

2734 result.append(s) 

2735 if not s or len(result) >= n: 

2736 break 

2737 i += 1 

2738 result.reverse() 

2739 if count > 0: 

2740 result = result[:count] 

2741 if verbose: 

2742 return ''.join([f"\n {z}" for z in result]) 

2743 return ','.join(result) 

2744#@+node:ekr.20031218072017.3107: *5* g._callerName 

2745def _callerName(n: int, verbose: bool=False) -> str: 

2746 try: 

2747 # get the function name from the call stack. 

2748 f1 = sys._getframe(n) # The stack frame, n levels up. 

2749 code1 = f1.f_code # The code object 

2750 sfn = shortFilename(code1.co_filename) # The file name. 

2751 locals_ = f1.f_locals # The local namespace. 

2752 name = code1.co_name 

2753 line = code1.co_firstlineno 

2754 if verbose: 

2755 obj = locals_.get('self') 

2756 full_name = f"{obj.__class__.__name__}.{name}" if obj else name 

2757 return f"line {line:4} {sfn:>30} {full_name}" 

2758 return name 

2759 except ValueError: 

2760 return '' 

2761 # The stack is not deep enough OR 

2762 # sys._getframe does not exist on this platform. 

2763 except Exception: 

2764 es_exception() 

2765 return '' # "<no caller name>" 

2766#@+node:ekr.20180328170441.1: *5* g.caller 

2767def caller(i: int=1) -> str: 

2768 """Return the caller name i levels up the stack.""" 

2769 return g.callers(i + 1).split(',')[0] 

2770#@+node:ekr.20031218072017.3109: *4* g.dump 

2771def dump(s: str) -> str: 

2772 out = "" 

2773 for i in s: 

2774 out += str(ord(i)) + "," 

2775 return out 

2776 

2777def oldDump(s: str) -> str: 

2778 out = "" 

2779 for i in s: 

2780 if i == '\n': 

2781 out += "[" 

2782 out += "n" 

2783 out += "]" 

2784 if i == '\t': 

2785 out += "[" 

2786 out += "t" 

2787 out += "]" 

2788 elif i == ' ': 

2789 out += "[" 

2790 out += " " 

2791 out += "]" 

2792 else: 

2793 out += i 

2794 return out 

2795#@+node:ekr.20210904114446.1: *4* g.dump_tree & g.tree_to_string 

2796def dump_tree(c: Cmdr, dump_body: bool=False, msg: str=None) -> None: 

2797 if msg: 

2798 print(msg.rstrip()) 

2799 else: 

2800 print('') 

2801 for p in c.all_positions(): 

2802 print(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}") 

2803 if dump_body: 

2804 for z in g.splitLines(p.b): 

2805 print(z.rstrip()) 

2806 

2807def tree_to_string(c: Cmdr, dump_body: bool=False, msg: str=None) -> str: 

2808 result = ['\n'] 

2809 if msg: 

2810 result.append(msg) 

2811 for p in c.all_positions(): 

2812 result.append(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}") 

2813 if dump_body: 

2814 for z in g.splitLines(p.b): 

2815 result.append(z.rstrip()) 

2816 return '\n'.join(result) 

2817#@+node:ekr.20150227102835.8: *4* g.dump_encoded_string 

2818def dump_encoded_string(encoding: str, s: str) -> None: 

2819 """Dump s, assumed to be an encoded string.""" 

2820 # Can't use g.trace here: it calls this function! 

2821 print(f"dump_encoded_string: {g.callers()}") 

2822 print(f"dump_encoded_string: encoding {encoding}\n") 

2823 print(s) 

2824 in_comment = False 

2825 for ch in s: 

2826 if ch == '#': 

2827 in_comment = True 

2828 elif not in_comment: 

2829 print(f"{ord(ch):02x} {repr(ch)}") 

2830 elif ch == '\n': 

2831 in_comment = False 

2832#@+node:ekr.20031218072017.1317: *4* g.file/module/plugin_date 

2833def module_date(mod: Any, format: str=None) -> str: 

2834 theFile = g.os_path_join(app.loadDir, mod.__file__) 

2835 root, ext = g.os_path_splitext(theFile) 

2836 return g.file_date(root + ".py", format=format) 

2837 

2838def plugin_date(plugin_mod: Any, format: str=None) -> str: 

2839 theFile = g.os_path_join(app.loadDir, "..", "plugins", plugin_mod.__file__) 

2840 root, ext = g.os_path_splitext(theFile) 

2841 return g.file_date(root + ".py", format=str) 

2842 

2843def file_date(theFile: Any, format: str=None) -> str: 

2844 if theFile and g.os_path_exists(theFile): 

2845 try: 

2846 n = g.os_path_getmtime(theFile) 

2847 if format is None: 

2848 format = "%m/%d/%y %H:%M:%S" 

2849 return time.strftime(format, time.gmtime(n)) 

2850 except(ImportError, NameError): 

2851 pass # Time module is platform dependent. 

2852 return "" 

2853#@+node:ekr.20031218072017.3127: *4* g.get_line & get_line__after 

2854# Very useful for tracing. 

2855 

2856def get_line(s: str, i: int) -> str: 

2857 nl = "" 

2858 if g.is_nl(s, i): 

2859 i = g.skip_nl(s, i) 

2860 nl = "[nl]" 

2861 j = g.find_line_start(s, i) 

2862 k = g.skip_to_end_of_line(s, i) 

2863 return nl + s[j:k] 

2864 

2865# Important: getLine is a completely different function. 

2866# getLine = get_line 

2867 

2868def get_line_after(s: str, i: int) -> str: 

2869 nl = "" 

2870 if g.is_nl(s, i): 

2871 i = g.skip_nl(s, i) 

2872 nl = "[nl]" 

2873 k = g.skip_to_end_of_line(s, i) 

2874 return nl + s[i:k] 

2875 

2876getLineAfter = get_line_after 

2877#@+node:ekr.20080729142651.1: *4* g.getIvarsDict and checkUnchangedIvars 

2878def getIvarsDict(obj: Any) -> Dict[str, Any]: 

2879 """Return a dictionary of ivars:values for non-methods of obj.""" 

2880 d: Dict[str, Any] = dict( 

2881 [[key, getattr(obj, key)] for key in dir(obj) # type:ignore 

2882 if not isinstance(getattr(obj, key), types.MethodType)]) 

2883 return d 

2884 

2885def checkUnchangedIvars( 

2886 obj: Any, 

2887 d: Dict[str, Any], 

2888 exceptions: Sequence[str]=None, 

2889) -> bool: 

2890 if not exceptions: 

2891 exceptions = [] 

2892 ok = True 

2893 for key in d: 

2894 if key not in exceptions: 

2895 if getattr(obj, key) != d.get(key): 

2896 g.trace( 

2897 f"changed ivar: {key} " 

2898 f"old: {repr(d.get(key))} " 

2899 f"new: {repr(getattr(obj, key))}") 

2900 ok = False 

2901 return ok 

2902#@+node:ekr.20031218072017.3128: *4* g.pause 

2903def pause(s: str) -> None: 

2904 g.pr(s) 

2905 i = 0 

2906 while i < 1000 * 1000: 

2907 i += 1 

2908#@+node:ekr.20041105091148: *4* g.pdb 

2909def pdb(message: str='') -> None: 

2910 """Fall into pdb.""" 

2911 import pdb # Required: we have just defined pdb as a function! 

2912 if app and not app.useIpython: 

2913 try: 

2914 from leo.core.leoQt import QtCore 

2915 QtCore.pyqtRemoveInputHook() 

2916 except Exception: 

2917 pass 

2918 if message: 

2919 print(message) 

2920 # pylint: disable=forgotten-debug-statement 

2921 pdb.set_trace() 

2922#@+node:ekr.20041224080039: *4* g.dictToString 

2923def dictToString(d: Dict[str, str], indent: str='', tag: str=None) -> str: 

2924 """Pretty print a Python dict to a string.""" 

2925 # pylint: disable=unnecessary-lambda 

2926 if not d: 

2927 return '{}' 

2928 result = ['{\n'] 

2929 indent2 = indent + ' ' * 4 

2930 n = 2 + len(indent) + max([len(repr(z)) for z in d.keys()]) 

2931 for i, key in enumerate(sorted(d, key=lambda z: repr(z))): 

2932 pad = ' ' * max(0, (n - len(repr(key)))) 

2933 result.append(f"{pad}{key}:") 

2934 result.append(objToString(d.get(key), indent=indent2)) 

2935 if i + 1 < len(d.keys()): 

2936 result.append(',') 

2937 result.append('\n') 

2938 result.append(indent + '}') 

2939 s = ''.join(result) 

2940 return f"{tag}...\n{s}\n" if tag else s 

2941#@+node:ekr.20041126060136: *4* g.listToString 

2942def listToString(obj: Any, indent: str='', tag: str=None) -> str: 

2943 """Pretty print a Python list to a string.""" 

2944 if not obj: 

2945 return '[]' 

2946 result = ['['] 

2947 indent2 = indent + ' ' * 4 

2948 # I prefer not to compress lists. 

2949 for i, obj2 in enumerate(obj): 

2950 result.append('\n' + indent2) 

2951 result.append(objToString(obj2, indent=indent2)) 

2952 if i + 1 < len(obj) > 1: 

2953 result.append(',') 

2954 else: 

2955 result.append('\n' + indent) 

2956 result.append(']') 

2957 s = ''.join(result) 

2958 return f"{tag}...\n{s}\n" if tag else s 

2959#@+node:ekr.20050819064157: *4* g.objToSTring & g.toString 

2960def objToString(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> str: 

2961 """Pretty print any Python object to a string.""" 

2962 # pylint: disable=undefined-loop-variable 

2963 # Looks like a a pylint bug. 

2964 # 

2965 # Compute s. 

2966 if isinstance(obj, dict): 

2967 s = dictToString(obj, indent=indent) 

2968 elif isinstance(obj, list): 

2969 s = listToString(obj, indent=indent) 

2970 elif isinstance(obj, tuple): 

2971 s = tupleToString(obj, indent=indent) 

2972 elif isinstance(obj, str): 

2973 # Print multi-line strings as lists. 

2974 s = obj 

2975 lines = g.splitLines(s) 

2976 if len(lines) > 1: 

2977 s = listToString(lines, indent=indent) 

2978 else: 

2979 s = repr(s) 

2980 else: 

2981 s = repr(obj) 

2982 # 

2983 # Compute the return value. 

2984 if printCaller and tag: 

2985 prefix = f"{g.caller()}: {tag}" 

2986 elif printCaller or tag: 

2987 prefix = g.caller() if printCaller else tag 

2988 else: 

2989 prefix = '' 

2990 if prefix: 

2991 sep = '\n' if '\n' in s else ' ' 

2992 return f"{prefix}:{sep}{s}" 

2993 return s 

2994 

2995toString = objToString 

2996#@+node:ekr.20140401054342.16844: *4* g.run_pylint 

2997def run_pylint( 

2998 fn: str, # Path to file under test. 

2999 rc: str, # Path to settings file. 

3000 dots: bool=True, # Show level dots in Sherlock traces. 

3001 patterns: List[str]=None, # List of Sherlock trace patterns. 

3002 sherlock: bool=False, # Enable Sherlock tracing. 

3003 show_return: bool=True, # Show returns in Sherlock traces. 

3004 stats_patterns: bool=None, # Patterns for Sherlock statistics. 

3005 verbose: bool=True, # Show filenames in Sherlock traces. 

3006) -> None: 

3007 """ 

3008 Run pylint with the given args, with Sherlock tracing if requested. 

3009 

3010 **Do not assume g.app exists.** 

3011 

3012 run() in pylint-leo.py and PylintCommand.run_pylint *optionally* call this function. 

3013 """ 

3014 try: 

3015 from pylint import lint #type:ignore 

3016 except ImportError: 

3017 g.trace('can not import pylint') 

3018 return 

3019 if not g.os_path_exists(fn): 

3020 g.trace('does not exist:', fn) 

3021 return 

3022 if not g.os_path_exists(rc): 

3023 g.trace('does not exist', rc) 

3024 return 

3025 args = [f"--rcfile={rc}"] 

3026 # Prints error number. 

3027 # args.append('--msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}') 

3028 args.append(fn) 

3029 if sherlock: 

3030 sherlock = g.SherlockTracer( 

3031 dots=dots, 

3032 show_return=show_return, 

3033 verbose=True, # verbose: show filenames. 

3034 patterns=patterns or [], 

3035 ) 

3036 try: 

3037 sherlock.run() 

3038 lint.Run(args) 

3039 finally: 

3040 sherlock.stop() 

3041 sherlock.print_stats(patterns=stats_patterns or []) 

3042 else: 

3043 # print('run_pylint: %s' % g.shortFileName(fn)) 

3044 try: 

3045 lint.Run(args) # does sys.exit 

3046 finally: 

3047 # Printing does not work well here. 

3048 # When not waiting, printing from severl process can be interspersed. 

3049 pass 

3050#@+node:ekr.20120912153732.10597: *4* g.wait 

3051def sleep(n: float) -> None: 

3052 """Wait about n milliseconds.""" 

3053 from time import sleep # type:ignore 

3054 sleep(n) # type:ignore 

3055#@+node:ekr.20171023140544.1: *4* g.printObj & aliases 

3056def printObj(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> None: 

3057 """Pretty print any Python object using g.pr.""" 

3058 g.pr(objToString(obj, indent=indent, printCaller=printCaller, tag=tag)) 

3059 

3060printDict = printObj 

3061printList = printObj 

3062printTuple = printObj 

3063#@+node:ekr.20171023110057.1: *4* g.tupleToString 

3064def tupleToString(obj: Any, indent: str='', tag: str=None) -> str: 

3065 """Pretty print a Python tuple to a string.""" 

3066 if not obj: 

3067 return '(),' 

3068 result = ['('] 

3069 indent2 = indent + ' ' * 4 

3070 for i, obj2 in enumerate(obj): 

3071 if len(obj) > 1: 

3072 result.append('\n' + indent2) 

3073 result.append(objToString(obj2, indent=indent2)) 

3074 if len(obj) == 1 or i + 1 < len(obj): 

3075 result.append(',') 

3076 elif len(obj) > 1: 

3077 result.append('\n' + indent) 

3078 result.append(')') 

3079 s = ''.join(result) 

3080 return f"{tag}...\n{s}\n" if tag else s 

3081#@+node:ekr.20031218072017.1588: *3* g.Garbage Collection 

3082#@+node:ekr.20031218072017.1589: *4* g.clearAllIvars 

3083def clearAllIvars(o: Any) -> None: 

3084 """Clear all ivars of o, a member of some class.""" 

3085 if o: 

3086 o.__dict__.clear() 

3087#@+node:ekr.20060127162818: *4* g.enable_gc_debug 

3088def enable_gc_debug() -> None: 

3089 

3090 gc.set_debug( 

3091 gc.DEBUG_STATS | # prints statistics. 

3092 gc.DEBUG_LEAK | # Same as all below. 

3093 gc.DEBUG_COLLECTABLE | 

3094 gc.DEBUG_UNCOLLECTABLE | 

3095 # gc.DEBUG_INSTANCES | 

3096 # gc.DEBUG_OBJECTS | 

3097 gc.DEBUG_SAVEALL) 

3098#@+node:ekr.20031218072017.1592: *4* g.printGc 

3099# Formerly called from unit tests. 

3100 

3101def printGc() -> None: 

3102 """Called from trace_gc_plugin.""" 

3103 g.printGcSummary() 

3104 g.printGcObjects() 

3105 g.printGcRefs() 

3106#@+node:ekr.20060127164729.1: *4* g.printGcObjects 

3107lastObjectCount = 0 

3108 

3109def printGcObjects() -> int: 

3110 """Print a summary of GC statistics.""" 

3111 global lastObjectCount 

3112 n = len(gc.garbage) 

3113 n2 = len(gc.get_objects()) 

3114 delta = n2 - lastObjectCount 

3115 print('-' * 30) 

3116 print(f"garbage: {n}") 

3117 print(f"{delta:6d} = {n2:7d} totals") 

3118 # print number of each type of object. 

3119 d: Dict[str, int] = {} 

3120 count = 0 

3121 for obj in gc.get_objects(): 

3122 key = str(type(obj)) 

3123 n = d.get(key, 0) 

3124 d[key] = n + 1 

3125 count += 1 

3126 print(f"{count:7} objects...") 

3127 # Invert the dict. 

3128 d2: Dict[int, str] = {v: k for k, v in d.items()} 

3129 for key in reversed(sorted(d2.keys())): # type:ignore 

3130 val = d2.get(key) # type:ignore 

3131 print(f"{key:7} {val}") 

3132 lastObjectCount = count 

3133 return delta 

3134#@+node:ekr.20031218072017.1593: *4* g.printGcRefs 

3135def printGcRefs() -> None: 

3136 

3137 refs = gc.get_referrers(app.windowList[0]) 

3138 print(f"{len(refs):d} referers") 

3139#@+node:ekr.20060205043324.1: *4* g.printGcSummary 

3140def printGcSummary() -> None: 

3141 

3142 g.enable_gc_debug() 

3143 try: 

3144 n = len(gc.garbage) 

3145 n2 = len(gc.get_objects()) 

3146 s = f"printGCSummary: garbage: {n}, objects: {n2}" 

3147 print(s) 

3148 except Exception: 

3149 traceback.print_exc() 

3150#@+node:ekr.20180528151850.1: *3* g.printTimes 

3151def printTimes(times: List) -> None: 

3152 """ 

3153 Print the differences in the times array. 

3154 

3155 times: an array of times (calls to time.process_time()). 

3156 """ 

3157 for n, junk in enumerate(times[:-1]): 

3158 t = times[n + 1] - times[n] 

3159 if t > 0.1: 

3160 g.trace(f"*** {n} {t:5.4f} sec.") 

3161#@+node:ekr.20031218072017.3133: *3* g.Statistics 

3162#@+node:ekr.20031218072017.3134: *4* g.clearStats 

3163def clearStats() -> None: 

3164 

3165 g.app.statsDict = {} 

3166#@+node:ekr.20031218072017.3135: *4* g.printStats 

3167@command('show-stats') 

3168def printStats(event: Any=None, name: str=None) -> None: 

3169 """ 

3170 Print all gathered statistics. 

3171 

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

3173 

3174 if not g.app.statsLockout: 

3175 g.app.statsLockout = True 

3176 try: 

3177 d = g.app.statsDict 

3178 key = 'g.isUnicode:' + g.callers() 

3179 d [key] = d.get(key, 0) + 1 

3180 finally: 

3181 g.app.statsLockout = False 

3182 """ 

3183 if name: 

3184 if not isinstance(name, str): 

3185 name = repr(name) 

3186 else: 

3187 # Get caller name 2 levels back. 

3188 name = g._callerName(n=2) 

3189 # Print the stats, organized by number of calls. 

3190 d = g.app.statsDict 

3191 print('g.app.statsDict...') 

3192 for key in reversed(sorted(d)): 

3193 print(f"{key:7} {d.get(key)}") 

3194#@+node:ekr.20031218072017.3136: *4* g.stat 

3195def stat(name: str=None) -> None: 

3196 """Increments the statistic for name in g.app.statsDict 

3197 The caller's name is used by default. 

3198 """ 

3199 d = g.app.statsDict 

3200 if name: 

3201 if not isinstance(name, str): 

3202 name = repr(name) 

3203 else: 

3204 name = g._callerName(n=2) # Get caller name 2 levels back. 

3205 d[name] = 1 + d.get(name, 0) 

3206#@+node:ekr.20031218072017.3137: *3* g.Timing 

3207def getTime() -> float: 

3208 return time.time() 

3209 

3210def esDiffTime(message: str, start: float) -> float: 

3211 delta = time.time() - start 

3212 g.es('', f"{message} {delta:5.2f} sec.") 

3213 return time.time() 

3214 

3215def printDiffTime(message: str, start: float) -> float: 

3216 delta = time.time() - start 

3217 g.pr(f"{message} {delta:5.2f} sec.") 

3218 return time.time() 

3219 

3220def timeSince(start: float) -> str: 

3221 return f"{time.time()-start:5.2f} sec." 

3222#@+node:ekr.20031218072017.1380: ** g.Directives 

3223# Weird pylint bug, activated by TestLeoGlobals class. 

3224# Disabling this will be safe, because pyflakes will still warn about true redefinitions 

3225# pylint: disable=function-redefined 

3226#@+node:EKR.20040504150046.4: *3* g.comment_delims_from_extension 

3227def comment_delims_from_extension(filename: str) -> Tuple[str, str, str]: 

3228 """ 

3229 Return the comment delims corresponding to the filename's extension. 

3230 """ 

3231 if filename.startswith('.'): 

3232 root, ext = None, filename 

3233 else: 

3234 root, ext = os.path.splitext(filename) 

3235 if ext == '.tmp': 

3236 root, ext = os.path.splitext(root) 

3237 language = g.app.extension_dict.get(ext[1:]) 

3238 if ext: 

3239 return g.set_delims_from_language(language) 

3240 g.trace( 

3241 f"unknown extension: {ext!r}, " 

3242 f"filename: {filename!r}, " 

3243 f"root: {root!r}") 

3244 return '', '', '' 

3245#@+node:ekr.20170201150505.1: *3* g.findAllValidLanguageDirectives 

3246def findAllValidLanguageDirectives(s: str) -> List: 

3247 """Return list of all valid @language directives in p.b""" 

3248 if not s.strip(): 

3249 return [] 

3250 languages = set() 

3251 for m in g.g_language_pat.finditer(s): 

3252 language = m.group(1) 

3253 if g.isValidLanguage(language): 

3254 languages.add(language) 

3255 return list(sorted(languages)) 

3256#@+node:ekr.20090214075058.8: *3* g.findAtTabWidthDirectives (must be fast) 

3257def findTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[str]: 

3258 """Return the language in effect at position p.""" 

3259 if c is None: 

3260 return None # c may be None for testing. 

3261 w = None 

3262 # 2009/10/02: no need for copy arg to iter 

3263 for p in p.self_and_parents(copy=False): 

3264 if w: 

3265 break 

3266 for s in p.h, p.b: 

3267 if w: 

3268 break 

3269 anIter = g_tabwidth_pat.finditer(s) 

3270 for m in anIter: 

3271 word = m.group(0) 

3272 i = m.start(0) 

3273 j = g.skip_ws(s, i + len(word)) 

3274 junk, w = g.skip_long(s, j) 

3275 if w == 0: 

3276 w = None 

3277 return w 

3278#@+node:ekr.20170127142001.5: *3* g.findFirstAtLanguageDirective 

3279def findFirstValidAtLanguageDirective(s: str) -> Optional[str]: 

3280 """Return the first *valid* @language directive ins.""" 

3281 if not s.strip(): 

3282 return None 

3283 for m in g.g_language_pat.finditer(s): 

3284 language = m.group(1) 

3285 if g.isValidLanguage(language): 

3286 return language 

3287 return None 

3288#@+node:ekr.20090214075058.6: *3* g.findLanguageDirectives (must be fast) 

3289def findLanguageDirectives(c: Cmdr, p: Pos) -> Optional[str]: 

3290 """Return the language in effect at position p.""" 

3291 if c is None or p is None: 

3292 return None # c may be None for testing. 

3293 

3294 v0 = p.v 

3295 

3296 def find_language(p_or_v: Any) -> Optional[str]: 

3297 for s in p_or_v.h, p_or_v.b: 

3298 for m in g_language_pat.finditer(s): 

3299 language = m.group(1) 

3300 if g.isValidLanguage(language): 

3301 return language 

3302 return None 

3303 

3304 # First, search up the tree. 

3305 for p in p.self_and_parents(copy=False): 

3306 language = find_language(p) 

3307 if language: 

3308 return language 

3309 # #1625: Second, expand the search for cloned nodes. 

3310 seen = [] # vnodes that have already been searched. 

3311 parents = v0.parents[:] # vnodes whose ancestors are to be searched. 

3312 while parents: 

3313 parent_v = parents.pop() 

3314 if parent_v in seen: 

3315 continue 

3316 seen.append(parent_v) 

3317 language = find_language(parent_v) 

3318 if language: 

3319 return language 

3320 for grand_parent_v in parent_v.parents: 

3321 if grand_parent_v not in seen: 

3322 parents.append(grand_parent_v) 

3323 # Finally, fall back to the defaults. 

3324 return c.target_language.lower() if c.target_language else 'python' 

3325#@+node:ekr.20031218072017.1385: *3* g.findReference 

3326# Called from the syntax coloring method that colorizes section references. 

3327# Also called from write at.putRefAt. 

3328 

3329def findReference(name: str, root: Pos) -> Optional[Pos]: 

3330 """Return the position containing the section definition for name.""" 

3331 for p in root.subtree(copy=False): 

3332 assert p != root 

3333 if p.matchHeadline(name) and not p.isAtIgnoreNode(): 

3334 return p.copy() 

3335 return None 

3336#@+node:ekr.20090214075058.9: *3* g.get_directives_dict (must be fast) 

3337# The caller passes [root_node] or None as the second arg. 

3338# This allows us to distinguish between None and [None]. 

3339 

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

3341 """ 

3342 Scan p for Leo directives found in globalDirectiveList. 

3343 

3344 Returns a dict containing the stripped remainder of the line 

3345 following the first occurrence of each recognized directive 

3346 """ 

3347 if root: 

3348 root_node = root[0] 

3349 d = {} 

3350 # 

3351 # #1688: legacy: Always compute the pattern. 

3352 # g.directives_pat is updated whenever loading a plugin. 

3353 # 

3354 # The headline has higher precedence because it is more visible. 

3355 for kind, s in (('head', p.h), ('body', p.b)): 

3356 anIter = g.directives_pat.finditer(s) 

3357 for m in anIter: 

3358 word = m.group(1).strip() 

3359 i = m.start(1) 

3360 if word in d: 

3361 continue 

3362 j = i + len(word) 

3363 if j < len(s) and s[j] not in ' \t\n': 

3364 continue 

3365 # Not a valid directive: just ignore it. 

3366 # A unit test tests that @path:any is invalid. 

3367 k = g.skip_line(s, j) 

3368 val = s[j:k].strip() 

3369 d[word] = val 

3370 if root: 

3371 anIter = g_noweb_root.finditer(p.b) 

3372 for m in anIter: 

3373 if root_node: 

3374 d["root"] = 0 # value not immportant 

3375 else: 

3376 g.es(f'{g.angleBrackets("*")} may only occur in a topmost node (i.e., without a parent)') 

3377 break 

3378 return d 

3379#@+node:ekr.20080827175609.1: *3* g.get_directives_dict_list (must be fast) 

3380def get_directives_dict_list(p: Pos) -> List[Dict]: 

3381 """Scans p and all its ancestors for directives. 

3382 

3383 Returns a list of dicts containing pointers to 

3384 the start of each directive""" 

3385 result = [] 

3386 p1 = p.copy() 

3387 for p in p1.self_and_parents(copy=False): 

3388 root = None if p.hasParent() else [p] 

3389 # No copy necessary: g.get_directives_dict does not change p. 

3390 result.append(g.get_directives_dict(p, root=root)) 

3391 return result 

3392#@+node:ekr.20111010082822.15545: *3* g.getLanguageFromAncestorAtFileNode 

3393def getLanguageFromAncestorAtFileNode(p: Pos) -> Optional[str]: 

3394 """ 

3395 Return the language in effect at node p. 

3396  

3397 1. Use an unambiguous @language directive in p itself. 

3398 2. Search p's "extended parents" for an @<file> node. 

3399 3. Search p's "extended parents" for an unambiguous @language directive. 

3400 """ 

3401 v0 = p.v 

3402 seen: Set[VNode] 

3403 

3404 # The same generator as in v.setAllAncestorAtFileNodesDirty. 

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

3406 # Modified by EKR. 

3407 

3408 def v_and_parents(v: "VNode") -> Generator: 

3409 if v in seen: 

3410 return 

3411 seen.add(v) 

3412 yield v 

3413 for parent_v in v.parents: 

3414 if parent_v not in seen: 

3415 yield from v_and_parents(parent_v) 

3416 

3417 def find_language(v: "VNode", phase: int) -> Optional[str]: 

3418 """ 

3419 A helper for all searches. 

3420 Phase one searches only @<file> nodes. 

3421 """ 

3422 if phase == 1 and not v.isAnyAtFileNode(): 

3423 return None 

3424 # #1693: Scan v.b for an *unambiguous* @language directive. 

3425 languages = g.findAllValidLanguageDirectives(v.b) 

3426 if len(languages) == 1: # An unambiguous language 

3427 return languages[0] 

3428 if v.isAnyAtFileNode(): 

3429 # Use the file's extension. 

3430 name = v.anyAtFileNodeName() 

3431 junk, ext = g.os_path_splitext(name) 

3432 ext = ext[1:] # strip the leading period. 

3433 language = g.app.extension_dict.get(ext) 

3434 if g.isValidLanguage(language): 

3435 return language 

3436 return None 

3437 

3438 # First, see if p contains any @language directive. 

3439 language = g.findFirstValidAtLanguageDirective(p.b) 

3440 if language: 

3441 return language 

3442 # 

3443 # Phase 1: search only @<file> nodes: #2308. 

3444 # Phase 2: search all nodes. 

3445 for phase in (1, 2): 

3446 # Search direct parents. 

3447 for p2 in p.self_and_parents(copy=False): 

3448 language = find_language(p2.v, phase) 

3449 if language: 

3450 return language 

3451 # Search all extended parents. 

3452 seen = set([v0.context.hiddenRootNode]) 

3453 for v in v_and_parents(v0): 

3454 language = find_language(v, phase) 

3455 if language: 

3456 return language 

3457 return None 

3458#@+node:ekr.20150325075144.1: *3* g.getLanguageFromPosition 

3459def getLanguageAtPosition(c: Cmdr, p: Pos) -> str: 

3460 """ 

3461 Return the language in effect at position p. 

3462 This is always a lowercase language name, never None. 

3463 """ 

3464 aList = g.get_directives_dict_list(p) 

3465 d = g.scanAtCommentAndAtLanguageDirectives(aList) 

3466 language = ( 

3467 d and d.get('language') or 

3468 g.getLanguageFromAncestorAtFileNode(p) or 

3469 c.config.getString('target-language') or 

3470 'python' 

3471 ) 

3472 return language.lower() 

3473#@+node:ekr.20031218072017.1386: *3* g.getOutputNewline 

3474def getOutputNewline(c: Cmdr=None, name: str=None) -> str: 

3475 """Convert the name of a line ending to the line ending itself. 

3476 

3477 Priority: 

3478 - Use name if name given 

3479 - Use c.config.output_newline if c given, 

3480 - Otherwise use g.app.config.output_newline. 

3481 """ 

3482 if name: 

3483 s = name 

3484 elif c: 

3485 s = c.config.output_newline 

3486 else: 

3487 s = app.config.output_newline 

3488 if not s: 

3489 s = '' 

3490 s = s.lower() 

3491 if s in ("nl", "lf"): 

3492 s = '\n' 

3493 elif s == "cr": 

3494 s = '\r' 

3495 elif s == "platform": 

3496 s = os.linesep # 12/2/03: emakital 

3497 elif s == "crlf": 

3498 s = "\r\n" 

3499 else: 

3500 s = '\n' # Default for erroneous values. 

3501 assert isinstance(s, str), repr(s) 

3502 return s 

3503#@+node:ekr.20200521075143.1: *3* g.inAtNosearch 

3504def inAtNosearch(p: Pos) -> bool: 

3505 """Return True if p or p's ancestors contain an @nosearch directive.""" 

3506 if not p: 

3507 return False # #2288. 

3508 for p in p.self_and_parents(): 

3509 if p.is_at_ignore() or re.search(r'(^@|\n@)nosearch\b', p.b): 

3510 return True 

3511 return False 

3512#@+node:ekr.20131230090121.16528: *3* g.isDirective 

3513def isDirective(s: str) -> bool: 

3514 """Return True if s starts with a directive.""" 

3515 m = g_is_directive_pattern.match(s) 

3516 if m: 

3517 s2 = s[m.end(1) :] 

3518 if s2 and s2[0] in ".(": 

3519 return False 

3520 return bool(m.group(1) in g.globalDirectiveList) 

3521 return False 

3522#@+node:ekr.20200810074755.1: *3* g.isValidLanguage 

3523def isValidLanguage(language: str) -> bool: 

3524 """True if language exists in leo/modes.""" 

3525 # 2020/08/12: A hack for c++ 

3526 if language in ('c++', 'cpp'): 

3527 language = 'cplusplus' 

3528 fn = g.os_path_join(g.app.loadDir, '..', 'modes', f"{language}.py") 

3529 return g.os_path_exists(fn) 

3530#@+node:ekr.20080827175609.52: *3* g.scanAtCommentAndLanguageDirectives 

3531def scanAtCommentAndAtLanguageDirectives(aList: List) -> Optional[Dict[str, str]]: 

3532 """ 

3533 Scan aList for @comment and @language directives. 

3534 

3535 @comment should follow @language if both appear in the same node. 

3536 """ 

3537 lang = None 

3538 for d in aList: 

3539 comment = d.get('comment') 

3540 language = d.get('language') 

3541 # Important: assume @comment follows @language. 

3542 if language: 

3543 lang, delim1, delim2, delim3 = g.set_language(language, 0) 

3544 if comment: 

3545 delim1, delim2, delim3 = g.set_delims_from_string(comment) 

3546 if comment or language: 

3547 delims = delim1, delim2, delim3 

3548 d = {'language': lang, 'comment': comment, 'delims': delims} 

3549 return d 

3550 return None 

3551#@+node:ekr.20080827175609.32: *3* g.scanAtEncodingDirectives 

3552def scanAtEncodingDirectives(aList: List) -> Optional[str]: 

3553 """Scan aList for @encoding directives.""" 

3554 for d in aList: 

3555 encoding = d.get('encoding') 

3556 if encoding and g.isValidEncoding(encoding): 

3557 return encoding 

3558 if encoding and not g.unitTesting: 

3559 g.error("invalid @encoding:", encoding) 

3560 return None 

3561#@+node:ekr.20080827175609.53: *3* g.scanAtHeaderDirectives 

3562def scanAtHeaderDirectives(aList: List) -> None: 

3563 """scan aList for @header and @noheader directives.""" 

3564 for d in aList: 

3565 if d.get('header') and d.get('noheader'): 

3566 g.error("conflicting @header and @noheader directives") 

3567#@+node:ekr.20080827175609.33: *3* g.scanAtLineendingDirectives 

3568def scanAtLineendingDirectives(aList: List) -> Optional[str]: 

3569 """Scan aList for @lineending directives.""" 

3570 for d in aList: 

3571 e = d.get('lineending') 

3572 if e in ("cr", "crlf", "lf", "nl", "platform"): 

3573 lineending = g.getOutputNewline(name=e) 

3574 return lineending 

3575 # else: 

3576 # g.error("invalid @lineending directive:",e) 

3577 return None 

3578#@+node:ekr.20080827175609.34: *3* g.scanAtPagewidthDirectives 

3579def scanAtPagewidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[str]: 

3580 """Scan aList for @pagewidth directives.""" 

3581 for d in aList: 

3582 s = d.get('pagewidth') 

3583 if s is not None: 

3584 i, val = g.skip_long(s, 0) 

3585 if val is not None and val > 0: 

3586 return val 

3587 if issue_error_flag and not g.unitTesting: 

3588 g.error("ignoring @pagewidth", s) 

3589 return None 

3590#@+node:ekr.20101022172109.6108: *3* g.scanAtPathDirectives 

3591def scanAtPathDirectives(c: Cmdr, aList: List) -> str: 

3592 path = c.scanAtPathDirectives(aList) 

3593 return path 

3594 

3595def scanAllAtPathDirectives(c: Cmdr, p: Pos) -> str: 

3596 aList = g.get_directives_dict_list(p) 

3597 path = c.scanAtPathDirectives(aList) 

3598 return path 

3599#@+node:ekr.20080827175609.37: *3* g.scanAtTabwidthDirectives 

3600def scanAtTabwidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[int]: 

3601 """Scan aList for @tabwidth directives.""" 

3602 for d in aList: 

3603 s = d.get('tabwidth') 

3604 if s is not None: 

3605 junk, val = g.skip_long(s, 0) 

3606 if val not in (None, 0): 

3607 return val 

3608 if issue_error_flag and not g.unitTesting: 

3609 g.error("ignoring @tabwidth", s) 

3610 return None 

3611 

3612def scanAllAtTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[int]: 

3613 """Scan p and all ancestors looking for @tabwidth directives.""" 

3614 if c and p: 

3615 aList = g.get_directives_dict_list(p) 

3616 val = g.scanAtTabwidthDirectives(aList) 

3617 ret = c.tab_width if val is None else val 

3618 else: 

3619 ret = None 

3620 return ret 

3621#@+node:ekr.20080831084419.4: *3* g.scanAtWrapDirectives 

3622def scanAtWrapDirectives(aList: List, issue_error_flag: bool=False) -> Optional[bool]: 

3623 """Scan aList for @wrap and @nowrap directives.""" 

3624 for d in aList: 

3625 if d.get('wrap') is not None: 

3626 return True 

3627 if d.get('nowrap') is not None: 

3628 return False 

3629 return None 

3630 

3631def scanAllAtWrapDirectives(c: Cmdr, p: Pos) -> Optional[bool]: 

3632 """Scan p and all ancestors looking for @wrap/@nowrap directives.""" 

3633 if c and p: 

3634 default = bool(c and c.config.getBool("body-pane-wraps")) 

3635 aList = g.get_directives_dict_list(p) 

3636 val = g.scanAtWrapDirectives(aList) 

3637 ret = default if val is None else val 

3638 else: 

3639 ret = None 

3640 return ret 

3641#@+node:ekr.20040715155607: *3* g.scanForAtIgnore 

3642def scanForAtIgnore(c: Cmdr, p: Pos) -> bool: 

3643 """Scan position p and its ancestors looking for @ignore directives.""" 

3644 if g.unitTesting: 

3645 return False # For unit tests. 

3646 for p in p.self_and_parents(copy=False): 

3647 d = g.get_directives_dict(p) 

3648 if 'ignore' in d: 

3649 return True 

3650 return False 

3651#@+node:ekr.20040712084911.1: *3* g.scanForAtLanguage 

3652def scanForAtLanguage(c: Cmdr, p: Pos) -> str: 

3653 """Scan position p and p's ancestors looking only for @language and @ignore directives. 

3654 

3655 Returns the language found, or c.target_language.""" 

3656 # Unlike the code in x.scanAllDirectives, this code ignores @comment directives. 

3657 if c and p: 

3658 for p in p.self_and_parents(copy=False): 

3659 d = g.get_directives_dict(p) 

3660 if 'language' in d: 

3661 z = d["language"] 

3662 language, delim1, delim2, delim3 = g.set_language(z, 0) 

3663 return language 

3664 return c.target_language 

3665#@+node:ekr.20041123094807: *3* g.scanForAtSettings 

3666def scanForAtSettings(p: Pos) -> bool: 

3667 """Scan position p and its ancestors looking for @settings nodes.""" 

3668 for p in p.self_and_parents(copy=False): 

3669 h = p.h 

3670 h = g.app.config.canonicalizeSettingName(h) 

3671 if h.startswith("@settings"): 

3672 return True 

3673 return False 

3674#@+node:ekr.20031218072017.1382: *3* g.set_delims_from_language 

3675def set_delims_from_language(language: str) -> Tuple[str, str, str]: 

3676 """Return a tuple (single,start,end) of comment delims.""" 

3677 val = g.app.language_delims_dict.get(language) 

3678 if val: 

3679 delim1, delim2, delim3 = g.set_delims_from_string(val) 

3680 if delim2 and not delim3: 

3681 return '', delim1, delim2 

3682 # 0,1 or 3 params. 

3683 return delim1, delim2, delim3 

3684 return '', '', '' 

3685 # Indicate that no change should be made 

3686#@+node:ekr.20031218072017.1383: *3* g.set_delims_from_string 

3687def set_delims_from_string(s: str) -> Tuple[str, str, str]: 

3688 """ 

3689 Return (delim1, delim2, delim2), the delims following the @comment 

3690 directive. 

3691 

3692 This code can be called from @language logic, in which case s can 

3693 point at @comment 

3694 """ 

3695 # Skip an optional @comment 

3696 tag = "@comment" 

3697 i = 0 

3698 if g.match_word(s, i, tag): 

3699 i += len(tag) 

3700 count = 0 

3701 delims = ['', '', ''] 

3702 while count < 3 and i < len(s): 

3703 i = j = g.skip_ws(s, i) 

3704 while i < len(s) and not g.is_ws(s[i]) and not g.is_nl(s, i): 

3705 i += 1 

3706 if j == i: 

3707 break 

3708 delims[count] = s[j:i] or '' 

3709 count += 1 

3710 # 'rr 09/25/02 

3711 if count == 2: # delims[0] is always the single-line delim. 

3712 delims[2] = delims[1] 

3713 delims[1] = delims[0] 

3714 delims[0] = '' 

3715 for i in range(0, 3): 

3716 if delims[i]: 

3717 if delims[i].startswith("@0x"): 

3718 # Allow delimiter definition as @0x + hexadecimal encoded delimiter 

3719 # to avoid problems with duplicate delimiters on the @comment line. 

3720 # If used, whole delimiter must be encoded. 

3721 if len(delims[i]) == 3: 

3722 g.warning(f"'{delims[i]}' delimiter is invalid") 

3723 return None, None, None 

3724 try: 

3725 delims[i] = binascii.unhexlify(delims[i][3:]) # type:ignore 

3726 delims[i] = g.toUnicode(delims[i]) 

3727 except Exception as e: 

3728 g.warning(f"'{delims[i]}' delimiter is invalid: {e}") 

3729 return None, None, None 

3730 else: 

3731 # 7/8/02: The "REM hack": replace underscores by blanks. 

3732 # 9/25/02: The "perlpod hack": replace double underscores by newlines. 

3733 delims[i] = delims[i].replace("__", '\n').replace('_', ' ') 

3734 return delims[0], delims[1], delims[2] 

3735#@+node:ekr.20031218072017.1384: *3* g.set_language 

3736def set_language(s: str, i: int, issue_errors_flag: bool=False) -> Tuple: 

3737 """Scan the @language directive that appears at s[i:]. 

3738 

3739 The @language may have been stripped away. 

3740 

3741 Returns (language, delim1, delim2, delim3) 

3742 """ 

3743 tag = "@language" 

3744 assert i is not None 

3745 if g.match_word(s, i, tag): 

3746 i += len(tag) 

3747 # Get the argument. 

3748 i = g.skip_ws(s, i) 

3749 j = i 

3750 i = g.skip_c_id(s, i) 

3751 # Allow tcl/tk. 

3752 arg = s[j:i].lower() 

3753 if app.language_delims_dict.get(arg): 

3754 language = arg 

3755 delim1, delim2, delim3 = g.set_delims_from_language(language) 

3756 return language, delim1, delim2, delim3 

3757 if issue_errors_flag: 

3758 g.es("ignoring:", g.get_line(s, i)) 

3759 return None, None, None, None 

3760#@+node:ekr.20071109165315: *3* g.stripPathCruft 

3761def stripPathCruft(path: str) -> str: 

3762 """Strip cruft from a path name.""" 

3763 if not path: 

3764 return path # Retain empty paths for warnings. 

3765 if len(path) > 2 and ( 

3766 (path[0] == '<' and path[-1] == '>') or 

3767 (path[0] == '"' and path[-1] == '"') or 

3768 (path[0] == "'" and path[-1] == "'") 

3769 ): 

3770 path = path[1:-1].strip() 

3771 # We want a *relative* path, not an absolute path. 

3772 return path 

3773#@+node:ekr.20090214075058.10: *3* g.update_directives_pat 

3774def update_directives_pat() -> None: 

3775 """Init/update g.directives_pat""" 

3776 global globalDirectiveList, directives_pat 

3777 # Use a pattern that guarantees word matches. 

3778 aList = [ 

3779 fr"\b{z}\b" for z in globalDirectiveList if z != 'others' 

3780 ] 

3781 pat = "^@(%s)" % "|".join(aList) 

3782 directives_pat = re.compile(pat, re.MULTILINE) 

3783 

3784# #1688: Initialize g.directives_pat 

3785update_directives_pat() 

3786#@+node:ekr.20031218072017.3116: ** g.Files & Directories 

3787#@+node:ekr.20080606074139.2: *3* g.chdir 

3788def chdir(path: str) -> None: 

3789 if not g.os_path_isdir(path): 

3790 path = g.os_path_dirname(path) 

3791 if g.os_path_isdir(path) and g.os_path_exists(path): 

3792 os.chdir(path) 

3793#@+node:ekr.20120222084734.10287: *3* g.compute...Dir 

3794# For compatibility with old code. 

3795 

3796def computeGlobalConfigDir() -> str: 

3797 return g.app.loadManager.computeGlobalConfigDir() 

3798 

3799def computeHomeDir() -> str: 

3800 return g.app.loadManager.computeHomeDir() 

3801 

3802def computeLeoDir() -> str: 

3803 return g.app.loadManager.computeLeoDir() 

3804 

3805def computeLoadDir() -> str: 

3806 return g.app.loadManager.computeLoadDir() 

3807 

3808def computeMachineName() -> str: 

3809 return g.app.loadManager.computeMachineName() 

3810 

3811def computeStandardDirectories() -> str: 

3812 return g.app.loadManager.computeStandardDirectories() 

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

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

3815 

3816 branch, commit = g.gitInfoForFile(fileName) # #1616 

3817 if not fileName: 

3818 return branch + ": untitled" if branch else 'untitled' 

3819 path, fn = g.os_path_split(fileName) 

3820 if path: 

3821 title = fn + " in " + path 

3822 else: 

3823 title = fn 

3824 # Yet another fix for bug 1194209: regularize slashes. 

3825 if os.sep in '/\\': 

3826 title = title.replace('/', os.sep).replace('\\', os.sep) 

3827 if branch: 

3828 title = branch + ": " + title 

3829 return title 

3830#@+node:ekr.20031218072017.3117: *3* g.create_temp_file 

3831def create_temp_file(textMode: bool=False) -> Tuple[Any, str]: 

3832 """ 

3833 Return a tuple (theFile,theFileName) 

3834 

3835 theFile: a file object open for writing. 

3836 theFileName: the name of the temporary file. 

3837 """ 

3838 try: 

3839 # fd is an handle to an open file as would be returned by os.open() 

3840 fd, theFileName = tempfile.mkstemp(text=textMode) 

3841 mode = 'w' if textMode else 'wb' 

3842 theFile = os.fdopen(fd, mode) 

3843 except Exception: 

3844 g.error('unexpected exception in g.create_temp_file') 

3845 g.es_exception() 

3846 theFile, theFileName = None, '' 

3847 return theFile, theFileName 

3848#@+node:ekr.20210307060731.1: *3* g.createHiddenCommander 

3849def createHiddenCommander(fn: str) -> Optional[Cmdr]: 

3850 """Read the file into a hidden commander (Similar to g.openWithFileName).""" 

3851 from leo.core.leoCommands import Commands 

3852 c = Commands(fn, gui=g.app.nullGui) 

3853 theFile = g.app.loadManager.openAnyLeoFile(fn) 

3854 if theFile: 

3855 c.fileCommands.openLeoFile( # type:ignore 

3856 theFile, fn, readAtFileNodesFlag=True, silent=True) 

3857 return c 

3858 return None 

3859#@+node:vitalije.20170714085545.1: *3* g.defaultLeoFileExtension 

3860def defaultLeoFileExtension(c: Cmdr=None) -> str: 

3861 conf = c.config if c else g.app.config 

3862 return conf.getString('default-leo-extension') or '.leo' 

3863#@+node:ekr.20031218072017.3118: *3* g.ensure_extension 

3864def ensure_extension(name: str, ext: str) -> str: 

3865 

3866 theFile, old_ext = g.os_path_splitext(name) 

3867 if not name: 

3868 return name # don't add to an empty name. 

3869 if old_ext in ('.db', '.leo'): 

3870 return name 

3871 if old_ext and old_ext == ext: 

3872 return name 

3873 return name + ext 

3874#@+node:ekr.20150403150655.1: *3* g.fullPath 

3875def fullPath(c: Cmdr, p: Pos, simulate: bool=False) -> str: 

3876 """ 

3877 Return the full path (including fileName) in effect at p. Neither the 

3878 path nor the fileName will be created if it does not exist. 

3879 """ 

3880 # Search p and p's parents. 

3881 for p in p.self_and_parents(copy=False): 

3882 aList = g.get_directives_dict_list(p) 

3883 path = c.scanAtPathDirectives(aList) 

3884 fn = p.h if simulate else p.anyAtFileNodeName() 

3885 # Use p.h for unit tests. 

3886 if fn: 

3887 # Fix #102: expand path expressions. 

3888 fn = c.expand_path_expression(fn) # #1341. 

3889 fn = os.path.expanduser(fn) # 1900. 

3890 return g.os_path_finalize_join(path, fn) # #1341. 

3891 return '' 

3892#@+node:ekr.20190327192721.1: *3* g.get_files_in_directory 

3893def get_files_in_directory(directory: str, kinds: List=None, recursive: bool=True) -> List[str]: 

3894 """ 

3895 Return a list of all files of the given file extensions in the directory. 

3896 Default kinds: ['*.py']. 

3897 """ 

3898 files: List[str] = [] 

3899 sep = os.path.sep 

3900 if not g.os.path.exists(directory): 

3901 g.es_print('does not exist', directory) 

3902 return files 

3903 try: 

3904 if kinds: 

3905 kinds = [z if z.startswith('*') else '*' + z for z in kinds] 

3906 else: 

3907 kinds = ['*.py'] 

3908 if recursive: 

3909 # Works for all versions of Python. 

3910 for root, dirnames, filenames in os.walk(directory): 

3911 for kind in kinds: 

3912 for filename in fnmatch.filter(filenames, kind): 

3913 files.append(os.path.join(root, filename)) 

3914 else: 

3915 for kind in kinds: 

3916 files.extend(glob.glob(directory + sep + kind)) 

3917 return list(set(sorted(files))) 

3918 except Exception: 

3919 g.es_exception() 

3920 return [] 

3921#@+node:ekr.20031218072017.1264: *3* g.getBaseDirectory 

3922# Handles the conventions applying to the "relative_path_base_directory" configuration option. 

3923 

3924def getBaseDirectory(c: Cmdr) -> str: 

3925 """Convert '!' or '.' to proper directory references.""" 

3926 base = app.config.relative_path_base_directory 

3927 if base and base == "!": 

3928 base = app.loadDir 

3929 elif base and base == ".": 

3930 base = c.openDirectory 

3931 if base and g.os_path_isabs(base): 

3932 # Set c.chdir_to_relative_path as needed. 

3933 if not hasattr(c, 'chdir_to_relative_path'): 

3934 c.chdir_to_relative_path = c.config.getBool('chdir-to-relative-path') 

3935 # Call os.chdir if requested. 

3936 if c.chdir_to_relative_path: 

3937 os.chdir(base) 

3938 return base # base need not exist yet. 

3939 return "" # No relative base given. 

3940#@+node:ekr.20170223093758.1: *3* g.getEncodingAt 

3941def getEncodingAt(p: Pos, s: str=None) -> str: 

3942 """ 

3943 Return the encoding in effect at p and/or for string s. 

3944 

3945 Read logic: s is not None. 

3946 Write logic: s is None. 

3947 """ 

3948 # A BOM overrides everything. 

3949 if s: 

3950 e, junk_s = g.stripBOM(s) 

3951 if e: 

3952 return e 

3953 aList = g.get_directives_dict_list(p) 

3954 e = g.scanAtEncodingDirectives(aList) 

3955 if s and s.strip() and not e: 

3956 e = 'utf-8' 

3957 return e 

3958#@+node:ville.20090701144325.14942: *3* g.guessExternalEditor 

3959def guessExternalEditor(c: Cmdr=None) -> Optional[str]: 

3960 """ Return a 'sensible' external editor """ 

3961 editor = ( 

3962 os.environ.get("LEO_EDITOR") or 

3963 os.environ.get("EDITOR") or 

3964 g.app.db and g.app.db.get("LEO_EDITOR") or 

3965 c and c.config.getString('external-editor')) 

3966 if editor: 

3967 return editor 

3968 # fallbacks 

3969 platform = sys.platform.lower() 

3970 if platform.startswith('win'): 

3971 return "notepad" 

3972 if platform.startswith('linux'): 

3973 return 'gedit' 

3974 g.es( 

3975 '''No editor set. 

3976Please set LEO_EDITOR or EDITOR environment variable, 

3977or do g.app.db['LEO_EDITOR'] = "gvim"''', 

3978 ) 

3979 return None 

3980#@+node:ekr.20160330204014.1: *3* g.init_dialog_folder 

3981def init_dialog_folder(c: Cmdr, p: Pos, use_at_path: bool=True) -> str: 

3982 """Return the most convenient folder to open or save a file.""" 

3983 if c and p and use_at_path: 

3984 path = g.fullPath(c, p) 

3985 if path: 

3986 dir_ = g.os_path_dirname(path) 

3987 if dir_ and g.os_path_exists(dir_): 

3988 return dir_ 

3989 table = ( 

3990 ('c.last_dir', c and c.last_dir), 

3991 ('os.curdir', g.os_path_abspath(os.curdir)), 

3992 ) 

3993 for kind, dir_ in table: 

3994 if dir_ and g.os_path_exists(dir_): 

3995 return dir_ 

3996 return '' 

3997#@+node:ekr.20100329071036.5744: *3* g.is_binary_file/external_file/string 

3998def is_binary_file(f: Any) -> bool: 

3999 return f and isinstance(f, io.BufferedIOBase) 

4000 

4001def is_binary_external_file(fileName: str) -> bool: 

4002 try: 

4003 with open(fileName, 'rb') as f: 

4004 s = f.read(1024) # bytes, in Python 3. 

4005 return g.is_binary_string(s) 

4006 except IOError: 

4007 return False 

4008 except Exception: 

4009 g.es_exception() 

4010 return False 

4011 

4012def is_binary_string(s: str) -> bool: 

4013 # http://stackoverflow.com/questions/898669 

4014 # aList is a list of all non-binary characters. 

4015 aList = [7, 8, 9, 10, 12, 13, 27] + list(range(0x20, 0x100)) 

4016 return bool(s.translate(None, bytes(aList))) # type:ignore 

4017#@+node:EKR.20040504154039: *3* g.is_sentinel 

4018def is_sentinel(line: str, delims: Sequence) -> bool: 

4019 """Return True if line starts with a sentinel comment.""" 

4020 delim1, delim2, delim3 = delims 

4021 line = line.lstrip() 

4022 if delim1: 

4023 return line.startswith(delim1 + '@') 

4024 if delim2 and delim3: 

4025 i = line.find(delim2 + '@') 

4026 j = line.find(delim3) 

4027 return 0 == i < j 

4028 g.error(f"is_sentinel: can not happen. delims: {repr(delims)}") 

4029 return False 

4030#@+node:ekr.20031218072017.3119: *3* g.makeAllNonExistentDirectories 

4031def makeAllNonExistentDirectories(theDir: str) -> Optional[str]: 

4032 """ 

4033 A wrapper from os.makedirs. 

4034 Attempt to make all non-existent directories. 

4035 

4036 Return True if the directory exists or was created successfully. 

4037 """ 

4038 # Return True if the directory already exists. 

4039 theDir = g.os_path_normpath(theDir) 

4040 ok = g.os_path_isdir(theDir) and g.os_path_exists(theDir) 

4041 if ok: 

4042 return theDir 

4043 # #1450: Create the directory with os.makedirs. 

4044 try: 

4045 os.makedirs(theDir, mode=0o777, exist_ok=False) 

4046 return theDir 

4047 except Exception: 

4048 return None 

4049#@+node:ekr.20071114113736: *3* g.makePathRelativeTo 

4050def makePathRelativeTo(fullPath: str, basePath: str) -> str: 

4051 if fullPath.startswith(basePath): 

4052 s = fullPath[len(basePath) :] 

4053 if s.startswith(os.path.sep): 

4054 s = s[len(os.path.sep) :] 

4055 return s 

4056 return fullPath 

4057#@+node:ekr.20090520055433.5945: *3* g.openWithFileName 

4058def openWithFileName(fileName: str, old_c: Cmdr=None, gui: str=None) -> Cmdr: 

4059 """ 

4060 Create a Leo Frame for the indicated fileName if the file exists. 

4061 

4062 Return the commander of the newly-opened outline. 

4063 """ 

4064 return g.app.loadManager.loadLocalFile(fileName, gui, old_c) 

4065#@+node:ekr.20150306035851.7: *3* g.readFileIntoEncodedString 

4066def readFileIntoEncodedString(fn: str, silent: bool=False) -> Optional[bytes]: 

4067 """Return the raw contents of the file whose full path is fn.""" 

4068 try: 

4069 with open(fn, 'rb') as f: 

4070 return f.read() 

4071 except IOError: 

4072 if not silent: 

4073 g.error('can not open', fn) 

4074 except Exception: 

4075 if not silent: 

4076 g.error(f"readFileIntoEncodedString: exception reading {fn}") 

4077 g.es_exception() 

4078 return None 

4079#@+node:ekr.20100125073206.8710: *3* g.readFileIntoString 

4080def readFileIntoString( 

4081 fileName: str, 

4082 encoding: str='utf-8', # BOM may override this. 

4083 kind: str=None, # @file, @edit, ... 

4084 verbose: bool=True, 

4085) -> Tuple[Any, Any]: 

4086 """ 

4087 Return the contents of the file whose full path is fileName. 

4088 

4089 Return (s,e) 

4090 s is the string, converted to unicode, or None if there was an error. 

4091 e is the encoding of s, computed in the following order: 

4092 - The BOM encoding if the file starts with a BOM mark. 

4093 - The encoding given in the # -*- coding: utf-8 -*- line for python files. 

4094 - The encoding given by the 'encoding' keyword arg. 

4095 - None, which typically means 'utf-8'. 

4096 """ 

4097 if not fileName: 

4098 if verbose: 

4099 g.trace('no fileName arg given') 

4100 return None, None 

4101 if g.os_path_isdir(fileName): 

4102 if verbose: 

4103 g.trace('not a file:', fileName) 

4104 return None, None 

4105 if not g.os_path_exists(fileName): 

4106 if verbose: 

4107 g.error('file not found:', fileName) 

4108 return None, None 

4109 try: 

4110 e = None 

4111 with open(fileName, 'rb') as f: 

4112 s = f.read() 

4113 # Fix #391. 

4114 if not s: 

4115 return '', None 

4116 # New in Leo 4.11: check for unicode BOM first. 

4117 e, s = g.stripBOM(s) 

4118 if not e: 

4119 # Python's encoding comments override everything else. 

4120 junk, ext = g.os_path_splitext(fileName) 

4121 if ext == '.py': 

4122 e = g.getPythonEncodingFromString(s) 

4123 s = g.toUnicode(s, encoding=e or encoding) 

4124 return s, e 

4125 except IOError: 

4126 # Translate 'can not open' and kind, but not fileName. 

4127 if verbose: 

4128 g.error('can not open', '', (kind or ''), fileName) 

4129 except Exception: 

4130 g.error(f"readFileIntoString: unexpected exception reading {fileName}") 

4131 g.es_exception() 

4132 return None, None 

4133#@+node:ekr.20160504062833.1: *3* g.readFileToUnicodeString 

4134def readFileIntoUnicodeString(fn: str, encoding: Optional[str]=None, silent: bool=False) -> Optional[str]: 

4135 """Return the raw contents of the file whose full path is fn.""" 

4136 try: 

4137 with open(fn, 'rb') as f: 

4138 s = f.read() 

4139 return g.toUnicode(s, encoding=encoding) 

4140 except IOError: 

4141 if not silent: 

4142 g.error('can not open', fn) 

4143 except Exception: 

4144 g.error(f"readFileIntoUnicodeString: unexpected exception reading {fn}") 

4145 g.es_exception() 

4146 return None 

4147#@+node:ekr.20031218072017.3120: *3* g.readlineForceUnixNewline 

4148#@+at Stephen P. Schaefer 9/7/2002 

4149# 

4150# The Unix readline() routine delivers "\r\n" line end strings verbatim, 

4151# while the windows versions force the string to use the Unix convention 

4152# of using only "\n". This routine causes the Unix readline to do the 

4153# same. 

4154#@@c 

4155 

4156def readlineForceUnixNewline(f: Any, fileName: Optional[str]=None) -> str: 

4157 try: 

4158 s = f.readline() 

4159 except UnicodeDecodeError: 

4160 g.trace(f"UnicodeDecodeError: {fileName}", f, g.callers()) 

4161 s = '' 

4162 if len(s) >= 2 and s[-2] == "\r" and s[-1] == "\n": 

4163 s = s[0:-2] + "\n" 

4164 return s 

4165#@+node:ekr.20031218072017.3124: *3* g.sanitize_filename 

4166def sanitize_filename(s: str) -> str: 

4167 """ 

4168 Prepares string s to be a valid file name: 

4169 

4170 - substitute '_' for whitespace and special path characters. 

4171 - eliminate all other non-alphabetic characters. 

4172 - convert double quotes to single quotes. 

4173 - strip leading and trailing whitespace. 

4174 - return at most 128 characters. 

4175 """ 

4176 result = [] 

4177 for ch in s: 

4178 if ch in string.ascii_letters: 

4179 result.append(ch) 

4180 elif ch == '\t': 

4181 result.append(' ') 

4182 elif ch == '"': 

4183 result.append("'") 

4184 elif ch in '\\/:|<>*:._': 

4185 result.append('_') 

4186 s = ''.join(result).strip() 

4187 while len(s) > 1: 

4188 n = len(s) 

4189 s = s.replace('__', '_') 

4190 if len(s) == n: 

4191 break 

4192 return s[:128] 

4193#@+node:ekr.20060328150113: *3* g.setGlobalOpenDir 

4194def setGlobalOpenDir(fileName: str) -> None: 

4195 if fileName: 

4196 g.app.globalOpenDir = g.os_path_dirname(fileName) 

4197 # g.es('current directory:',g.app.globalOpenDir) 

4198#@+node:ekr.20031218072017.3125: *3* g.shortFileName & shortFilename 

4199def shortFileName(fileName: str, n: int=None) -> str: 

4200 """Return the base name of a path.""" 

4201 if n is not None: 

4202 g.trace('"n" keyword argument is no longer used') 

4203 return g.os_path_basename(fileName) if fileName else '' 

4204 

4205shortFilename = shortFileName 

4206#@+node:ekr.20150610125813.1: *3* g.splitLongFileName 

4207def splitLongFileName(fn: str, limit: int=40) -> str: 

4208 """Return fn, split into lines at slash characters.""" 

4209 aList = fn.replace('\\', '/').split('/') 

4210 n, result = 0, [] 

4211 for i, s in enumerate(aList): 

4212 n += len(s) 

4213 result.append(s) 

4214 if i + 1 < len(aList): 

4215 result.append('/') 

4216 n += 1 

4217 if n > limit: 

4218 result.append('\n') 

4219 n = 0 

4220 return ''.join(result) 

4221#@+node:ekr.20190114061452.26: *3* g.writeFile 

4222def writeFile(contents: Union[bytes, str], encoding: str, fileName: str) -> bool: 

4223 """Create a file with the given contents.""" 

4224 try: 

4225 if isinstance(contents, str): 

4226 contents = g.toEncodedString(contents, encoding=encoding) 

4227 # 'wb' preserves line endings. 

4228 with open(fileName, 'wb') as f: 

4229 f.write(contents) # type:ignore 

4230 return True 

4231 except Exception as e: 

4232 print(f"exception writing: {fileName}:\n{e}") 

4233 # g.trace(g.callers()) 

4234 # g.es_exception() 

4235 return False 

4236#@+node:ekr.20031218072017.3151: ** g.Finding & Scanning 

4237#@+node:ekr.20140602083643.17659: *3* g.find_word 

4238def find_word(s: str, word: str, i: int=0) -> int: 

4239 """ 

4240 Return the index of the first occurance of word in s, or -1 if not found. 

4241 

4242 g.find_word is *not* the same as s.find(i,word); 

4243 g.find_word ensures that only word-matches are reported. 

4244 """ 

4245 while i < len(s): 

4246 progress = i 

4247 i = s.find(word, i) 

4248 if i == -1: 

4249 return -1 

4250 # Make sure we are at the start of a word. 

4251 if i > 0: 

4252 ch = s[i - 1] 

4253 if ch == '_' or ch.isalnum(): 

4254 i += len(word) 

4255 continue 

4256 if g.match_word(s, i, word): 

4257 return i 

4258 i += len(word) 

4259 assert progress < i 

4260 return -1 

4261#@+node:ekr.20211029090118.1: *3* g.findAncestorVnodeByPredicate 

4262def findAncestorVnodeByPredicate(p: Pos, v_predicate: Any) -> Optional["VNode"]: 

4263 """ 

4264 Return first ancestor vnode matching the predicate. 

4265  

4266 The predicate must must be a function of a single vnode argument. 

4267 """ 

4268 if not p: 

4269 return None 

4270 # First, look up the tree. 

4271 for p2 in p.self_and_parents(): 

4272 if v_predicate(p2.v): 

4273 return p2.v 

4274 # Look at parents of all cloned nodes. 

4275 if not p.isCloned(): 

4276 return None 

4277 seen = [] # vnodes that have already been searched. 

4278 parents = p.v.parents[:] # vnodes to be searched. 

4279 while parents: 

4280 parent_v = parents.pop() 

4281 if parent_v in seen: 

4282 continue 

4283 seen.append(parent_v) 

4284 if v_predicate(parent_v): 

4285 return parent_v 

4286 for grand_parent_v in parent_v.parents: 

4287 if grand_parent_v not in seen: 

4288 parents.append(grand_parent_v) 

4289 return None 

4290#@+node:ekr.20170220103251.1: *3* g.findRootsWithPredicate 

4291def findRootsWithPredicate(c: Cmdr, root: Pos, predicate: Any=None) -> List[Pos]: 

4292 """ 

4293 Commands often want to find one or more **roots**, given a position p. 

4294 A root is the position of any node matching a predicate. 

4295 

4296 This function formalizes the search order used by the black, 

4297 pylint, pyflakes and the rst3 commands, returning a list of zero 

4298 or more found roots. 

4299 """ 

4300 seen = [] 

4301 roots = [] 

4302 if predicate is None: 

4303 

4304 # A useful default predicate for python. 

4305 # pylint: disable=function-redefined 

4306 

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

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

4309 

4310 # 1. Search p's tree. 

4311 for p in root.self_and_subtree(copy=False): 

4312 if predicate(p) and p.v not in seen: 

4313 seen.append(p.v) 

4314 roots.append(p.copy()) 

4315 if roots: 

4316 return roots 

4317 # 2. Look up the tree. 

4318 for p in root.parents(): 

4319 if predicate(p): 

4320 return [p.copy()] 

4321 # 3. Expand the search if root is a clone. 

4322 clones = [] 

4323 for p in root.self_and_parents(copy=False): 

4324 if p.isCloned(): 

4325 clones.append(p.v) 

4326 if clones: 

4327 for p in c.all_positions(copy=False): 

4328 if predicate(p): 

4329 # Match if any node in p's tree matches any clone. 

4330 for p2 in p.self_and_subtree(): 

4331 if p2.v in clones: 

4332 return [p.copy()] 

4333 return [] 

4334#@+node:ekr.20031218072017.3156: *3* g.scanError 

4335# It is dubious to bump the Tangle error count here, but it really doesn't hurt. 

4336 

4337def scanError(s: str) -> None: 

4338 """Bump the error count in the tangle command.""" 

4339 # New in Leo 4.4b1: just set this global. 

4340 g.app.scanErrors += 1 

4341 g.es('', s) 

4342#@+node:ekr.20031218072017.3157: *3* g.scanf 

4343# A quick and dirty sscanf. Understands only %s and %d. 

4344 

4345def scanf(s: str, pat: str) -> List[str]: 

4346 count = pat.count("%s") + pat.count("%d") 

4347 pat = pat.replace("%s", r"(\S+)") 

4348 pat = pat.replace("%d", r"(\d+)") 

4349 parts = re.split(pat, s) 

4350 result: List[str] = [] 

4351 for part in parts: 

4352 if part and len(result) < count: 

4353 result.append(part) 

4354 return result 

4355#@+node:ekr.20031218072017.3158: *3* g.Scanners: calling scanError 

4356#@+at These scanners all call g.scanError() directly or indirectly, so they 

4357# will call g.es if they find an error. g.scanError() also bumps 

4358# c.tangleCommands.errors, which is harmless if we aren't tangling, and 

4359# useful if we are. 

4360# 

4361# These routines are called by the Import routines and the Tangle routines. 

4362#@+node:ekr.20031218072017.3159: *4* g.skip_block_comment 

4363# Scans past a block comment (an old_style C comment). 

4364 

4365def skip_block_comment(s: str, i: int) -> int: 

4366 assert g.match(s, i, "/*") 

4367 j = i 

4368 i += 2 

4369 n = len(s) 

4370 k = s.find("*/", i) 

4371 if k == -1: 

4372 g.scanError("Run on block comment: " + s[j:i]) 

4373 return n 

4374 return k + 2 

4375#@+node:ekr.20031218072017.3160: *4* g.skip_braces 

4376#@+at This code is called only from the import logic, so we are allowed to 

4377# try some tricks. In particular, we assume all braces are matched in 

4378# if blocks. 

4379#@@c 

4380 

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

4382 """ 

4383 Skips from the opening to the matching brace. 

4384 

4385 If no matching is found i is set to len(s) 

4386 """ 

4387 # start = g.get_line(s,i) 

4388 assert g.match(s, i, '{') 

4389 level = 0 

4390 n = len(s) 

4391 while i < n: 

4392 c = s[i] 

4393 if c == '{': 

4394 level += 1 

4395 i += 1 

4396 elif c == '}': 

4397 level -= 1 

4398 if level <= 0: 

4399 return i 

4400 i += 1 

4401 elif c == '\'' or c == '"': 

4402 i = g.skip_string(s, i) 

4403 elif g.match(s, i, '//'): 

4404 i = g.skip_to_end_of_line(s, i) 

4405 elif g.match(s, i, '/*'): 

4406 i = g.skip_block_comment(s, i) 

4407 # 7/29/02: be more careful handling conditional code. 

4408 elif ( 

4409 g.match_word(s, i, "#if") or 

4410 g.match_word(s, i, "#ifdef") or 

4411 g.match_word(s, i, "#ifndef") 

4412 ): 

4413 i, delta = g.skip_pp_if(s, i) 

4414 level += delta 

4415 else: i += 1 

4416 return i 

4417#@+node:ekr.20031218072017.3162: *4* g.skip_parens 

4418def skip_parens(s: str, i: int) -> int: 

4419 """ 

4420 Skips from the opening ( to the matching ). 

4421 

4422 If no matching is found i is set to len(s). 

4423 """ 

4424 level = 0 

4425 n = len(s) 

4426 assert g.match(s, i, '('), repr(s[i]) 

4427 while i < n: 

4428 c = s[i] 

4429 if c == '(': 

4430 level += 1 

4431 i += 1 

4432 elif c == ')': 

4433 level -= 1 

4434 if level <= 0: 

4435 return i 

4436 i += 1 

4437 elif c == '\'' or c == '"': 

4438 i = g.skip_string(s, i) 

4439 elif g.match(s, i, "//"): 

4440 i = g.skip_to_end_of_line(s, i) 

4441 elif g.match(s, i, "/*"): 

4442 i = g.skip_block_comment(s, i) 

4443 else: 

4444 i += 1 

4445 return i 

4446#@+node:ekr.20031218072017.3163: *4* g.skip_pascal_begin_end 

4447def skip_pascal_begin_end(s: str, i: int) -> int: 

4448 """ 

4449 Skips from begin to matching end. 

4450 If found, i points to the end. Otherwise, i >= len(s) 

4451 The end keyword matches begin, case, class, record, and try. 

4452 """ 

4453 assert g.match_c_word(s, i, "begin") 

4454 level = 1 

4455 i = g.skip_c_id(s, i) # Skip the opening begin. 

4456 while i < len(s): 

4457 ch = s[i] 

4458 if ch == '{': 

4459 i = g.skip_pascal_braces(s, i) 

4460 elif ch == '"' or ch == '\'': 

4461 i = g.skip_pascal_string(s, i) 

4462 elif g.match(s, i, "//"): 

4463 i = g.skip_line(s, i) 

4464 elif g.match(s, i, "(*"): 

4465 i = g.skip_pascal_block_comment(s, i) 

4466 elif g.match_c_word(s, i, "end"): 

4467 level -= 1 

4468 if level == 0: 

4469 return i 

4470 i = g.skip_c_id(s, i) 

4471 elif g.is_c_id(ch): 

4472 j = i 

4473 i = g.skip_c_id(s, i) 

4474 name = s[j:i] 

4475 if name in ["begin", "case", "class", "record", "try"]: 

4476 level += 1 

4477 else: 

4478 i += 1 

4479 return i 

4480#@+node:ekr.20031218072017.3164: *4* g.skip_pascal_block_comment 

4481def skip_pascal_block_comment(s: str, i: int) -> int: 

4482 """Scan past a pascal comment delimited by (* and *).""" 

4483 j = i 

4484 assert g.match(s, i, "(*") 

4485 i = s.find("*)", i) 

4486 if i > -1: 

4487 return i + 2 

4488 g.scanError("Run on comment" + s[j:i]) 

4489 return len(s) 

4490#@+node:ekr.20031218072017.3165: *4* g.skip_pascal_string 

4491def skip_pascal_string(s: str, i: int) -> int: 

4492 j = i 

4493 delim = s[i] 

4494 i += 1 

4495 assert delim == '"' or delim == '\'' 

4496 while i < len(s): 

4497 if s[i] == delim: 

4498 return i + 1 

4499 i += 1 

4500 g.scanError("Run on string: " + s[j:i]) 

4501 return i 

4502#@+node:ekr.20031218072017.3166: *4* g.skip_heredoc_string 

4503def skip_heredoc_string(s: str, i: int) -> int: 

4504 """ 

4505 08-SEP-2002 DTHEIN. 

4506 A heredoc string in PHP looks like: 

4507 

4508 <<<EOS 

4509 This is my string. 

4510 It is mine. I own it. 

4511 No one else has it. 

4512 EOS 

4513 

4514 It begins with <<< plus a token (naming same as PHP variable names). 

4515 It ends with the token on a line by itself (must start in first position. 

4516 """ 

4517 j = i 

4518 assert g.match(s, i, "<<<") 

4519 m = re.match(r"\<\<\<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)", s[i:]) 

4520 if m is None: 

4521 i += 3 

4522 return i 

4523 # 14-SEP-2002 DTHEIN: needed to add \n to find word, not just string 

4524 delim = m.group(1) + '\n' 

4525 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: look after \n, not before 

4526 n = len(s) 

4527 while i < n and not g.match(s, i, delim): 

4528 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: move past \n 

4529 if i >= n: 

4530 g.scanError("Run on string: " + s[j:i]) 

4531 elif g.match(s, i, delim): 

4532 i += len(delim) 

4533 return i 

4534#@+node:ekr.20031218072017.3167: *4* g.skip_pp_directive 

4535def skip_pp_directive(s: str, i: int) -> int: 

4536 """Now handles continuation lines and block comments.""" 

4537 while i < len(s): 

4538 if g.is_nl(s, i): 

4539 if g.escaped(s, i): 

4540 i = g.skip_nl(s, i) 

4541 else: 

4542 break 

4543 elif g.match(s, i, "//"): 

4544 i = g.skip_to_end_of_line(s, i) 

4545 elif g.match(s, i, "/*"): 

4546 i = g.skip_block_comment(s, i) 

4547 else: 

4548 i += 1 

4549 return i 

4550#@+node:ekr.20031218072017.3168: *4* g.skip_pp_if 

4551# Skips an entire if or if def statement, including any nested statements. 

4552 

4553def skip_pp_if(s: str, i: int) -> Tuple[int, int]: 

4554 start_line = g.get_line(s, i) # used for error messages. 

4555 assert( 

4556 g.match_word(s, i, "#if") or 

4557 g.match_word(s, i, "#ifdef") or 

4558 g.match_word(s, i, "#ifndef")) 

4559 i = g.skip_line(s, i) 

4560 i, delta1 = g.skip_pp_part(s, i) 

4561 i = g.skip_ws(s, i) 

4562 if g.match_word(s, i, "#else"): 

4563 i = g.skip_line(s, i) 

4564 i = g.skip_ws(s, i) 

4565 i, delta2 = g.skip_pp_part(s, i) 

4566 if delta1 != delta2: 

4567 g.es("#if and #else parts have different braces:", start_line) 

4568 i = g.skip_ws(s, i) 

4569 if g.match_word(s, i, "#endif"): 

4570 i = g.skip_line(s, i) 

4571 else: 

4572 g.es("no matching #endif:", start_line) 

4573 return i, delta1 

4574#@+node:ekr.20031218072017.3169: *4* g.skip_pp_part 

4575# Skip to an #else or #endif. The caller has eaten the #if, #ifdef, #ifndef or #else 

4576 

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

4578 

4579 delta = 0 

4580 while i < len(s): 

4581 c = s[i] 

4582 if ( 

4583 g.match_word(s, i, "#if") or 

4584 g.match_word(s, i, "#ifdef") or 

4585 g.match_word(s, i, "#ifndef") 

4586 ): 

4587 i, delta1 = g.skip_pp_if(s, i) 

4588 delta += delta1 

4589 elif g.match_word(s, i, "#else") or g.match_word(s, i, "#endif"): 

4590 return i, delta 

4591 elif c == '\'' or c == '"': 

4592 i = g.skip_string(s, i) 

4593 elif c == '{': 

4594 delta += 1 

4595 i += 1 

4596 elif c == '}': 

4597 delta -= 1 

4598 i += 1 

4599 elif g.match(s, i, "//"): 

4600 i = g.skip_line(s, i) 

4601 elif g.match(s, i, "/*"): 

4602 i = g.skip_block_comment(s, i) 

4603 else: 

4604 i += 1 

4605 return i, delta 

4606#@+node:ekr.20031218072017.3171: *4* g.skip_to_semicolon 

4607# Skips to the next semicolon that is not in a comment or a string. 

4608 

4609def skip_to_semicolon(s: str, i: int) -> int: 

4610 n = len(s) 

4611 while i < n: 

4612 c = s[i] 

4613 if c == ';': 

4614 return i 

4615 if c == '\'' or c == '"': 

4616 i = g.skip_string(s, i) 

4617 elif g.match(s, i, "//"): 

4618 i = g.skip_to_end_of_line(s, i) 

4619 elif g.match(s, i, "/*"): 

4620 i = g.skip_block_comment(s, i) 

4621 else: 

4622 i += 1 

4623 return i 

4624#@+node:ekr.20031218072017.3172: *4* g.skip_typedef 

4625def skip_typedef(s: str, i: int) -> int: 

4626 n = len(s) 

4627 while i < n and g.is_c_id(s[i]): 

4628 i = g.skip_c_id(s, i) 

4629 i = g.skip_ws_and_nl(s, i) 

4630 if g.match(s, i, '{'): 

4631 i = g.skip_braces(s, i) 

4632 i = g.skip_to_semicolon(s, i) 

4633 return i 

4634#@+node:ekr.20201127143342.1: *3* g.see_more_lines 

4635def see_more_lines(s: str, ins: int, n: int=4) -> int: 

4636 """ 

4637 Extend index i within string s to include n more lines. 

4638 """ 

4639 # Show more lines, if they exist. 

4640 if n > 0: 

4641 for z in range(n): 

4642 if ins >= len(s): 

4643 break 

4644 i, j = g.getLine(s, ins) 

4645 ins = j 

4646 return max(0, min(ins, len(s))) 

4647#@+node:ekr.20031218072017.3195: *3* g.splitLines 

4648def splitLines(s: str) -> List[str]: 

4649 """ 

4650 Split s into lines, preserving the number of lines and 

4651 the endings of all lines, including the last line. 

4652 """ 

4653 return s.splitlines(True) if s else [] # This is a Python string function! 

4654 

4655splitlines = splitLines 

4656#@+node:ekr.20031218072017.3173: *3* Scanners: no error messages 

4657#@+node:ekr.20031218072017.3174: *4* g.escaped 

4658# Returns True if s[i] is preceded by an odd number of backslashes. 

4659 

4660def escaped(s: str, i: int) -> bool: 

4661 count = 0 

4662 while i - 1 >= 0 and s[i - 1] == '\\': 

4663 count += 1 

4664 i -= 1 

4665 return (count % 2) == 1 

4666#@+node:ekr.20031218072017.3175: *4* g.find_line_start 

4667def find_line_start(s: str, i: int) -> int: 

4668 """Return the index in s of the start of the line containing s[i].""" 

4669 if i < 0: 

4670 return 0 # New in Leo 4.4.5: add this defensive code. 

4671 # bug fix: 11/2/02: change i to i+1 in rfind 

4672 i = s.rfind('\n', 0, i + 1) # Finds the highest index in the range. 

4673 return 0 if i == -1 else i + 1 

4674#@+node:ekr.20031218072017.3176: *4* g.find_on_line 

4675def find_on_line(s: str, i: int, pattern: str) -> int: 

4676 j = s.find('\n', i) 

4677 if j == -1: 

4678 j = len(s) 

4679 k = s.find(pattern, i, j) 

4680 return k 

4681#@+node:ekr.20031218072017.3179: *4* g.g.is_special 

4682def is_special(s: str, directive: str) -> Tuple[bool, int]: 

4683 """Return True if the body text contains the @ directive.""" 

4684 assert(directive and directive[0] == '@') 

4685 lws = directive in ("@others", "@all") 

4686 # Most directives must start the line. 

4687 pattern_s = r'^\s*(%s\b)' if lws else r'^(%s\b)' 

4688 pattern = re.compile(pattern_s % directive, re.MULTILINE) 

4689 m = re.search(pattern, s) 

4690 if m: 

4691 return True, m.start(1) 

4692 return False, -1 

4693#@+node:ekr.20031218072017.3177: *4* g.is_c_id 

4694def is_c_id(ch: str) -> bool: 

4695 return g.isWordChar(ch) 

4696#@+node:ekr.20031218072017.3178: *4* g.is_nl 

4697def is_nl(s: str, i: int) -> bool: 

4698 return i < len(s) and (s[i] == '\n' or s[i] == '\r') 

4699#@+node:ekr.20031218072017.3180: *4* g.is_ws & is_ws_or_nl 

4700def is_ws(ch: str) -> bool: 

4701 return ch == '\t' or ch == ' ' 

4702 

4703def is_ws_or_nl(s: str, i: int) -> bool: 

4704 return g.is_nl(s, i) or (i < len(s) and g.is_ws(s[i])) 

4705#@+node:ekr.20031218072017.3181: *4* g.match 

4706# Warning: this code makes no assumptions about what follows pattern. 

4707 

4708def match(s: str, i: int, pattern: str) -> bool: 

4709 return bool(s and pattern and s.find(pattern, i, i + len(pattern)) == i) 

4710#@+node:ekr.20031218072017.3182: *4* g.match_c_word 

4711def match_c_word(s: str, i: int, name: str) -> bool: 

4712 n = len(name) 

4713 return bool( 

4714 name and 

4715 name == s[i : i + n] and 

4716 (i + n == len(s) or not g.is_c_id(s[i + n])) 

4717 ) 

4718#@+node:ekr.20031218072017.3183: *4* g.match_ignoring_case 

4719def match_ignoring_case(s1: str, s2: str) -> bool: 

4720 return bool(s1 and s2 and s1.lower() == s2.lower()) 

4721#@+node:ekr.20031218072017.3184: *4* g.match_word & g.match_words 

4722def match_word(s: str, i: int, pattern: str) -> bool: 

4723 

4724 # Using a regex is surprisingly tricky. 

4725 if pattern is None: 

4726 return False 

4727 if i > 0 and g.isWordChar(s[i - 1]): # Bug fix: 2017/06/01. 

4728 return False 

4729 j = len(pattern) 

4730 if j == 0: 

4731 return False 

4732 if s.find(pattern, i, i + j) != i: 

4733 return False 

4734 if i + j >= len(s): 

4735 return True 

4736 ch = s[i + j] 

4737 return not g.isWordChar(ch) 

4738 

4739def match_words(s: str, i: int, patterns: Sequence[str]) -> bool: 

4740 return any(g.match_word(s, i, pattern) for pattern in patterns) 

4741#@+node:ekr.20031218072017.3185: *4* g.skip_blank_lines 

4742# This routine differs from skip_ws_and_nl in that 

4743# it does not advance over whitespace at the start 

4744# of a non-empty or non-nl terminated line 

4745 

4746def skip_blank_lines(s: str, i: int) -> int: 

4747 while i < len(s): 

4748 if g.is_nl(s, i): 

4749 i = g.skip_nl(s, i) 

4750 elif g.is_ws(s[i]): 

4751 j = g.skip_ws(s, i) 

4752 if g.is_nl(s, j): 

4753 i = j 

4754 else: break 

4755 else: break 

4756 return i 

4757#@+node:ekr.20031218072017.3186: *4* g.skip_c_id 

4758def skip_c_id(s: str, i: int) -> int: 

4759 n = len(s) 

4760 while i < n and g.isWordChar(s[i]): 

4761 i += 1 

4762 return i 

4763#@+node:ekr.20040705195048: *4* g.skip_id 

4764def skip_id(s: str, i: int, chars: str=None) -> int: 

4765 chars = g.toUnicode(chars) if chars else '' 

4766 n = len(s) 

4767 while i < n and (g.isWordChar(s[i]) or s[i] in chars): 

4768 i += 1 

4769 return i 

4770#@+node:ekr.20031218072017.3187: *4* g.skip_line, skip_to_start/end_of_line 

4771#@+at These methods skip to the next newline, regardless of whether the 

4772# newline may be preceeded by a backslash. Consequently, they should be 

4773# used only when we know that we are not in a preprocessor directive or 

4774# string. 

4775#@@c 

4776 

4777def skip_line(s: str, i: int) -> int: 

4778 if i >= len(s): 

4779 return len(s) 

4780 if i < 0: 

4781 i = 0 

4782 i = s.find('\n', i) 

4783 if i == -1: 

4784 return len(s) 

4785 return i + 1 

4786 

4787def skip_to_end_of_line(s: str, i: int) -> int: 

4788 if i >= len(s): 

4789 return len(s) 

4790 if i < 0: 

4791 i = 0 

4792 i = s.find('\n', i) 

4793 if i == -1: 

4794 return len(s) 

4795 return i 

4796 

4797def skip_to_start_of_line(s: str, i: int) -> int: 

4798 if i >= len(s): 

4799 return len(s) 

4800 if i <= 0: 

4801 return 0 

4802 # Don't find s[i], so it doesn't matter if s[i] is a newline. 

4803 i = s.rfind('\n', 0, i) 

4804 if i == -1: 

4805 return 0 

4806 return i + 1 

4807#@+node:ekr.20031218072017.3188: *4* g.skip_long 

4808def skip_long(s: str, i: int) -> Tuple[int, Optional[int]]: 

4809 """ 

4810 Scan s[i:] for a valid int. 

4811 Return (i, val) or (i, None) if s[i] does not point at a number. 

4812 """ 

4813 val = 0 

4814 i = g.skip_ws(s, i) 

4815 n = len(s) 

4816 if i >= n or (not s[i].isdigit() and s[i] not in '+-'): 

4817 return i, None 

4818 j = i 

4819 if s[i] in '+-': # Allow sign before the first digit 

4820 i += 1 

4821 while i < n and s[i].isdigit(): 

4822 i += 1 

4823 try: # There may be no digits. 

4824 val = int(s[j:i]) 

4825 return i, val 

4826 except Exception: 

4827 return i, None 

4828#@+node:ekr.20031218072017.3190: *4* g.skip_nl 

4829# We need this function because different systems have different end-of-line conventions. 

4830 

4831def skip_nl(s: str, i: int) -> int: 

4832 """Skips a single "logical" end-of-line character.""" 

4833 if g.match(s, i, "\r\n"): 

4834 return i + 2 

4835 if g.match(s, i, '\n') or g.match(s, i, '\r'): 

4836 return i + 1 

4837 return i 

4838#@+node:ekr.20031218072017.3191: *4* g.skip_non_ws 

4839def skip_non_ws(s: str, i: int) -> int: 

4840 n = len(s) 

4841 while i < n and not g.is_ws(s[i]): 

4842 i += 1 

4843 return i 

4844#@+node:ekr.20031218072017.3192: *4* g.skip_pascal_braces 

4845# Skips from the opening { to the matching }. 

4846 

4847def skip_pascal_braces(s: str, i: int) -> int: 

4848 # No constructs are recognized inside Pascal block comments! 

4849 if i == -1: 

4850 return len(s) 

4851 return s.find('}', i) 

4852#@+node:ekr.20031218072017.3170: *4* g.skip_python_string 

4853def skip_python_string(s: str, i: int) -> int: 

4854 if g.match(s, i, "'''") or g.match(s, i, '"""'): 

4855 delim = s[i] * 3 

4856 i += 3 

4857 k = s.find(delim, i) 

4858 if k > -1: 

4859 return k + 3 

4860 return len(s) 

4861 return g.skip_string(s, i) 

4862#@+node:ekr.20031218072017.2369: *4* g.skip_string 

4863def skip_string(s: str, i: int) -> int: 

4864 """Scan forward to the end of a string.""" 

4865 delim = s[i] 

4866 i += 1 

4867 assert delim in '\'"', (repr(delim), repr(s)) 

4868 n = len(s) 

4869 while i < n and s[i] != delim: 

4870 if s[i] == '\\': 

4871 i += 2 

4872 else: 

4873 i += 1 

4874 if i >= n: 

4875 pass 

4876 elif s[i] == delim: 

4877 i += 1 

4878 return i 

4879#@+node:ekr.20031218072017.3193: *4* g.skip_to_char 

4880def skip_to_char(s: str, i: int, ch: str) -> Tuple[int, str]: 

4881 j = s.find(ch, i) 

4882 if j == -1: 

4883 return len(s), s[i:] 

4884 return j, s[i:j] 

4885#@+node:ekr.20031218072017.3194: *4* g.skip_ws, skip_ws_and_nl 

4886def skip_ws(s: str, i: int) -> int: 

4887 n = len(s) 

4888 while i < n and g.is_ws(s[i]): 

4889 i += 1 

4890 return i 

4891 

4892def skip_ws_and_nl(s: str, i: int) -> int: 

4893 n = len(s) 

4894 while i < n and (g.is_ws(s[i]) or g.is_nl(s, i)): 

4895 i += 1 

4896 return i 

4897#@+node:ekr.20170414034616.1: ** g.Git 

4898#@+node:ekr.20180325025502.1: *3* g.backupGitIssues 

4899def backupGitIssues(c: Cmdr, base_url: str=None) -> None: 

4900 """Get a list of issues from Leo's GitHub site.""" 

4901 if base_url is None: 

4902 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues' 

4903 

4904 root = c.lastTopLevel().insertAfter() 

4905 root.h = f'Backup of issues: {time.strftime("%Y/%m/%d")}' 

4906 label_list: List[str] = [] 

4907 GitIssueController().backup_issues(base_url, c, label_list, root) 

4908 root.expand() 

4909 c.selectPosition(root) 

4910 c.redraw() 

4911 g.trace('done') 

4912#@+node:ekr.20170616102324.1: *3* g.execGitCommand 

4913def execGitCommand(command: str, directory: str) -> List[str]: 

4914 """Execute the given git command in the given directory.""" 

4915 git_dir = g.os_path_finalize_join(directory, '.git') 

4916 if not g.os_path_exists(git_dir): 

4917 g.trace('not found:', git_dir, g.callers()) 

4918 return [] 

4919 if '\n' in command: 

4920 g.trace('removing newline from', command) 

4921 command = command.replace('\n', '') 

4922 # #1777: Save/restore os.curdir 

4923 old_dir = os.getcwd() 

4924 if directory: 

4925 os.chdir(directory) 

4926 try: 

4927 p = subprocess.Popen( 

4928 shlex.split(command), 

4929 stdout=subprocess.PIPE, 

4930 stderr=None, # Shows error traces. 

4931 shell=False, 

4932 ) 

4933 out, err = p.communicate() 

4934 lines = [g.toUnicode(z) for z in g.splitLines(out or [])] 

4935 finally: 

4936 os.chdir(old_dir) 

4937 return lines 

4938#@+node:ekr.20180126043905.1: *3* g.getGitIssues 

4939def getGitIssues(c: Cmdr, 

4940 base_url: str=None, 

4941 label_list: List=None, 

4942 milestone: str=None, 

4943 state: Optional[str]=None, # in (None, 'closed', 'open') 

4944) -> None: 

4945 """Get a list of issues from Leo's GitHub site.""" 

4946 if base_url is None: 

4947 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues' 

4948 if isinstance(label_list, (list, tuple)): 

4949 root = c.lastTopLevel().insertAfter() 

4950 root.h = 'Issues for ' + milestone if milestone else 'Backup' 

4951 GitIssueController().backup_issues(base_url, c, label_list, root) 

4952 root.expand() 

4953 c.selectPosition(root) 

4954 c.redraw() 

4955 g.trace('done') 

4956 else: 

4957 g.trace('label_list must be a list or tuple', repr(label_list)) 

4958#@+node:ekr.20180126044602.1: *4* class GitIssueController 

4959class GitIssueController: 

4960 """ 

4961 A class encapsulating the retrieval of GitHub issues. 

4962 

4963 The GitHub api: https://developer.github.com/v3/issues/ 

4964 """ 

4965 #@+others 

4966 #@+node:ekr.20180325023336.1: *5* git.backup_issues 

4967 def backup_issues(self, base_url: str, c: Cmdr, label_list: List, root: Pos, state: Any=None) -> None: 

4968 

4969 self.base_url = base_url 

4970 self.root = root 

4971 self.milestone = None 

4972 if label_list: 

4973 for state in ('closed', 'open'): 

4974 for label in label_list: 

4975 self.get_one_issue(label, state) 

4976 elif state is None: 

4977 for state in ('closed', 'open'): 

4978 organizer = root.insertAsLastChild() 

4979 organizer.h = f"{state} issues..." 

4980 self.get_all_issues(label_list, organizer, state) 

4981 elif state in ('closed', 'open'): 

4982 self.get_all_issues(label_list, root, state) 

4983 else: 

4984 g.es_print('state must be in (None, "open", "closed")') 

4985 #@+node:ekr.20180325024334.1: *5* git.get_all_issues 

4986 def get_all_issues(self, label_list: List, root: Pos, state: Any, limit: int=100) -> None: 

4987 """Get all issues for the base url.""" 

4988 try: 

4989 import requests 

4990 except Exception: 

4991 g.trace('requests not found: `pip install requests`') 

4992 return 

4993 label = None 

4994 assert state in ('open', 'closed') 

4995 page_url = self.base_url + '?&state=%s&page=%s' 

4996 page, total = 1, 0 

4997 while True: 

4998 url = page_url % (state, page) 

4999 r = requests.get(url) 

5000 try: 

5001 done, n = self.get_one_page(label, page, r, root) 

5002 # Do not remove this trace. It's reassuring. 

5003 g.trace(f"done: {done:5} page: {page:3} found: {n} label: {label}") 

5004 except AttributeError: 

5005 g.trace('Possible rate limit') 

5006 self.print_header(r) 

5007 g.es_exception() 

5008 break 

5009 total += n 

5010 if done: 

5011 break 

5012 page += 1 

5013 if page > limit: 

5014 g.trace('too many pages') 

5015 break 

5016 #@+node:ekr.20180126044850.1: *5* git.get_issues 

5017 def get_issues(self, base_url: str, label_list: List, milestone: Any, root: Pos, state: Any) -> None: 

5018 """Create a list of issues for each label in label_list.""" 

5019 self.base_url = base_url 

5020 self.milestone = milestone 

5021 self.root = root 

5022 for label in label_list: 

5023 self.get_one_issue(label, state) 

5024 #@+node:ekr.20180126043719.3: *5* git.get_one_issue 

5025 def get_one_issue(self, label: str, state: Any, limit: int=20) -> None: 

5026 """Create a list of issues with the given label.""" 

5027 try: 

5028 import requests 

5029 except Exception: 

5030 g.trace('requests not found: `pip install requests`') 

5031 return 

5032 root = self.root.insertAsLastChild() 

5033 page, total = 1, 0 

5034 page_url = self.base_url + '?labels=%s&state=%s&page=%s' 

5035 while True: 

5036 url = page_url % (label, state, page) 

5037 r = requests.get(url) 

5038 try: 

5039 done, n = self.get_one_page(label, page, r, root) 

5040 # Do not remove this trace. It's reassuring. 

5041 g.trace(f"done: {done:5} page: {page:3} found: {n:3} label: {label}") 

5042 except AttributeError: 

5043 g.trace('Possible rate limit') 

5044 self.print_header(r) 

5045 g.es_exception() 

5046 break 

5047 total += n 

5048 if done: 

5049 break 

5050 page += 1 

5051 if page > limit: 

5052 g.trace('too many pages') 

5053 break 

5054 state = state.capitalize() 

5055 if self.milestone: 

5056 root.h = f"{total} {state} {label} issues for milestone {self.milestone}" 

5057 else: 

5058 root.h = f"{total} {state} {label} issues" 

5059 #@+node:ekr.20180126043719.4: *5* git.get_one_page 

5060 def get_one_page(self, label: str, page: int, r: Any, root: Pos) -> Tuple[bool, int]: 

5061 

5062 if self.milestone: 

5063 aList = [ 

5064 z for z in r.json() 

5065 if z.get('milestone') is not None and 

5066 self.milestone == z.get('milestone').get('title') 

5067 ] 

5068 else: 

5069 aList = [z for z in r.json()] 

5070 for d in aList: 

5071 n, title = d.get('number'), d.get('title') 

5072 html_url = d.get('html_url') or self.base_url 

5073 p = root.insertAsNthChild(0) 

5074 p.h = f"#{n}: {title}" 

5075 p.b = f"{html_url}\n\n" 

5076 p.b += d.get('body').strip() 

5077 link = r.headers.get('Link') 

5078 done = not link or link.find('rel="next"') == -1 

5079 return done, len(aList) 

5080 #@+node:ekr.20180127092201.1: *5* git.print_header 

5081 def print_header(self, r: Any) -> None: 

5082 

5083 # r.headers is a CaseInsensitiveDict 

5084 # so g.printObj(r.headers) is just repr(r.headers) 

5085 if 0: 

5086 print('Link', r.headers.get('Link')) 

5087 else: 

5088 for key in r.headers: 

5089 print(f"{key:35}: {r.headers.get(key)}") 

5090 #@-others 

5091#@+node:ekr.20190428173354.1: *3* g.getGitVersion 

5092def getGitVersion(directory: str=None) -> Tuple[str, str, str]: 

5093 """Return a tuple (author, build, date) from the git log, or None.""" 

5094 # 

5095 # -n: Get only the last log. 

5096 trace = 'git' in g.app.debug 

5097 try: 

5098 s = subprocess.check_output( 

5099 'git log -n 1 --date=iso', 

5100 cwd=directory or g.app.loadDir, 

5101 stderr=subprocess.DEVNULL, 

5102 shell=True, 

5103 ) 

5104 # #1209. 

5105 except subprocess.CalledProcessError as e: 

5106 s = e.output 

5107 if trace: 

5108 g.trace('return code', e.returncode) 

5109 g.trace('value', repr(s)) 

5110 g.es_print('Exception in g.getGitVersion') 

5111 g.es_exception() 

5112 s = g.toUnicode(s) 

5113 if not isinstance(s, str): 

5114 return '', '', '' 

5115 except Exception: 

5116 if trace: 

5117 g.es_print('Exception in g.getGitVersion') 

5118 g.es_exception() 

5119 return '', '', '' 

5120 

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

5122 

5123 def find(kind: str) -> str: 

5124 """Return the given type of log line.""" 

5125 for z in info: 

5126 if z.startswith(kind): 

5127 return z.lstrip(kind).lstrip(':').strip() 

5128 return '' 

5129 

5130 return find('Author'), find('commit')[:10], find('Date') 

5131#@+node:ekr.20170414034616.2: *3* g.gitBranchName 

5132def gitBranchName(path: str=None) -> str: 

5133 """ 

5134 Return the git branch name associated with path/.git, or the empty 

5135 string if path/.git does not exist. If path is None, use the leo-editor 

5136 directory. 

5137 """ 

5138 branch, commit = g.gitInfo(path) 

5139 return branch 

5140#@+node:ekr.20170414034616.4: *3* g.gitCommitNumber 

5141def gitCommitNumber(path: str=None) -> str: 

5142 """ 

5143 Return the git commit number associated with path/.git, or the empty 

5144 string if path/.git does not exist. If path is None, use the leo-editor 

5145 directory. 

5146 """ 

5147 branch, commit = g.gitInfo(path) 

5148 return commit 

5149#@+node:ekr.20200724132432.1: *3* g.gitInfoForFile 

5150def gitInfoForFile(filename: str) -> Tuple[str, str]: 

5151 """ 

5152 Return the git (branch, commit) info associated for the given file. 

5153 """ 

5154 # g.gitInfo and g.gitHeadPath now do all the work. 

5155 return g.gitInfo(filename) 

5156#@+node:ekr.20200724133754.1: *3* g.gitInfoForOutline 

5157def gitInfoForOutline(c: Cmdr) -> Tuple[str, str]: 

5158 """ 

5159 Return the git (branch, commit) info associated for commander c. 

5160 """ 

5161 return g.gitInfoForFile(c.fileName()) 

5162#@+node:maphew.20171112205129.1: *3* g.gitDescribe 

5163def gitDescribe(path: str=None) -> Tuple[str, str, str]: 

5164 """ 

5165 Return the Git tag, distance-from-tag, and commit hash for the 

5166 associated path. If path is None, use the leo-editor directory. 

5167 

5168 Given `git describe` cmd line output: `x-leo-v5.6-55-ge1129da\n` 

5169 This function returns ('x-leo-v5.6', '55', 'e1129da') 

5170 """ 

5171 describe = g.execGitCommand('git describe --tags --long', path) 

5172 tag, distance, commit = describe[0].rsplit('-', 2) 

5173 # rsplit not split, as '-' might be in tag name 

5174 if 'g' in commit[0:]: 

5175 # leading 'g' isn't part of the commit hash 

5176 commit = commit[1:] 

5177 commit = commit.rstrip() 

5178 return tag, distance, commit 

5179#@+node:ekr.20170414034616.6: *3* g.gitHeadPath 

5180def gitHeadPath(path_s: str) -> Optional[str]: 

5181 """ 

5182 Compute the path to .git/HEAD given the path. 

5183 """ 

5184 path = Path(path_s) 

5185 # #1780: Look up the directory tree, looking the .git directory. 

5186 while os.path.exists(path): 

5187 head = os.path.join(path, '.git', 'HEAD') 

5188 if os.path.exists(head): 

5189 return head 

5190 if path == path.parent: 

5191 break 

5192 path = path.parent 

5193 return None 

5194#@+node:ekr.20170414034616.3: *3* g.gitInfo 

5195def gitInfo(path: str=None) -> Tuple[str, str]: 

5196 """ 

5197 Path may be a directory or file. 

5198 

5199 Return the branch and commit number or ('', ''). 

5200 """ 

5201 branch, commit = '', '' # Set defaults. 

5202 if path is None: 

5203 # Default to leo/core. 

5204 path = os.path.dirname(__file__) 

5205 if not os.path.isdir(path): 

5206 path = os.path.dirname(path) 

5207 # Does path/../ref exist? 

5208 path = g.gitHeadPath(path) 

5209 if not path: 

5210 return branch, commit 

5211 try: 

5212 with open(path) as f: 

5213 s = f.read() 

5214 if not s.startswith('ref'): 

5215 branch = 'None' 

5216 commit = s[:7] 

5217 return branch, commit 

5218 # On a proper branch 

5219 pointer = s.split()[1] 

5220 dirs = pointer.split('/') 

5221 branch = dirs[-1] 

5222 except IOError: 

5223 g.trace('can not open:', path) 

5224 return branch, commit 

5225 # Try to get a better commit number. 

5226 git_dir = g.os_path_finalize_join(path, '..') 

5227 try: 

5228 path = g.os_path_finalize_join(git_dir, pointer) 

5229 with open(path) as f: # type:ignore 

5230 s = f.read() 

5231 commit = s.strip()[0:12] 

5232 # shorten the hash to a unique shortname 

5233 except IOError: 

5234 try: 

5235 path = g.os_path_finalize_join(git_dir, 'packed-refs') 

5236 with open(path) as f: # type:ignore 

5237 for line in f: 

5238 if line.strip().endswith(' ' + pointer): 

5239 commit = line.split()[0][0:12] 

5240 break 

5241 except IOError: 

5242 pass 

5243 return branch, commit 

5244#@+node:ekr.20031218072017.3139: ** g.Hooks & Plugins 

5245#@+node:ekr.20101028131948.5860: *3* g.act_on_node 

5246def dummy_act_on_node(c: Cmdr, p: Pos, event: Any) -> None: 

5247 pass 

5248 

5249# This dummy definition keeps pylint happy. 

5250# Plugins can change this. 

5251 

5252act_on_node = dummy_act_on_node 

5253#@+node:ville.20120502221057.7500: *3* g.childrenModifiedSet, g.contentModifiedSet 

5254childrenModifiedSet: Set["VNode"] = set() 

5255contentModifiedSet: Set["VNode"] = set() 

5256#@+node:ekr.20031218072017.1596: *3* g.doHook 

5257def doHook(tag: str, *args: Any, **keywords: Any) -> Any: 

5258 """ 

5259 This global function calls a hook routine. Hooks are identified by the 

5260 tag param. 

5261 

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

5263 an exception. 

5264 

5265 We look for a hook routine in three places: 

5266 1. c.hookFunction 

5267 2. app.hookFunction 

5268 3. leoPlugins.doPlugins() 

5269 

5270 Set app.hookError on all exceptions. 

5271 Scripts may reset app.hookError to try again. 

5272 """ 

5273 if g.app.killed or g.app.hookError: 

5274 return None 

5275 if args: 

5276 # A minor error in Leo's core. 

5277 g.pr(f"***ignoring args param. tag = {tag}") 

5278 if not g.app.config.use_plugins: 

5279 if tag in ('open0', 'start1'): 

5280 g.warning("Plugins disabled: use_plugins is 0 in a leoSettings.leo file.") 

5281 return None 

5282 # Get the hook handler function. Usually this is doPlugins. 

5283 c = keywords.get("c") 

5284 # pylint: disable=consider-using-ternary 

5285 f = (c and c.hookFunction) or g.app.hookFunction 

5286 if not f: 

5287 g.app.hookFunction = f = g.app.pluginsController.doPlugins 

5288 try: 

5289 # Pass the hook to the hook handler. 

5290 # g.pr('doHook',f.__name__,keywords.get('c')) 

5291 return f(tag, keywords) 

5292 except Exception: 

5293 g.es_exception() 

5294 g.app.hookError = True # Supress this function. 

5295 g.app.idle_time_hooks_enabled = False 

5296 return None 

5297#@+node:ekr.20100910075900.5950: *3* g.Wrappers for g.app.pluginController methods 

5298# Important: we can not define g.pc here! 

5299#@+node:ekr.20100910075900.5951: *4* g.Loading & registration 

5300def loadOnePlugin(pluginName: str, verbose: bool=False) -> Any: 

5301 pc = g.app.pluginsController 

5302 return pc.loadOnePlugin(pluginName, verbose=verbose) 

5303 

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

5305 pc = g.app.pluginsController 

5306 return pc.registerExclusiveHandler(tags, fn) 

5307 

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

5309 pc = g.app.pluginsController 

5310 return pc.registerHandler(tags, fn) 

5311 

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

5313 pc = g.app.pluginsController 

5314 return pc.plugin_signon(module_name, verbose) 

5315 

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

5317 pc = g.app.pluginsController 

5318 return pc.unloadOnePlugin(moduleOrFileName, verbose) 

5319 

5320def unregisterHandler(tags: Any, fn: Any) -> Any: 

5321 pc = g.app.pluginsController 

5322 return pc.unregisterHandler(tags, fn) 

5323#@+node:ekr.20100910075900.5952: *4* g.Information 

5324def getHandlersForTag(tags: List[str]) -> List: 

5325 pc = g.app.pluginsController 

5326 return pc.getHandlersForTag(tags) 

5327 

5328def getLoadedPlugins() -> List: 

5329 pc = g.app.pluginsController 

5330 return pc.getLoadedPlugins() 

5331 

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

5333 pc = g.app.pluginsController 

5334 return pc.getPluginModule(moduleName) 

5335 

5336def pluginIsLoaded(fn: str) -> bool: 

5337 pc = g.app.pluginsController 

5338 return pc.isLoaded(fn) 

5339#@+node:ekr.20031218072017.1315: ** g.Idle time functions 

5340#@+node:EKR.20040602125018.1: *3* g.disableIdleTimeHook 

5341def disableIdleTimeHook() -> None: 

5342 """Disable the global idle-time hook.""" 

5343 g.app.idle_time_hooks_enabled = False 

5344#@+node:EKR.20040602125018: *3* g.enableIdleTimeHook 

5345def enableIdleTimeHook(*args: Any, **keys: Any) -> None: 

5346 """Enable idle-time processing.""" 

5347 g.app.idle_time_hooks_enabled = True 

5348#@+node:ekr.20140825042850.18410: *3* g.IdleTime 

5349def IdleTime(handler: Any, delay: int=500, tag: str=None) -> Any: 

5350 """ 

5351 A thin wrapper for the LeoQtGui.IdleTime class. 

5352 

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

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

5355 

5356 def handler(timer): 

5357 '''IdleTime handler. timer is an IdleTime instance.''' 

5358 delta_t = timer.time-timer.starting_time 

5359 g.trace(timer.count, '%2.4f' % (delta_t)) 

5360 if timer.count >= 5: 

5361 g.trace('done') 

5362 timer.stop() 

5363 

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

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

5366 if timer: timer.start() 

5367 

5368 Timer instances are completely independent:: 

5369 

5370 def handler1(timer): 

5371 delta_t = timer.time-timer.starting_time 

5372 g.trace('%2s %2.4f' % (timer.count,delta_t)) 

5373 if timer.count >= 5: 

5374 g.trace('done') 

5375 timer.stop() 

5376 

5377 def handler2(timer): 

5378 delta_t = timer.time-timer.starting_time 

5379 g.trace('%2s %2.4f' % (timer.count,delta_t)) 

5380 if timer.count >= 10: 

5381 g.trace('done') 

5382 timer.stop() 

5383 

5384 timer1 = g.IdleTime(handler1, delay=500) 

5385 timer2 = g.IdleTime(handler2, delay=1000) 

5386 if timer1 and timer2: 

5387 timer1.start() 

5388 timer2.start() 

5389 """ 

5390 try: 

5391 return g.app.gui.idleTimeClass(handler, delay, tag) 

5392 except Exception: 

5393 return None 

5394#@+node:ekr.20161027205025.1: *3* g.idleTimeHookHandler (stub) 

5395def idleTimeHookHandler(timer: Any) -> None: 

5396 """This function exists for compatibility.""" 

5397 g.es_print('Replaced by IdleTimeManager.on_idle') 

5398 g.trace(g.callers()) 

5399#@+node:ekr.20041219095213: ** g.Importing 

5400#@+node:ekr.20040917061619: *3* g.cantImport 

5401def cantImport(moduleName: str, pluginName: str=None, verbose: bool=True) -> None: 

5402 """Print a "Can't Import" message and return None.""" 

5403 s = f"Can not import {moduleName}" 

5404 if pluginName: 

5405 s = s + f" from {pluginName}" 

5406 if not g.app or not g.app.gui: 

5407 print(s) 

5408 elif g.unitTesting: 

5409 return 

5410 else: 

5411 g.warning('', s) 

5412#@+node:ekr.20191220044128.1: *3* g.import_module 

5413def import_module(name: str, package: str=None) -> Any: 

5414 """ 

5415 A thin wrapper over importlib.import_module. 

5416 """ 

5417 trace = 'plugins' in g.app.debug and not g.unitTesting 

5418 exceptions = [] 

5419 try: 

5420 m = importlib.import_module(name, package=package) 

5421 except Exception as e: 

5422 m = None 

5423 if trace: 

5424 t, v, tb = sys.exc_info() 

5425 del tb # don't need the traceback 

5426 v = v or str(t) # type:ignore 

5427 # # in case v is empty, we'll at least have the execption type 

5428 if v not in exceptions: 

5429 exceptions.append(v) 

5430 g.trace(f"Can not import {name}: {e}") 

5431 return m 

5432#@+node:ekr.20140711071454.17650: ** g.Indices, Strings, Unicode & Whitespace 

5433#@+node:ekr.20140711071454.17647: *3* g.Indices 

5434#@+node:ekr.20050314140957: *4* g.convertPythonIndexToRowCol 

5435def convertPythonIndexToRowCol(s: str, i: int) -> Tuple[int, int]: 

5436 """Convert index i into string s into zero-based row/col indices.""" 

5437 if not s or i <= 0: 

5438 return 0, 0 

5439 i = min(i, len(s)) 

5440 # works regardless of what s[i] is 

5441 row = s.count('\n', 0, i) # Don't include i 

5442 if row == 0: 

5443 return row, i 

5444 prevNL = s.rfind('\n', 0, i) # Don't include i 

5445 return row, i - prevNL - 1 

5446#@+node:ekr.20050315071727: *4* g.convertRowColToPythonIndex 

5447def convertRowColToPythonIndex(s: str, row: int, col: int, lines: List[str]=None) -> int: 

5448 """Convert zero-based row/col indices into a python index into string s.""" 

5449 if row < 0: 

5450 return 0 

5451 if lines is None: 

5452 lines = g.splitLines(s) 

5453 if row >= len(lines): 

5454 return len(s) 

5455 col = min(col, len(lines[row])) 

5456 # A big bottleneck 

5457 prev = 0 

5458 for line in lines[:row]: 

5459 prev += len(line) 

5460 return prev + col 

5461#@+node:ekr.20061031102333.2: *4* g.getWord & getLine 

5462def getWord(s: str, i: int) -> Tuple[int, int]: 

5463 """Return i,j such that s[i:j] is the word surrounding s[i].""" 

5464 if i >= len(s): 

5465 i = len(s) - 1 

5466 if i < 0: 

5467 i = 0 

5468 # Scan backwards. 

5469 while 0 <= i < len(s) and g.isWordChar(s[i]): 

5470 i -= 1 

5471 i += 1 

5472 # Scan forwards. 

5473 j = i 

5474 while 0 <= j < len(s) and g.isWordChar(s[j]): 

5475 j += 1 

5476 return i, j 

5477 

5478def getLine(s: str, i: int) -> Tuple[int, int]: 

5479 """ 

5480 Return i,j such that s[i:j] is the line surrounding s[i]. 

5481 s[i] is a newline only if the line is empty. 

5482 s[j] is a newline unless there is no trailing newline. 

5483 """ 

5484 if i > len(s): 

5485 i = len(s) - 1 

5486 if i < 0: 

5487 i = 0 

5488 # A newline *ends* the line, so look to the left of a newline. 

5489 j = s.rfind('\n', 0, i) 

5490 if j == -1: 

5491 j = 0 

5492 else: 

5493 j += 1 

5494 k = s.find('\n', i) 

5495 if k == -1: 

5496 k = len(s) 

5497 else: 

5498 k = k + 1 

5499 return j, k 

5500#@+node:ekr.20111114151846.9847: *4* g.toPythonIndex 

5501def toPythonIndex(s: str, index: int) -> int: 

5502 """ 

5503 Convert index to a Python int. 

5504 

5505 index may be a Tk index (x.y) or 'end'. 

5506 """ 

5507 if index is None: 

5508 return 0 

5509 if isinstance(index, int): 

5510 return index 

5511 if index == '1.0': 

5512 return 0 

5513 if index == 'end': 

5514 return len(s) 

5515 data = index.split('.') 

5516 if len(data) == 2: 

5517 row, col = data 

5518 row, col = int(row), int(col) 

5519 i = g.convertRowColToPythonIndex(s, row - 1, col) 

5520 return i 

5521 g.trace(f"bad string index: {index}") 

5522 return 0 

5523#@+node:ekr.20140526144610.17601: *3* g.Strings 

5524#@+node:ekr.20190503145501.1: *4* g.isascii 

5525def isascii(s: str) -> bool: 

5526 # s.isascii() is defined in Python 3.7. 

5527 return all(ord(ch) < 128 for ch in s) 

5528#@+node:ekr.20031218072017.3106: *4* g.angleBrackets & virtual_event_name 

5529def angleBrackets(s: str) -> str: 

5530 """Returns < < s > >""" 

5531 lt = "<<" 

5532 rt = ">>" 

5533 return lt + s + rt 

5534 

5535virtual_event_name = angleBrackets 

5536#@+node:ekr.20090516135452.5777: *4* g.ensureLeading/TrailingNewlines 

5537def ensureLeadingNewlines(s: str, n: int) -> str: 

5538 s = g.removeLeading(s, '\t\n\r ') 

5539 return ('\n' * n) + s 

5540 

5541def ensureTrailingNewlines(s: str, n: int) -> str: 

5542 s = g.removeTrailing(s, '\t\n\r ') 

5543 return s + '\n' * n 

5544#@+node:ekr.20050920084036.4: *4* g.longestCommonPrefix & g.itemsMatchingPrefixInList 

5545def longestCommonPrefix(s1: str, s2: str) -> str: 

5546 """Find the longest prefix common to strings s1 and s2.""" 

5547 prefix = '' 

5548 for ch in s1: 

5549 if s2.startswith(prefix + ch): 

5550 prefix = prefix + ch 

5551 else: 

5552 return prefix 

5553 return prefix 

5554 

5555def itemsMatchingPrefixInList(s: str, aList: List[str], matchEmptyPrefix: bool=False) -> Tuple[List, str]: 

5556 """This method returns a sorted list items of aList whose prefix is s. 

5557 

5558 It also returns the longest common prefix of all the matches. 

5559 """ 

5560 if s: 

5561 pmatches = [a for a in aList if a.startswith(s)] 

5562 elif matchEmptyPrefix: 

5563 pmatches = aList[:] 

5564 else: pmatches = [] 

5565 if pmatches: 

5566 pmatches.sort() 

5567 common_prefix = reduce(g.longestCommonPrefix, pmatches) 

5568 else: 

5569 common_prefix = '' 

5570 return pmatches, common_prefix 

5571#@+node:ekr.20090516135452.5776: *4* g.removeLeading/Trailing 

5572# Warning: g.removeTrailingWs already exists. 

5573# Do not change it! 

5574 

5575def removeLeading(s: str, chars: str) -> str: 

5576 """Remove all characters in chars from the front of s.""" 

5577 i = 0 

5578 while i < len(s) and s[i] in chars: 

5579 i += 1 

5580 return s[i:] 

5581 

5582def removeTrailing(s: str, chars: str) -> str: 

5583 """Remove all characters in chars from the end of s.""" 

5584 i = len(s) - 1 

5585 while i >= 0 and s[i] in chars: 

5586 i -= 1 

5587 i += 1 

5588 return s[:i] 

5589#@+node:ekr.20060410112600: *4* g.stripBrackets 

5590def stripBrackets(s: str) -> str: 

5591 """Strip leading and trailing angle brackets.""" 

5592 if s.startswith('<'): 

5593 s = s[1:] 

5594 if s.endswith('>'): 

5595 s = s[:-1] 

5596 return s 

5597#@+node:ekr.20170317101100.1: *4* g.unCamel 

5598def unCamel(s: str) -> List[str]: 

5599 """Return a list of sub-words in camelCased string s.""" 

5600 result: List[str] = [] 

5601 word: List[str] = [] 

5602 for ch in s: 

5603 if ch.isalpha() and ch.isupper(): 

5604 if word: 

5605 result.append(''.join(word)) 

5606 word = [ch] 

5607 elif ch.isalpha(): 

5608 word.append(ch) 

5609 elif word: 

5610 result.append(''.join(word)) 

5611 word = [] 

5612 if word: 

5613 result.append(''.join(word)) 

5614 return result 

5615#@+node:ekr.20031218072017.1498: *3* g.Unicode 

5616#@+node:ekr.20190505052756.1: *4* g.checkUnicode 

5617checkUnicode_dict: Dict[str, bool] = {} 

5618 

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

5620 """ 

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

5622 

5623 This method is meant to document defensive programming. We don't expect 

5624 these errors, but they might arise as the result of problems in 

5625 user-defined plugins or scripts. 

5626 """ 

5627 tag = 'g.checkUnicode' 

5628 if s is None and g.unitTesting: 

5629 return '' 

5630 if isinstance(s, str): 

5631 return s 

5632 if not isinstance(s, bytes): 

5633 g.error(f"{tag}: unexpected argument: {s!r}") 

5634 return '' 

5635 # 

5636 # Report the unexpected conversion. 

5637 callers = g.callers(1) 

5638 if callers not in checkUnicode_dict: 

5639 g.trace(g.callers()) 

5640 g.error(f"\n{tag}: expected unicode. got: {s!r}\n") 

5641 checkUnicode_dict[callers] = True 

5642 # 

5643 # Convert to unicode, reporting all errors. 

5644 if not encoding: 

5645 encoding = 'utf-8' 

5646 try: 

5647 s = s.decode(encoding, 'strict') 

5648 except(UnicodeDecodeError, UnicodeError): 

5649 # https://wiki.python.org/moin/UnicodeDecodeError 

5650 s = s.decode(encoding, 'replace') 

5651 g.trace(g.callers()) 

5652 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}") 

5653 except Exception: 

5654 g.trace(g.callers()) 

5655 g.es_excption() 

5656 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}") 

5657 return s 

5658#@+node:ekr.20100125073206.8709: *4* g.getPythonEncodingFromString 

5659def getPythonEncodingFromString(s: str) -> str: 

5660 """Return the encoding given by Python's encoding line. 

5661 s is the entire file. 

5662 """ 

5663 encoding = None 

5664 tag, tag2 = '# -*- coding:', '-*-' 

5665 n1, n2 = len(tag), len(tag2) 

5666 if s: 

5667 # For Python 3.x we must convert to unicode before calling startswith. 

5668 # The encoding doesn't matter: we only look at the first line, and if 

5669 # the first line is an encoding line, it will contain only ascii characters. 

5670 s = g.toUnicode(s, encoding='ascii', reportErrors=False) 

5671 lines = g.splitLines(s) 

5672 line1 = lines[0].strip() 

5673 if line1.startswith(tag) and line1.endswith(tag2): 

5674 e = line1[n1 : -n2].strip() 

5675 if e and g.isValidEncoding(e): 

5676 encoding = e 

5677 elif g.match_word(line1, 0, '@first'): # 2011/10/21. 

5678 line1 = line1[len('@first') :].strip() 

5679 if line1.startswith(tag) and line1.endswith(tag2): 

5680 e = line1[n1 : -n2].strip() 

5681 if e and g.isValidEncoding(e): 

5682 encoding = e 

5683 return encoding 

5684#@+node:ekr.20031218072017.1500: *4* g.isValidEncoding 

5685def isValidEncoding(encoding: str) -> bool: 

5686 """Return True if the encooding is valid.""" 

5687 if not encoding: 

5688 return False 

5689 if sys.platform == 'cli': 

5690 return True 

5691 try: 

5692 codecs.lookup(encoding) 

5693 return True 

5694 except LookupError: # Windows 

5695 return False 

5696 except AttributeError: # Linux 

5697 return False 

5698 except Exception: 

5699 # UnicodeEncodeError 

5700 g.es_print('Please report the following error') 

5701 g.es_exception() 

5702 return False 

5703#@+node:ekr.20061006152327: *4* g.isWordChar & g.isWordChar1 

5704def isWordChar(ch: str) -> bool: 

5705 """Return True if ch should be considered a letter.""" 

5706 return bool(ch and (ch.isalnum() or ch == '_')) 

5707 

5708def isWordChar1(ch: str) -> bool: 

5709 return bool(ch and (ch.isalpha() or ch == '_')) 

5710#@+node:ekr.20130910044521.11304: *4* g.stripBOM 

5711def stripBOM(s: str) -> Tuple[Optional[str], str]: 

5712 """ 

5713 If there is a BOM, return (e,s2) where e is the encoding 

5714 implied by the BOM and s2 is the s stripped of the BOM. 

5715 

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

5717 

5718 s must be the contents of a file (a string) read in binary mode. 

5719 """ 

5720 table = ( 

5721 # Important: test longer bom's first. 

5722 (4, 'utf-32', codecs.BOM_UTF32_BE), 

5723 (4, 'utf-32', codecs.BOM_UTF32_LE), 

5724 (3, 'utf-8', codecs.BOM_UTF8), 

5725 (2, 'utf-16', codecs.BOM_UTF16_BE), 

5726 (2, 'utf-16', codecs.BOM_UTF16_LE), 

5727 ) 

5728 if s: 

5729 for n, e, bom in table: 

5730 assert len(bom) == n 

5731 if bom == s[: len(bom)]: 

5732 return e, s[len(bom) :] 

5733 return None, s 

5734#@+node:ekr.20050208093800: *4* g.toEncodedString 

5735def toEncodedString(s: str, encoding: str='utf-8', reportErrors: bool=False) -> bytes: 

5736 """Convert unicode string to an encoded string.""" 

5737 if not isinstance(s, str): 

5738 return s 

5739 if not encoding: 

5740 encoding = 'utf-8' 

5741 # These are the only significant calls to s.encode in Leo. 

5742 try: 

5743 s = s.encode(encoding, "strict") # type:ignore 

5744 except UnicodeError: 

5745 s = s.encode(encoding, "replace") # type:ignore 

5746 if reportErrors: 

5747 g.error(f"Error converting {s} from unicode to {encoding} encoding") 

5748 # Tracing these calls directly yields thousands of calls. 

5749 return s # type:ignore 

5750#@+node:ekr.20050208093800.1: *4* g.toUnicode 

5751unicode_warnings: Dict[str, bool] = {} # Keys are g.callers. 

5752 

5753def toUnicode(s: Any, encoding: str=None, reportErrors: bool=False) -> str: 

5754 """Convert bytes to unicode if necessary.""" 

5755 if isinstance(s, str): 

5756 return s 

5757 tag = 'g.toUnicode' 

5758 if not isinstance(s, bytes): 

5759 if not isinstance(s, (NullObject, TracingNullObject)): 

5760 callers = g.callers() 

5761 if callers not in unicode_warnings: 

5762 unicode_warnings[callers] = True 

5763 g.error(f"{tag}: unexpected argument of type {s.__class__.__name__}") 

5764 g.trace(callers) 

5765 return '' 

5766 if not encoding: 

5767 encoding = 'utf-8' 

5768 try: 

5769 s = s.decode(encoding, 'strict') 

5770 except(UnicodeDecodeError, UnicodeError): 

5771 # https://wiki.python.org/moin/UnicodeDecodeError 

5772 s = s.decode(encoding, 'replace') 

5773 if reportErrors: 

5774 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}") 

5775 g.trace(g.callers()) 

5776 except Exception: 

5777 g.es_exception() 

5778 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}") 

5779 g.trace(g.callers()) 

5780 return s 

5781#@+node:ekr.20031218072017.3197: *3* g.Whitespace 

5782#@+node:ekr.20031218072017.3198: *4* g.computeLeadingWhitespace 

5783# Returns optimized whitespace corresponding to width with the indicated tab_width. 

5784 

5785def computeLeadingWhitespace(width: int, tab_width: int) -> str: 

5786 if width <= 0: 

5787 return "" 

5788 if tab_width > 1: 

5789 tabs = int(width / tab_width) 

5790 blanks = int(width % tab_width) 

5791 return ('\t' * tabs) + (' ' * blanks) 

5792 # Negative tab width always gets converted to blanks. 

5793 return ' ' * width 

5794#@+node:ekr.20120605172139.10263: *4* g.computeLeadingWhitespaceWidth 

5795# Returns optimized whitespace corresponding to width with the indicated tab_width. 

5796 

5797def computeLeadingWhitespaceWidth(s: str, tab_width: int) -> int: 

5798 w = 0 

5799 for ch in s: 

5800 if ch == ' ': 

5801 w += 1 

5802 elif ch == '\t': 

5803 w += (abs(tab_width) - (w % abs(tab_width))) 

5804 else: 

5805 break 

5806 return w 

5807#@+node:ekr.20031218072017.3199: *4* g.computeWidth 

5808# Returns the width of s, assuming s starts a line, with indicated tab_width. 

5809 

5810def computeWidth(s: str, tab_width: int) -> int: 

5811 w = 0 

5812 for ch in s: 

5813 if ch == '\t': 

5814 w += (abs(tab_width) - (w % abs(tab_width))) 

5815 elif ch == '\n': # Bug fix: 2012/06/05. 

5816 break 

5817 else: 

5818 w += 1 

5819 return w 

5820#@+node:ekr.20110727091744.15083: *4* g.wrap_lines (newer) 

5821#@@language rest 

5822#@+at 

5823# Important note: this routine need not deal with leading whitespace. 

5824# 

5825# Instead, the caller should simply reduce pageWidth by the width of 

5826# leading whitespace wanted, then add that whitespace to the lines 

5827# returned here. 

5828# 

5829# The key to this code is the invarient that line never ends in whitespace. 

5830#@@c 

5831#@@language python 

5832 

5833def wrap_lines(lines: List[str], pageWidth: int, firstLineWidth: int=None) -> List[str]: 

5834 """Returns a list of lines, consisting of the input lines wrapped to the given pageWidth.""" 

5835 if pageWidth < 10: 

5836 pageWidth = 10 

5837 # First line is special 

5838 if not firstLineWidth: 

5839 firstLineWidth = pageWidth 

5840 if firstLineWidth < 10: 

5841 firstLineWidth = 10 

5842 outputLineWidth = firstLineWidth 

5843 # Sentence spacing 

5844 # This should be determined by some setting, and can only be either 1 or 2 

5845 sentenceSpacingWidth = 1 

5846 assert 0 < sentenceSpacingWidth < 3 

5847 result = [] # The lines of the result. 

5848 line = "" # The line being formed. It never ends in whitespace. 

5849 for s in lines: 

5850 i = 0 

5851 while i < len(s): 

5852 assert len(line) <= outputLineWidth # DTHEIN 18-JAN-2004 

5853 j = g.skip_ws(s, i) 

5854 k = g.skip_non_ws(s, j) 

5855 word = s[j:k] 

5856 assert k > i 

5857 i = k 

5858 # DTHEIN 18-JAN-2004: wrap at exactly the text width, 

5859 # not one character less 

5860 # 

5861 wordLen = len(word) 

5862 if line.endswith('.') or line.endswith('?') or line.endswith('!'): 

5863 space = ' ' * sentenceSpacingWidth 

5864 else: 

5865 space = ' ' 

5866 if line and wordLen > 0: 

5867 wordLen += len(space) 

5868 if wordLen + len(line) <= outputLineWidth: 

5869 if wordLen > 0: 

5870 #@+<< place blank and word on the present line >> 

5871 #@+node:ekr.20110727091744.15084: *5* << place blank and word on the present line >> 

5872 if line: 

5873 # Add the word, preceeded by a blank. 

5874 line = space.join((line, word)) 

5875 else: 

5876 # Just add the word to the start of the line. 

5877 line = word 

5878 #@-<< place blank and word on the present line >> 

5879 else: pass # discard the trailing whitespace. 

5880 else: 

5881 #@+<< place word on a new line >> 

5882 #@+node:ekr.20110727091744.15085: *5* << place word on a new line >> 

5883 # End the previous line. 

5884 if line: 

5885 result.append(line) 

5886 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines 

5887 # Discard the whitespace and put the word on a new line. 

5888 line = word 

5889 # Careful: the word may be longer than pageWidth. 

5890 if len(line) > pageWidth: # DTHEIN 18-JAN-2004: line can equal pagewidth 

5891 result.append(line) 

5892 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines 

5893 line = "" 

5894 #@-<< place word on a new line >> 

5895 if line: 

5896 result.append(line) 

5897 return result 

5898#@+node:ekr.20031218072017.3200: *4* g.get_leading_ws 

5899def get_leading_ws(s: str) -> str: 

5900 """Returns the leading whitespace of 's'.""" 

5901 i = 0 

5902 n = len(s) 

5903 while i < n and s[i] in (' ', '\t'): 

5904 i += 1 

5905 return s[0:i] 

5906#@+node:ekr.20031218072017.3201: *4* g.optimizeLeadingWhitespace 

5907# Optimize leading whitespace in s with the given tab_width. 

5908 

5909def optimizeLeadingWhitespace(line: str, tab_width: int) -> str: 

5910 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width) 

5911 s = g.computeLeadingWhitespace(width, tab_width) + line[i:] 

5912 return s 

5913#@+node:ekr.20040723093558: *4* g.regularizeTrailingNewlines 

5914#@+at The caller should call g.stripBlankLines before calling this routine 

5915# if desired. 

5916# 

5917# This routine does _not_ simply call rstrip(): that would delete all 

5918# trailing whitespace-only lines, and in some cases that would change 

5919# the meaning of program or data. 

5920#@@c 

5921 

5922def regularizeTrailingNewlines(s: str, kind: str) -> None: 

5923 """Kind is 'asis', 'zero' or 'one'.""" 

5924 pass 

5925#@+node:ekr.20091229090857.11698: *4* g.removeBlankLines 

5926def removeBlankLines(s: str) -> str: 

5927 lines = g.splitLines(s) 

5928 lines = [z for z in lines if z.strip()] 

5929 return ''.join(lines) 

5930#@+node:ekr.20091229075924.6235: *4* g.removeLeadingBlankLines 

5931def removeLeadingBlankLines(s: str) -> str: 

5932 lines = g.splitLines(s) 

5933 result = [] 

5934 remove = True 

5935 for line in lines: 

5936 if remove and not line.strip(): 

5937 pass 

5938 else: 

5939 remove = False 

5940 result.append(line) 

5941 return ''.join(result) 

5942#@+node:ekr.20031218072017.3202: *4* g.removeLeadingWhitespace 

5943# Remove whitespace up to first_ws wide in s, given tab_width, the width of a tab. 

5944 

5945def removeLeadingWhitespace(s: str, first_ws: int, tab_width: int) -> str: 

5946 j = 0 

5947 ws = 0 

5948 first_ws = abs(first_ws) 

5949 for ch in s: 

5950 if ws >= first_ws: 

5951 break 

5952 elif ch == ' ': 

5953 j += 1 

5954 ws += 1 

5955 elif ch == '\t': 

5956 j += 1 

5957 ws += (abs(tab_width) - (ws % abs(tab_width))) 

5958 else: 

5959 break 

5960 if j > 0: 

5961 s = s[j:] 

5962 return s 

5963#@+node:ekr.20031218072017.3203: *4* g.removeTrailingWs 

5964# Warning: string.rstrip also removes newlines! 

5965 

5966def removeTrailingWs(s: str) -> str: 

5967 j = len(s) - 1 

5968 while j >= 0 and (s[j] == ' ' or s[j] == '\t'): 

5969 j -= 1 

5970 return s[: j + 1] 

5971#@+node:ekr.20031218072017.3204: *4* g.skip_leading_ws 

5972# Skips leading up to width leading whitespace. 

5973 

5974def skip_leading_ws(s: str, i: int, ws: int, tab_width: int) -> int: 

5975 count = 0 

5976 while count < ws and i < len(s): 

5977 ch = s[i] 

5978 if ch == ' ': 

5979 count += 1 

5980 i += 1 

5981 elif ch == '\t': 

5982 count += (abs(tab_width) - (count % abs(tab_width))) 

5983 i += 1 

5984 else: break 

5985 return i 

5986#@+node:ekr.20031218072017.3205: *4* g.skip_leading_ws_with_indent 

5987def skip_leading_ws_with_indent(s: str, i: int, tab_width: int) -> Tuple[int, int]: 

5988 """Skips leading whitespace and returns (i, indent), 

5989 

5990 - i points after the whitespace 

5991 - indent is the width of the whitespace, assuming tab_width wide tabs.""" 

5992 count = 0 

5993 n = len(s) 

5994 while i < n: 

5995 ch = s[i] 

5996 if ch == ' ': 

5997 count += 1 

5998 i += 1 

5999 elif ch == '\t': 

6000 count += (abs(tab_width) - (count % abs(tab_width))) 

6001 i += 1 

6002 else: break 

6003 return i, count 

6004#@+node:ekr.20040723093558.1: *4* g.stripBlankLines 

6005def stripBlankLines(s: str) -> str: 

6006 lines = g.splitLines(s) 

6007 for i, line in enumerate(lines): 

6008 j = g.skip_ws(line, 0) 

6009 if j >= len(line): 

6010 lines[i] = '' 

6011 elif line[j] == '\n': 

6012 lines[i] = '\n' 

6013 return ''.join(lines) 

6014#@+node:ekr.20031218072017.3108: ** g.Logging & Printing 

6015# g.es and related print to the Log window. 

6016# g.pr prints to the console. 

6017# g.es_print and related print to both the Log window and the console. 

6018#@+node:ekr.20080821073134.2: *3* g.doKeywordArgs 

6019def doKeywordArgs(keys: Dict, d: Dict=None) -> Dict: 

6020 """ 

6021 Return a result dict that is a copy of the keys dict 

6022 with missing items replaced by defaults in d dict. 

6023 """ 

6024 if d is None: 

6025 d = {} 

6026 result = {} 

6027 for key, default_val in d.items(): 

6028 isBool = default_val in (True, False) 

6029 val = keys.get(key) 

6030 if isBool and val in (True, 'True', 'true'): 

6031 result[key] = True 

6032 elif isBool and val in (False, 'False', 'false'): 

6033 result[key] = False 

6034 elif val is None: 

6035 result[key] = default_val 

6036 else: 

6037 result[key] = val 

6038 return result 

6039#@+node:ekr.20031218072017.1474: *3* g.enl, ecnl & ecnls 

6040def ecnl(tabName: str='Log') -> None: 

6041 g.ecnls(1, tabName) 

6042 

6043def ecnls(n: int, tabName: str='Log') -> None: 

6044 log = app.log 

6045 if log and not log.isNull: 

6046 while log.newlines < n: 

6047 g.enl(tabName) 

6048 

6049def enl(tabName: str='Log') -> None: 

6050 log = app.log 

6051 if log and not log.isNull: 

6052 log.newlines += 1 

6053 log.putnl(tabName) 

6054#@+node:ekr.20100914094836.5892: *3* g.error, g.note, g.warning, g.red, g.blue 

6055def blue(*args: Any, **keys: Any) -> None: 

6056 g.es_print(color='blue', *args, **keys) 

6057 

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

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

6060 

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

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

6063 

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

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

6066 

6067def warning(*args: Any, **keys: Any) -> None: 

6068 g.es_print(color='warning', *args, **keys) 

6069#@+node:ekr.20070626132332: *3* g.es 

6070def es(*args: Any, **keys: Any) -> None: 

6071 """Put all non-keyword args to the log pane. 

6072 The first, third, fifth, etc. arg translated by g.translateString. 

6073 Supports color, comma, newline, spaces and tabName keyword arguments. 

6074 """ 

6075 if not app or app.killed: 

6076 return 

6077 if app.gui and app.gui.consoleOnly: 

6078 return 

6079 log = app.log 

6080 # Compute the effective args. 

6081 d = { 

6082 'color': None, 

6083 'commas': False, 

6084 'newline': True, 

6085 'spaces': True, 

6086 'tabName': 'Log', 

6087 'nodeLink': None, 

6088 } 

6089 d = g.doKeywordArgs(keys, d) 

6090 color = d.get('color') 

6091 if color == 'suppress': 

6092 return # New in 4.3. 

6093 color = g.actualColor(color) 

6094 tabName = d.get('tabName') or 'Log' 

6095 newline = d.get('newline') 

6096 s = g.translateArgs(args, d) 

6097 # Do not call g.es, g.es_print, g.pr or g.trace here! 

6098 # sys.__stdout__.write('\n===== g.es: %r\n' % s) 

6099 if app.batchMode: 

6100 if app.log: 

6101 app.log.put(s) 

6102 elif g.unitTesting: 

6103 if log and not log.isNull: 

6104 # This makes the output of unit tests match the output of scripts. 

6105 g.pr(s, newline=newline) 

6106 elif log and app.logInited: 

6107 if newline: 

6108 s += '\n' 

6109 log.put(s, color=color, tabName=tabName, nodeLink=d['nodeLink']) 

6110 # Count the number of *trailing* newlines. 

6111 for ch in s: 

6112 if ch == '\n': 

6113 log.newlines += 1 

6114 else: 

6115 log.newlines = 0 

6116 else: 

6117 app.logWaiting.append((s, color, newline, d),) 

6118 

6119log = es 

6120#@+node:ekr.20060917120951: *3* g.es_dump 

6121def es_dump(s: str, n: int=30, title: str=None) -> None: 

6122 if title: 

6123 g.es_print('', title) 

6124 i = 0 

6125 while i < len(s): 

6126 aList = ''.join([f"{ord(ch):2x} " for ch in s[i : i + n]]) 

6127 g.es_print('', aList) 

6128 i += n 

6129#@+node:ekr.20031218072017.3110: *3* g.es_error & es_print_error 

6130def es_error(*args: Any, **keys: Any) -> None: 

6131 color = keys.get('color') 

6132 if color is None and g.app.config: 

6133 keys['color'] = g.app.config.getColor("log-error-color") or 'red' 

6134 g.es(*args, **keys) 

6135 

6136def es_print_error(*args: Any, **keys: Any) -> None: 

6137 color = keys.get('color') 

6138 if color is None and g.app.config: 

6139 keys['color'] = g.app.config.getColor("log-error-color") or 'red' 

6140 g.es_print(*args, **keys) 

6141#@+node:ekr.20031218072017.3111: *3* g.es_event_exception 

6142def es_event_exception(eventName: str, full: bool=False) -> None: 

6143 g.es("exception handling ", eventName, "event") 

6144 typ, val, tb = sys.exc_info() 

6145 if full: 

6146 errList = traceback.format_exception(typ, val, tb) 

6147 else: 

6148 errList = traceback.format_exception_only(typ, val) 

6149 for i in errList: 

6150 g.es('', i) 

6151 if not g.stdErrIsRedirected(): # 2/16/04 

6152 traceback.print_exc() 

6153#@+node:ekr.20031218072017.3112: *3* g.es_exception 

6154def es_exception(full: bool=True, c: Cmdr=None, color: str="red") -> Tuple[str, int]: 

6155 typ, val, tb = sys.exc_info() 

6156 # val is the second argument to the raise statement. 

6157 if full: 

6158 lines = traceback.format_exception(typ, val, tb) 

6159 else: 

6160 lines = traceback.format_exception_only(typ, val) 

6161 for line in lines: 

6162 g.es_print_error(line, color=color) 

6163 fileName, n = g.getLastTracebackFileAndLineNumber() 

6164 return fileName, n 

6165#@+node:ekr.20061015090538: *3* g.es_exception_type 

6166def es_exception_type(c: Cmdr=None, color: str="red") -> None: 

6167 # exctype is a Exception class object; value is the error message. 

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

6169 g.es_print('', f"{exctype.__name__}, {value}", color=color) # type:ignore 

6170#@+node:ekr.20050707064040: *3* g.es_print 

6171# see: http://www.diveintopython.org/xml_processing/unicode.html 

6172 

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

6174 """ 

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

6176 

6177 The first, third, fifth, etc. arg translated by g.translateString. 

6178 Supports color, comma, newline, spaces and tabName keyword arguments. 

6179 """ 

6180 g.pr(*args, **keys) 

6181 if g.app and not g.unitTesting: 

6182 g.es(*args, **keys) 

6183#@+node:ekr.20111107181638.9741: *3* g.print_exception 

6184def print_exception(full: bool=True, c: Cmdr=None, flush: bool=False, color: str="red") -> Tuple[str, int]: 

6185 """Print exception info about the last exception.""" 

6186 typ, val, tb = sys.exc_info() 

6187 # val is the second argument to the raise statement. 

6188 if full: 

6189 lines = traceback.format_exception(typ, val, tb) 

6190 else: 

6191 lines = traceback.format_exception_only(typ, val) 

6192 print(''.join(lines), flush=flush) 

6193 try: 

6194 fileName, n = g.getLastTracebackFileAndLineNumber() 

6195 return fileName, n 

6196 except Exception: 

6197 return "<no file>", 0 

6198#@+node:ekr.20050707065530: *3* g.es_trace 

6199def es_trace(*args: Any, **keys: Any) -> None: 

6200 if args: 

6201 try: 

6202 s = args[0] 

6203 g.trace(g.toEncodedString(s, 'ascii')) 

6204 except Exception: 

6205 pass 

6206 g.es(*args, **keys) 

6207#@+node:ekr.20040731204831: *3* g.getLastTracebackFileAndLineNumber 

6208def getLastTracebackFileAndLineNumber() -> Tuple[str, int]: 

6209 typ, val, tb = sys.exc_info() 

6210 if typ == SyntaxError: 

6211 # IndentationError is a subclass of SyntaxError. 

6212 return val.filename, val.lineno 

6213 # 

6214 # Data is a list of tuples, one per stack entry. 

6215 # Tupls have the form (filename,lineNumber,functionName,text). 

6216 data = traceback.extract_tb(tb) 

6217 if data: 

6218 item = data[-1] # Get the item at the top of the stack. 

6219 filename, n, functionName, text = item 

6220 return filename, n 

6221 # Should never happen. 

6222 return '<string>', 0 

6223#@+node:ekr.20150621095017.1: *3* g.goto_last_exception 

6224def goto_last_exception(c: Cmdr) -> None: 

6225 """Go to the line given by sys.last_traceback.""" 

6226 typ, val, tb = sys.exc_info() 

6227 if tb: 

6228 file_name, line_number = g.getLastTracebackFileAndLineNumber() 

6229 line_number = max(0, line_number - 1) 

6230 # Convert to zero-based. 

6231 if file_name.endswith('scriptFile.py'): 

6232 # A script. 

6233 c.goToScriptLineNumber(line_number, c.p) 

6234 else: 

6235 for p in c.all_nodes(): 

6236 if p.isAnyAtFileNode() and p.h.endswith(file_name): 

6237 c.goToLineNumber(line_number) # 2021/07/28: fixed by mypy. 

6238 return 

6239 else: 

6240 g.trace('No previous exception') 

6241#@+node:ekr.20100126062623.6240: *3* g.internalError 

6242def internalError(*args: Any) -> None: 

6243 """Report a serious interal error in Leo.""" 

6244 callers = g.callers(20).split(',') 

6245 caller = callers[-1] 

6246 g.error('\nInternal Leo error in', caller) 

6247 g.es_print(*args) 

6248 g.es_print('Called from', ', '.join(callers[:-1])) 

6249 g.es_print('Please report this error to Leo\'s developers', color='red') 

6250#@+node:ekr.20150127060254.5: *3* g.log_to_file 

6251def log_to_file(s: str, fn: str=None) -> None: 

6252 """Write a message to ~/test/leo_log.txt.""" 

6253 if fn is None: 

6254 fn = g.os_path_expanduser('~/test/leo_log.txt') 

6255 if not s.endswith('\n'): 

6256 s = s + '\n' 

6257 try: 

6258 with open(fn, 'a') as f: 

6259 f.write(s) 

6260 except Exception: 

6261 g.es_exception() 

6262#@+node:ekr.20080710101653.1: *3* g.pr 

6263# see: http://www.diveintopython.org/xml_processing/unicode.html 

6264 

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

6266 """ 

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

6268 

6269 The first, third, fifth, etc. arg translated by g.translateString. 

6270 Supports color, comma, newline, spaces and tabName keyword arguments. 

6271 """ 

6272 # Compute the effective args. 

6273 d = {'commas': False, 'newline': True, 'spaces': True} 

6274 d = doKeywordArgs(keys, d) 

6275 newline = d.get('newline') 

6276 stdout = sys.stdout if sys.stdout and g.unitTesting else sys.__stdout__ 

6277 # Unit tests require sys.stdout. 

6278 if not stdout: 

6279 # #541. 

6280 return 

6281 if sys.platform.lower().startswith('win'): 

6282 encoding = 'ascii' # 2011/11/9. 

6283 elif getattr(stdout, 'encoding', None): 

6284 # sys.stdout is a TextIOWrapper with a particular encoding. 

6285 encoding = stdout.encoding 

6286 else: 

6287 encoding = 'utf-8' 

6288 s = translateArgs(args, d) 

6289 # Translates everything to unicode. 

6290 s = g.toUnicode(s, encoding=encoding, reportErrors=False) 

6291 if newline: 

6292 s += '\n' 

6293 # 

6294 # Python's print statement *can* handle unicode, but 

6295 # sitecustomize.py must have sys.setdefaultencoding('utf-8') 

6296 try: 

6297 # #783: print-* commands fail under pythonw. 

6298 stdout.write(s) 

6299 except Exception: 

6300 pass 

6301#@+node:ekr.20060221083356: *3* g.prettyPrintType 

6302def prettyPrintType(obj: Any) -> str: 

6303 if isinstance(obj, str): # type:ignore 

6304 return 'string' 

6305 t: Any = type(obj) 

6306 if t in (types.BuiltinFunctionType, types.FunctionType): 

6307 return 'function' 

6308 if t == types.ModuleType: 

6309 return 'module' 

6310 if t in [types.MethodType, types.BuiltinMethodType]: 

6311 return 'method' 

6312 # Fall back to a hack. 

6313 t = str(type(obj)) # type:ignore 

6314 if t.startswith("<type '"): 

6315 t = t[7:] 

6316 if t.endswith("'>"): 

6317 t = t[:-2] 

6318 return t 

6319#@+node:ekr.20031218072017.3113: *3* g.printBindings 

6320def print_bindings(name: str, window: Any) -> None: 

6321 bindings = window.bind() 

6322 g.pr("\nBindings for", name) 

6323 for b in bindings: 

6324 g.pr(b) 

6325#@+node:ekr.20070510074941: *3* g.printEntireTree 

6326def printEntireTree(c: Cmdr, tag: str='') -> None: 

6327 g.pr('printEntireTree', '=' * 50) 

6328 g.pr('printEntireTree', tag, 'root', c.rootPosition()) 

6329 for p in c.all_positions(): 

6330 g.pr('..' * p.level(), p.v) 

6331#@+node:ekr.20031218072017.3114: *3* g.printGlobals 

6332def printGlobals(message: str=None) -> None: 

6333 # Get the list of globals. 

6334 globs = list(globals()) 

6335 globs.sort() 

6336 # Print the list. 

6337 if message: 

6338 leader = "-" * 10 

6339 g.pr(leader, ' ', message, ' ', leader) 

6340 for name in globs: 

6341 g.pr(name) 

6342#@+node:ekr.20031218072017.3115: *3* g.printLeoModules 

6343def printLeoModules(message: str=None) -> None: 

6344 # Create the list. 

6345 mods = [] 

6346 for name in sys.modules: 

6347 if name and name[0:3] == "leo": 

6348 mods.append(name) 

6349 # Print the list. 

6350 if message: 

6351 leader = "-" * 10 

6352 g.pr(leader, ' ', message, ' ', leader) 

6353 mods.sort() 

6354 for m in mods: 

6355 g.pr(m, newline=False) 

6356 g.pr('') 

6357#@+node:ekr.20041122153823: *3* g.printStack 

6358def printStack() -> None: 

6359 traceback.print_stack() 

6360#@+node:ekr.20031218072017.2317: *3* g.trace 

6361def trace(*args: Any, **keys: Any) -> None: 

6362 """Print a tracing message.""" 

6363 # Don't use g here: in standalone mode g is a NullObject! 

6364 # Compute the effective args. 

6365 d: Dict[str, Any] = {'align': 0, 'before': '', 'newline': True, 'caller_level': 1, 'noname': False} 

6366 d = doKeywordArgs(keys, d) 

6367 newline = d.get('newline') 

6368 align = d.get('align', 0) 

6369 caller_level = d.get('caller_level', 1) 

6370 noname = d.get('noname') 

6371 # Compute the caller name. 

6372 if noname: 

6373 name = '' 

6374 else: 

6375 try: # get the function name from the call stack. 

6376 f1 = sys._getframe(caller_level) # The stack frame, one level up. 

6377 code1 = f1.f_code # The code object 

6378 name = code1.co_name # The code name 

6379 except Exception: 

6380 name = g.shortFileName(__file__) 

6381 if name == '<module>': 

6382 name = g.shortFileName(__file__) 

6383 if name.endswith('.pyc'): 

6384 name = name[:-1] 

6385 # Pad the caller name. 

6386 if align != 0 and len(name) < abs(align): 

6387 pad = ' ' * (abs(align) - len(name)) 

6388 if align > 0: 

6389 name = name + pad 

6390 else: 

6391 name = pad + name 

6392 # Munge *args into s. 

6393 result = [name] if name else [] 

6394 # 

6395 # Put leading newlines into the prefix. 

6396 if isinstance(args, tuple): 

6397 args = list(args) # type:ignore 

6398 if args and isinstance(args[0], str): 

6399 prefix = '' 

6400 while args[0].startswith('\n'): 

6401 prefix += '\n' 

6402 args[0] = args[0][1:] # type:ignore 

6403 else: 

6404 prefix = '' 

6405 for arg in args: 

6406 if isinstance(arg, str): 

6407 pass 

6408 elif isinstance(arg, bytes): 

6409 arg = toUnicode(arg) 

6410 else: 

6411 arg = repr(arg) 

6412 if result: 

6413 result.append(" " + arg) 

6414 else: 

6415 result.append(arg) 

6416 s = d.get('before') + ''.join(result) 

6417 if prefix: 

6418 prefix = prefix[1:] # One less newline. 

6419 pr(prefix) 

6420 pr(s, newline=newline) 

6421#@+node:ekr.20080220111323: *3* g.translateArgs 

6422console_encoding = None 

6423 

6424def translateArgs(args: Iterable[Any], d: Dict[str, Any]) -> str: 

6425 """ 

6426 Return the concatenation of s and all args, with odd args translated. 

6427 """ 

6428 global console_encoding 

6429 if not console_encoding: 

6430 e = sys.getdefaultencoding() 

6431 console_encoding = e if isValidEncoding(e) else 'utf-8' 

6432 # print 'translateArgs',console_encoding 

6433 result: List[str] = [] 

6434 n, spaces = 0, d.get('spaces') 

6435 for arg in args: 

6436 n += 1 

6437 # First, convert to unicode. 

6438 if isinstance(arg, str): 

6439 arg = toUnicode(arg, console_encoding) 

6440 # Now translate. 

6441 if not isinstance(arg, str): 

6442 arg = repr(arg) 

6443 elif (n % 2) == 1: 

6444 arg = translateString(arg) 

6445 else: 

6446 pass # The arg is an untranslated string. 

6447 if arg: 

6448 if result and spaces: 

6449 result.append(' ') 

6450 result.append(arg) 

6451 return ''.join(result) 

6452#@+node:ekr.20060810095921: *3* g.translateString & tr 

6453def translateString(s: str) -> str: 

6454 """Return the translated text of s.""" 

6455 # pylint: disable=undefined-loop-variable 

6456 # looks like a pylint bug 

6457 upper = app and getattr(app, 'translateToUpperCase', None) 

6458 if not isinstance(s, str): 

6459 s = str(s, 'utf-8') 

6460 if upper: 

6461 s = s.upper() 

6462 else: 

6463 s = gettext.gettext(s) 

6464 return s 

6465 

6466tr = translateString 

6467#@+node:EKR.20040612114220: ** g.Miscellaneous 

6468#@+node:ekr.20120928142052.10116: *3* g.actualColor 

6469def actualColor(color: str) -> str: 

6470 """Return the actual color corresponding to the requested color.""" 

6471 c = g.app.log and g.app.log.c 

6472 # Careful: c.config may not yet exist. 

6473 if not c or not c.config: 

6474 return color 

6475 # Don't change absolute colors. 

6476 if color and color.startswith('#'): 

6477 return color 

6478 # #788: Translate colors to theme-defined colors. 

6479 if color is None: 

6480 # Prefer text_foreground_color' 

6481 color2 = c.config.getColor('log-text-foreground-color') 

6482 if color2: 

6483 return color2 

6484 # Fall back to log_black_color. 

6485 color2 = c.config.getColor('log-black-color') 

6486 return color2 or 'black' 

6487 if color == 'black': 

6488 # Prefer log_black_color. 

6489 color2 = c.config.getColor('log-black-color') 

6490 if color2: 

6491 return color2 

6492 # Fall back to log_text_foreground_color. 

6493 color2 = c.config.getColor('log-text-foreground-color') 

6494 return color2 or 'black' 

6495 color2 = c.config.getColor(f"log_{color}_color") 

6496 return color2 or color 

6497#@+node:ekr.20060921100435: *3* g.CheckVersion & helpers 

6498# Simplified version by EKR: stringCompare not used. 

6499 

6500def CheckVersion( 

6501 s1: str, 

6502 s2: str, 

6503 condition: str=">=", 

6504 stringCompare: bool=None, 

6505 delimiter: str='.', 

6506 trace: bool=False, 

6507) -> bool: 

6508 # CheckVersion is called early in the startup process. 

6509 vals1 = [g.CheckVersionToInt(s) for s in s1.split(delimiter)] 

6510 n1 = len(vals1) 

6511 vals2 = [g.CheckVersionToInt(s) for s in s2.split(delimiter)] 

6512 n2 = len(vals2) 

6513 n = max(n1, n2) 

6514 if n1 < n: 

6515 vals1.extend([0 for i in range(n - n1)]) 

6516 if n2 < n: 

6517 vals2.extend([0 for i in range(n - n2)]) 

6518 for cond, val in ( 

6519 ('==', vals1 == vals2), ('!=', vals1 != vals2), 

6520 ('<', vals1 < vals2), ('<=', vals1 <= vals2), 

6521 ('>', vals1 > vals2), ('>=', vals1 >= vals2), 

6522 ): 

6523 if condition == cond: 

6524 result = val 

6525 break 

6526 else: 

6527 raise EnvironmentError( 

6528 "condition must be one of '>=', '>', '==', '!=', '<', or '<='.") 

6529 return result 

6530#@+node:ekr.20070120123930: *4* g.CheckVersionToInt 

6531def CheckVersionToInt(s: str) -> int: 

6532 try: 

6533 return int(s) 

6534 except ValueError: 

6535 aList = [] 

6536 for ch in s: 

6537 if ch.isdigit(): 

6538 aList.append(ch) 

6539 else: 

6540 break 

6541 if aList: 

6542 s = ''.join(aList) 

6543 return int(s) 

6544 return 0 

6545#@+node:ekr.20111103205308.9657: *3* g.cls 

6546@command('cls') 

6547def cls(event: Any=None) -> None: 

6548 """Clear the screen.""" 

6549 if sys.platform.lower().startswith('win'): 

6550 os.system('cls') 

6551#@+node:ekr.20131114124839.16665: *3* g.createScratchCommander 

6552def createScratchCommander(fileName: str=None) -> None: 

6553 c = g.app.newCommander(fileName) 

6554 frame = c.frame 

6555 frame.createFirstTreeNode() 

6556 assert c.rootPosition() 

6557 frame.setInitialWindowGeometry() 

6558 frame.resizePanesToRatio(frame.ratio, frame.secondary_ratio) 

6559#@+node:ekr.20031218072017.3126: *3* g.funcToMethod (Python Cookbook) 

6560def funcToMethod(f: Any, theClass: Any, name: str=None) -> None: 

6561 """ 

6562 From the Python Cookbook... 

6563 

6564 The following method allows you to add a function as a method of 

6565 any class. That is, it converts the function to a method of the 

6566 class. The method just added is available instantly to all 

6567 existing instances of the class, and to all instances created in 

6568 the future. 

6569 

6570 The function's first argument should be self. 

6571 

6572 The newly created method has the same name as the function unless 

6573 the optional name argument is supplied, in which case that name is 

6574 used as the method name. 

6575 """ 

6576 setattr(theClass, name or f.__name__, f) 

6577#@+node:ekr.20060913090832.1: *3* g.init_zodb 

6578init_zodb_import_failed = False 

6579init_zodb_failed: Dict[str, bool] = {} # Keys are paths, values are True. 

6580init_zodb_db: Dict[str, Any] = {} # Keys are paths, values are ZODB.DB instances. 

6581 

6582def init_zodb(pathToZodbStorage: str, verbose: bool=True) -> Any: 

6583 """ 

6584 Return an ZODB.DB instance from the given path. 

6585 return None on any error. 

6586 """ 

6587 global init_zodb_db, init_zodb_failed, init_zodb_import_failed 

6588 db = init_zodb_db.get(pathToZodbStorage) 

6589 if db: 

6590 return db 

6591 if init_zodb_import_failed: 

6592 return None 

6593 failed = init_zodb_failed.get(pathToZodbStorage) 

6594 if failed: 

6595 return None 

6596 try: 

6597 import ZODB # type:ignore 

6598 except ImportError: 

6599 if verbose: 

6600 g.es('g.init_zodb: can not import ZODB') 

6601 g.es_exception() 

6602 init_zodb_import_failed = True 

6603 return None 

6604 try: 

6605 storage = ZODB.FileStorage.FileStorage(pathToZodbStorage) 

6606 init_zodb_db[pathToZodbStorage] = db = ZODB.DB(storage) 

6607 return db 

6608 except Exception: 

6609 if verbose: 

6610 g.es('g.init_zodb: exception creating ZODB.DB instance') 

6611 g.es_exception() 

6612 init_zodb_failed[pathToZodbStorage] = True 

6613 return None 

6614#@+node:ekr.20170206080908.1: *3* g.input_ 

6615def input_(message: str='', c: Cmdr=None) -> str: 

6616 """ 

6617 Safely execute python's input statement. 

6618 

6619 c.executeScriptHelper binds 'input' to be a wrapper that calls g.input_ 

6620 with c and handler bound properly. 

6621 """ 

6622 if app.gui.isNullGui: 

6623 return '' 

6624 # Prompt for input from the console, assuming there is one. 

6625 # pylint: disable=no-member 

6626 from leo.core.leoQt import QtCore 

6627 QtCore.pyqtRemoveInputHook() 

6628 return input(message) 

6629#@+node:ekr.20110609125359.16493: *3* g.isMacOS 

6630def isMacOS() -> bool: 

6631 return sys.platform == 'darwin' 

6632#@+node:ekr.20181027133311.1: *3* g.issueSecurityWarning 

6633def issueSecurityWarning(setting: str) -> None: 

6634 g.es('Security warning! Ignoring...', color='red') 

6635 g.es(setting, color='red') 

6636 g.es('This setting can be set only in') 

6637 g.es('leoSettings.leo or myLeoSettings.leo') 

6638#@+node:ekr.20031218072017.3144: *3* g.makeDict (Python Cookbook) 

6639# From the Python cookbook. 

6640 

6641def makeDict(**keys: Any) -> Dict: 

6642 """Returns a Python dictionary from using the optional keyword arguments.""" 

6643 return keys 

6644#@+node:ekr.20140528065727.17963: *3* g.pep8_class_name 

6645def pep8_class_name(s: str) -> str: 

6646 """Return the proper class name for s.""" 

6647 # Warning: s.capitalize() does not work. 

6648 # It lower cases all but the first letter! 

6649 return ''.join([z[0].upper() + z[1:] for z in s.split('_') if z]) 

6650 

6651if 0: # Testing: 

6652 cls() 

6653 aList = ( 

6654 '_', 

6655 '__', 

6656 '_abc', 

6657 'abc_', 

6658 'abc', 

6659 'abc_xyz', 

6660 'AbcPdQ', 

6661 ) 

6662 for s in aList: 

6663 print(pep8_class_name(s)) 

6664#@+node:ekr.20160417174224.1: *3* g.plural 

6665def plural(obj: Any) -> str: 

6666 """Return "s" or "" depending on n.""" 

6667 if isinstance(obj, (list, tuple, str)): 

6668 n = len(obj) 

6669 else: 

6670 n = obj 

6671 return '' if n == 1 else 's' 

6672#@+node:ekr.20160331194701.1: *3* g.truncate 

6673def truncate(s: str, n: int) -> str: 

6674 """Return s truncated to n characters.""" 

6675 if len(s) <= n: 

6676 return s 

6677 # Fail: weird ws. 

6678 s2 = s[: n - 3] + f"...({len(s)})" 

6679 if s.endswith('\n'): 

6680 return s2 + '\n' 

6681 return s2 

6682#@+node:ekr.20031218072017.3150: *3* g.windows 

6683def windows() -> Optional[List]: 

6684 return app and app.windowList 

6685#@+node:ekr.20031218072017.2145: ** g.os_path_ Wrappers 

6686#@+at Note: all these methods return Unicode strings. It is up to the user to 

6687# convert to an encoded string as needed, say when opening a file. 

6688#@+node:ekr.20180314120442.1: *3* g.glob_glob 

6689def glob_glob(pattern: str) -> List: 

6690 """Return the regularized glob.glob(pattern)""" 

6691 aList = glob.glob(pattern) 

6692 # os.path.normpath does the *reverse* of what we want. 

6693 if g.isWindows: 

6694 aList = [z.replace('\\', '/') for z in aList] 

6695 return aList 

6696#@+node:ekr.20031218072017.2146: *3* g.os_path_abspath 

6697def os_path_abspath(path: str) -> str: 

6698 """Convert a path to an absolute path.""" 

6699 if not path: 

6700 return '' 

6701 if '\x00' in path: 

6702 g.trace('NULL in', repr(path), g.callers()) 

6703 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6704 path = os.path.abspath(path) 

6705 # os.path.normpath does the *reverse* of what we want. 

6706 if g.isWindows: 

6707 path = path.replace('\\', '/') 

6708 return path 

6709#@+node:ekr.20031218072017.2147: *3* g.os_path_basename 

6710def os_path_basename(path: str) -> str: 

6711 """Return the second half of the pair returned by split(path).""" 

6712 if not path: 

6713 return '' 

6714 path = os.path.basename(path) 

6715 # os.path.normpath does the *reverse* of what we want. 

6716 if g.isWindows: 

6717 path = path.replace('\\', '/') 

6718 return path 

6719#@+node:ekr.20031218072017.2148: *3* g.os_path_dirname 

6720def os_path_dirname(path: str) -> str: 

6721 """Return the first half of the pair returned by split(path).""" 

6722 if not path: 

6723 return '' 

6724 path = os.path.dirname(path) 

6725 # os.path.normpath does the *reverse* of what we want. 

6726 if g.isWindows: 

6727 path = path.replace('\\', '/') 

6728 return path 

6729#@+node:ekr.20031218072017.2149: *3* g.os_path_exists 

6730def os_path_exists(path: str) -> bool: 

6731 """Return True if path exists.""" 

6732 if not path: 

6733 return False 

6734 if '\x00' in path: 

6735 g.trace('NULL in', repr(path), g.callers()) 

6736 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6737 return os.path.exists(path) 

6738#@+node:ekr.20080921060401.13: *3* g.os_path_expanduser 

6739def os_path_expanduser(path: str) -> str: 

6740 """wrap os.path.expanduser""" 

6741 if not path: 

6742 return '' 

6743 result = os.path.normpath(os.path.expanduser(path)) 

6744 # os.path.normpath does the *reverse* of what we want. 

6745 if g.isWindows: 

6746 path = path.replace('\\', '/') 

6747 return result 

6748#@+node:ekr.20080921060401.14: *3* g.os_path_finalize 

6749def os_path_finalize(path: str) -> str: 

6750 """ 

6751 Expand '~', then return os.path.normpath, os.path.abspath of the path. 

6752 There is no corresponding os.path method 

6753 """ 

6754 if '\x00' in path: 

6755 g.trace('NULL in', repr(path), g.callers()) 

6756 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6757 path = os.path.expanduser(path) # #1383. 

6758 path = os.path.abspath(path) 

6759 path = os.path.normpath(path) 

6760 # os.path.normpath does the *reverse* of what we want. 

6761 if g.isWindows: 

6762 path = path.replace('\\', '/') 

6763 # calling os.path.realpath here would cause problems in some situations. 

6764 return path 

6765#@+node:ekr.20140917154740.19483: *3* g.os_path_finalize_join 

6766def os_path_finalize_join(*args: Any, **keys: Any) -> str: 

6767 """ 

6768 Join and finalize. 

6769 

6770 **keys may contain a 'c' kwarg, used by g.os_path_join. 

6771 """ 

6772 path = g.os_path_join(*args, **keys) 

6773 path = g.os_path_finalize(path) 

6774 return path 

6775#@+node:ekr.20031218072017.2150: *3* g.os_path_getmtime 

6776def os_path_getmtime(path: str) -> float: 

6777 """Return the modification time of path.""" 

6778 if not path: 

6779 return 0 

6780 try: 

6781 return os.path.getmtime(path) 

6782 except Exception: 

6783 return 0 

6784#@+node:ekr.20080729142651.2: *3* g.os_path_getsize 

6785def os_path_getsize(path: str) -> int: 

6786 """Return the size of path.""" 

6787 return os.path.getsize(path) if path else 0 

6788#@+node:ekr.20031218072017.2151: *3* g.os_path_isabs 

6789def os_path_isabs(path: str) -> bool: 

6790 """Return True if path is an absolute path.""" 

6791 return os.path.isabs(path) if path else False 

6792#@+node:ekr.20031218072017.2152: *3* g.os_path_isdir 

6793def os_path_isdir(path: str) -> bool: 

6794 """Return True if the path is a directory.""" 

6795 return os.path.isdir(path) if path else False 

6796#@+node:ekr.20031218072017.2153: *3* g.os_path_isfile 

6797def os_path_isfile(path: str) -> bool: 

6798 """Return True if path is a file.""" 

6799 return os.path.isfile(path) if path else False 

6800#@+node:ekr.20031218072017.2154: *3* g.os_path_join 

6801def os_path_join(*args: Any, **keys: Any) -> str: 

6802 """ 

6803 Join paths, like os.path.join, with enhancements: 

6804 

6805 A '!!' arg prepends g.app.loadDir to the list of paths. 

6806 A '.' arg prepends c.openDirectory to the list of paths, 

6807 provided there is a 'c' kwarg. 

6808 """ 

6809 c = keys.get('c') 

6810 uargs = [z for z in args if z] 

6811 if not uargs: 

6812 return '' 

6813 # Note: This is exactly the same convention as used by getBaseDirectory. 

6814 if uargs[0] == '!!': 

6815 uargs[0] = g.app.loadDir 

6816 elif uargs[0] == '.': 

6817 c = keys.get('c') 

6818 if c and c.openDirectory: 

6819 uargs[0] = c.openDirectory 

6820 try: 

6821 path = os.path.join(*uargs) 

6822 except TypeError: 

6823 g.trace(uargs, args, keys, g.callers()) 

6824 raise 

6825 # May not be needed on some Pythons. 

6826 if '\x00' in path: 

6827 g.trace('NULL in', repr(path), g.callers()) 

6828 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6829 # os.path.normpath does the *reverse* of what we want. 

6830 if g.isWindows: 

6831 path = path.replace('\\', '/') 

6832 return path 

6833#@+node:ekr.20031218072017.2156: *3* g.os_path_normcase 

6834def os_path_normcase(path: str) -> str: 

6835 """Normalize the path's case.""" 

6836 if not path: 

6837 return '' 

6838 path = os.path.normcase(path) 

6839 if g.isWindows: 

6840 path = path.replace('\\', '/') 

6841 return path 

6842#@+node:ekr.20031218072017.2157: *3* g.os_path_normpath 

6843def os_path_normpath(path: str) -> str: 

6844 """Normalize the path.""" 

6845 if not path: 

6846 return '' 

6847 path = os.path.normpath(path) 

6848 # os.path.normpath does the *reverse* of what we want. 

6849 if g.isWindows: 

6850 path = path.replace('\\', '/').lower() # #2049: ignore case! 

6851 return path 

6852#@+node:ekr.20180314081254.1: *3* g.os_path_normslashes 

6853def os_path_normslashes(path: str) -> str: 

6854 

6855 # os.path.normpath does the *reverse* of what we want. 

6856 if g.isWindows and path: 

6857 path = path.replace('\\', '/') 

6858 return path 

6859#@+node:ekr.20080605064555.2: *3* g.os_path_realpath 

6860def os_path_realpath(path: str) -> str: 

6861 """Return the canonical path of the specified filename, eliminating any 

6862 symbolic links encountered in the path (if they are supported by the 

6863 operating system). 

6864 """ 

6865 if not path: 

6866 return '' 

6867 path = os.path.realpath(path) 

6868 # os.path.normpath does the *reverse* of what we want. 

6869 if g.isWindows: 

6870 path = path.replace('\\', '/') 

6871 return path 

6872#@+node:ekr.20031218072017.2158: *3* g.os_path_split 

6873def os_path_split(path: str) -> Tuple[str, str]: 

6874 if not path: 

6875 return '', '' 

6876 head, tail = os.path.split(path) 

6877 return head, tail 

6878#@+node:ekr.20031218072017.2159: *3* g.os_path_splitext 

6879def os_path_splitext(path: str) -> Tuple[str, str]: 

6880 

6881 if not path: 

6882 return '', '' 

6883 head, tail = os.path.splitext(path) 

6884 return head, tail 

6885#@+node:ekr.20090829140232.6036: *3* g.os_startfile 

6886def os_startfile(fname: str) -> None: 

6887 #@+others 

6888 #@+node:bob.20170516112250.1: *4* stderr2log() 

6889 def stderr2log(g: Any, ree: Any, fname: str) -> None: 

6890 """ Display stderr output in the Leo-Editor log pane 

6891 

6892 Arguments: 

6893 g: Leo-Editor globals 

6894 ree: Read file descriptor for stderr 

6895 fname: file pathname 

6896 

6897 Returns: 

6898 None 

6899 """ 

6900 

6901 while True: 

6902 emsg = ree.read().decode('utf-8') 

6903 if emsg: 

6904 g.es_print_error(f"xdg-open {fname} caused output to stderr:\n{emsg}") 

6905 else: 

6906 break 

6907 #@+node:bob.20170516112304.1: *4* itPoll() 

6908 def itPoll(fname: str, ree: Any, subPopen: Any, g: Any, ito: Any) -> None: 

6909 """ Poll for subprocess done 

6910 

6911 Arguments: 

6912 fname: File name 

6913 ree: stderr read file descriptor 

6914 subPopen: URL open subprocess object 

6915 g: Leo-Editor globals 

6916 ito: Idle time object for itPoll() 

6917 

6918 Returns: 

6919 None 

6920 """ 

6921 

6922 stderr2log(g, ree, fname) 

6923 rc = subPopen.poll() 

6924 if not rc is None: 

6925 ito.stop() 

6926 ito.destroy_self() 

6927 if rc != 0: 

6928 g.es_print(f"xdg-open {fname} failed with exit code {rc}") 

6929 stderr2log(g, ree, fname) 

6930 ree.close() 

6931 #@-others 

6932 if fname.find('"') > -1: 

6933 quoted_fname = f"'{fname}'" 

6934 else: 

6935 quoted_fname = f'"{fname}"' 

6936 if sys.platform.startswith('win'): 

6937 # pylint: disable=no-member 

6938 os.startfile(quoted_fname) 

6939 # Exists only on Windows. 

6940 elif sys.platform == 'darwin': 

6941 # From Marc-Antoine Parent. 

6942 try: 

6943 # Fix bug 1226358: File URL's are broken on MacOS: 

6944 # use fname, not quoted_fname, as the argument to subprocess.call. 

6945 subprocess.call(['open', fname]) 

6946 except OSError: 

6947 pass # There may be a spurious "Interrupted system call" 

6948 except ImportError: 

6949 os.system(f"open {quoted_fname}") 

6950 else: 

6951 try: 

6952 ree = None 

6953 wre = tempfile.NamedTemporaryFile() 

6954 ree = io.open(wre.name, 'rb', buffering=0) 

6955 except IOError: 

6956 g.trace(f"error opening temp file for {fname!r}") 

6957 if ree: 

6958 ree.close() 

6959 return 

6960 try: 

6961 subPopen = subprocess.Popen(['xdg-open', fname], stderr=wre, shell=False) 

6962 except Exception: 

6963 g.es_print(f"error opening {fname!r}") 

6964 g.es_exception() 

6965 try: 

6966 itoPoll = g.IdleTime( 

6967 (lambda ito: itPoll(fname, ree, subPopen, g, ito)), 

6968 delay=1000, 

6969 ) 

6970 itoPoll.start() 

6971 # Let the Leo-Editor process run 

6972 # so that Leo-Editor is usable while the file is open. 

6973 except Exception: 

6974 g.es_exception(f"exception executing g.startfile for {fname!r}") 

6975#@+node:ekr.20111115155710.9859: ** g.Parsing & Tokenizing 

6976#@+node:ekr.20031218072017.822: *3* g.createTopologyList 

6977def createTopologyList(c: Cmdr, root: Pos=None, useHeadlines: bool=False) -> List: 

6978 """Creates a list describing a node and all its descendents""" 

6979 if not root: 

6980 root = c.rootPosition() 

6981 v = root 

6982 if useHeadlines: 

6983 aList = [(v.numberOfChildren(), v.headString()),] # type: ignore 

6984 else: 

6985 aList = [v.numberOfChildren()] # type: ignore 

6986 child = v.firstChild() 

6987 while child: 

6988 aList.append(g.createTopologyList(c, child, useHeadlines)) # type: ignore 

6989 child = child.next() 

6990 return aList 

6991#@+node:ekr.20111017204736.15898: *3* g.getDocString 

6992def getDocString(s: str) -> str: 

6993 """Return the text of the first docstring found in s.""" 

6994 tags = ('"""', "'''") 

6995 tag1, tag2 = tags 

6996 i1, i2 = s.find(tag1), s.find(tag2) 

6997 if i1 == -1 and i2 == -1: 

6998 return '' 

6999 if i1 > -1 and i2 > -1: 

7000 i = min(i1, i2) 

7001 else: 

7002 i = max(i1, i2) 

7003 tag = s[i : i + 3] 

7004 assert tag in tags 

7005 j = s.find(tag, i + 3) 

7006 if j > -1: 

7007 return s[i + 3 : j] 

7008 return '' 

7009#@+node:ekr.20111017211256.15905: *3* g.getDocStringForFunction 

7010def getDocStringForFunction(func: Any) -> str: 

7011 """Return the docstring for a function that creates a Leo command.""" 

7012 

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

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

7015 

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

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

7018 return defaults[i] 

7019 

7020 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252 

7021 # Minibuffer commands created by mod_scripting.py have no docstrings. 

7022 # Do special cases first. 

7023 

7024 s = '' 

7025 if name(func) == 'minibufferCallback': 

7026 func = get_defaults(func, 0) 

7027 if hasattr(func, 'func.__doc__') and func.__doc__.strip(): 

7028 s = func.__doc__ 

7029 if not s and name(func) == 'commonCommandCallback': 

7030 script = get_defaults(func, 1) 

7031 s = g.getDocString(script) 

7032 # Do a text scan for the function. 

7033 # Now the general cases. Prefer __doc__ to docstring() 

7034 if not s and hasattr(func, '__doc__'): 

7035 s = func.__doc__ 

7036 if not s and hasattr(func, 'docstring'): 

7037 s = func.docstring 

7038 return s 

7039#@+node:ekr.20111115155710.9814: *3* g.python_tokenize (not used) 

7040def python_tokenize(s: str) -> List: 

7041 """ 

7042 Tokenize string s and return a list of tokens (kind, value, line_number) 

7043 

7044 where kind is in ('comment,'id','nl','other','string','ws'). 

7045 """ 

7046 result: List[Tuple[str, str, int]] = [] 

7047 i, line_number = 0, 0 

7048 while i < len(s): 

7049 progress = j = i 

7050 ch = s[i] 

7051 if ch == '\n': 

7052 kind, i = 'nl', i + 1 

7053 elif ch in ' \t': 

7054 kind = 'ws' 

7055 while i < len(s) and s[i] in ' \t': 

7056 i += 1 

7057 elif ch == '#': 

7058 kind, i = 'comment', g.skip_to_end_of_line(s, i) 

7059 elif ch in '"\'': 

7060 kind, i = 'string', g.skip_python_string(s, i) 

7061 elif ch == '_' or ch.isalpha(): 

7062 kind, i = 'id', g.skip_id(s, i) 

7063 else: 

7064 kind, i = 'other', i + 1 

7065 assert progress < i and j == progress 

7066 val = s[j:i] 

7067 assert val 

7068 line_number += val.count('\n') # A comment. 

7069 result.append((kind, val, line_number),) 

7070 return result 

7071#@+node:ekr.20040327103735.2: ** g.Scripting 

7072#@+node:ekr.20161223090721.1: *3* g.exec_file 

7073def exec_file(path: str, d: Dict[str, str], script: str=None) -> None: 

7074 """Simulate python's execfile statement for python 3.""" 

7075 if script is None: 

7076 with open(path) as f: 

7077 script = f.read() 

7078 exec(compile(script, path, 'exec'), d) 

7079#@+node:ekr.20131016032805.16721: *3* g.execute_shell_commands 

7080def execute_shell_commands(commands: Any, trace: bool=False) -> None: 

7081 """ 

7082 Execute each shell command in a separate process. 

7083 Wait for each command to complete, except those starting with '&' 

7084 """ 

7085 if isinstance(commands, str): 

7086 commands = [commands] 

7087 for command in commands: 

7088 wait = not command.startswith('&') 

7089 if trace: 

7090 g.trace(command) 

7091 if command.startswith('&'): 

7092 command = command[1:].strip() 

7093 proc = subprocess.Popen(command, shell=True) 

7094 if wait: 

7095 proc.communicate() 

7096 else: 

7097 if trace: 

7098 print('Start:', proc) 

7099 # #1489: call proc.poll at idle time. 

7100 

7101 def proc_poller(timer: Any, proc: Any=proc) -> None: 

7102 val = proc.poll() 

7103 if val is not None: 

7104 # This trace can be disruptive. 

7105 if trace: 

7106 print(' End:', proc, val) 

7107 timer.stop() 

7108 

7109 g.IdleTime(proc_poller, delay=0).start() 

7110#@+node:ekr.20180217113719.1: *3* g.execute_shell_commands_with_options & helpers 

7111def execute_shell_commands_with_options( 

7112 base_dir: str=None, 

7113 c: Cmdr=None, 

7114 command_setting: str=None, 

7115 commands: List=None, 

7116 path_setting: str=None, 

7117 trace: bool=False, 

7118 warning: str=None, 

7119) -> None: 

7120 """ 

7121 A helper for prototype commands or any other code that 

7122 runs programs in a separate process. 

7123 

7124 base_dir: Base directory to use if no config path given. 

7125 commands: A list of commands, for g.execute_shell_commands. 

7126 commands_setting: Name of @data setting for commands. 

7127 path_setting: Name of @string setting for the base directory. 

7128 warning: A warning to be printed before executing the commands. 

7129 """ 

7130 base_dir = g.computeBaseDir(c, base_dir, path_setting, trace) 

7131 if not base_dir: 

7132 return 

7133 commands = g.computeCommands(c, commands, command_setting, trace) 

7134 if not commands: 

7135 return 

7136 if warning: 

7137 g.es_print(warning) 

7138 os.chdir(base_dir) # Can't do this in the commands list. 

7139 g.execute_shell_commands(commands) 

7140#@+node:ekr.20180217152624.1: *4* g.computeBaseDir 

7141def computeBaseDir(c: Cmdr, base_dir: str, path_setting: str, trace: bool=False) -> Optional[str]: 

7142 """ 

7143 Compute a base_directory. 

7144 If given, @string path_setting takes precedence. 

7145 """ 

7146 # Prefer the path setting to the base_dir argument. 

7147 if path_setting: 

7148 if not c: 

7149 g.es_print('@string path_setting requires valid c arg') 

7150 return None 

7151 # It's not an error for the setting to be empty. 

7152 base_dir2 = c.config.getString(path_setting) 

7153 if base_dir2: 

7154 base_dir2 = base_dir2.replace('\\', '/') 

7155 if g.os_path_exists(base_dir2): 

7156 return base_dir2 

7157 g.es_print(f"@string {path_setting} not found: {base_dir2!r}") 

7158 return None 

7159 # Fall back to given base_dir. 

7160 if base_dir: 

7161 base_dir = base_dir.replace('\\', '/') 

7162 if g.os_path_exists(base_dir): 

7163 return base_dir 

7164 g.es_print(f"base_dir not found: {base_dir!r}") 

7165 return None 

7166 g.es_print(f"Please use @string {path_setting}") 

7167 return None 

7168#@+node:ekr.20180217153459.1: *4* g.computeCommands 

7169def computeCommands(c: Cmdr, commands: List[str], command_setting: str, trace: bool=False) -> List[str]: 

7170 """ 

7171 Get the list of commands. 

7172 If given, @data command_setting takes precedence. 

7173 """ 

7174 if not commands and not command_setting: 

7175 g.es_print('Please use commands, command_setting or both') 

7176 return [] 

7177 # Prefer the setting to the static commands. 

7178 if command_setting: 

7179 if c: 

7180 aList = c.config.getData(command_setting) 

7181 # It's not an error for the setting to be empty. 

7182 # Fall back to the commands. 

7183 return aList or commands 

7184 g.es_print('@data command_setting requires valid c arg') 

7185 return [] 

7186 return commands 

7187#@+node:ekr.20050503112513.7: *3* g.executeFile 

7188def executeFile(filename: str, options: str='') -> None: 

7189 if not os.access(filename, os.R_OK): 

7190 return 

7191 fdir, fname = g.os_path_split(filename) 

7192 # New in Leo 4.10: alway use subprocess. 

7193 

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

7195 

7196 p = subprocess.Popen(cmdlst, cwd=fdir, 

7197 universal_newlines=True, 

7198 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 

7199 stdo, stde = p.communicate() 

7200 return p.wait(), stdo, stde 

7201 

7202 rc, so, se = subprocess_wrapper(f"{sys.executable} {fname} {options}") 

7203 if rc: 

7204 g.pr('return code', rc) 

7205 g.pr(so, se) 

7206#@+node:ekr.20040321065415: *3* g.find*Node* 

7207#@+others 

7208#@+node:ekr.20210303123423.3: *4* findNodeAnywhere 

7209def findNodeAnywhere(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]: 

7210 h = headline.strip() 

7211 for p in c.all_unique_positions(copy=False): 

7212 if p.h.strip() == h: 

7213 return p.copy() 

7214 if not exact: 

7215 for p in c.all_unique_positions(copy=False): 

7216 if p.h.strip().startswith(h): 

7217 return p.copy() 

7218 return None 

7219#@+node:ekr.20210303123525.1: *4* findNodeByPath 

7220def findNodeByPath(c: Cmdr, path: str) -> Optional[Pos]: 

7221 """Return the first @<file> node in Cmdr c whose path is given.""" 

7222 if not os.path.isabs(path): # #2049. Only absolute paths could possibly work. 

7223 g.trace(f"path not absolute: {path}") 

7224 return None 

7225 path = g.os_path_normpath(path) # #2049. Do *not* use os.path.normpath. 

7226 for p in c.all_positions(): 

7227 if p.isAnyAtFileNode(): 

7228 if path == g.os_path_normpath(g.fullPath(c, p)): # #2049. Do *not* use os.path.normpath. 

7229 return p 

7230 return None 

7231#@+node:ekr.20210303123423.1: *4* findNodeInChildren 

7232def findNodeInChildren(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]: 

7233 """Search for a node in v's tree matching the given headline.""" 

7234 p1 = p.copy() 

7235 h = headline.strip() 

7236 for p in p1.children(): 

7237 if p.h.strip() == h: 

7238 return p.copy() 

7239 if not exact: 

7240 for p in p1.children(): 

7241 if p.h.strip().startswith(h): 

7242 return p.copy() 

7243 return None 

7244#@+node:ekr.20210303123423.2: *4* findNodeInTree 

7245def findNodeInTree(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]: 

7246 """Search for a node in v's tree matching the given headline.""" 

7247 h = headline.strip() 

7248 p1 = p.copy() 

7249 for p in p1.subtree(): 

7250 if p.h.strip() == h: 

7251 return p.copy() 

7252 if not exact: 

7253 for p in p1.subtree(): 

7254 if p.h.strip().startswith(h): 

7255 return p.copy() 

7256 return None 

7257#@+node:ekr.20210303123423.4: *4* findTopLevelNode 

7258def findTopLevelNode(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]: 

7259 h = headline.strip() 

7260 for p in c.rootPosition().self_and_siblings(copy=False): 

7261 if p.h.strip() == h: 

7262 return p.copy() 

7263 if not exact: 

7264 for p in c.rootPosition().self_and_siblings(copy=False): 

7265 if p.h.strip().startswith(h): 

7266 return p.copy() 

7267 return None 

7268#@-others 

7269#@+node:EKR.20040614071102.1: *3* g.getScript & helpers 

7270def getScript( 

7271 c: Cmdr, 

7272 p: Pos, 

7273 useSelectedText: bool=True, 

7274 forcePythonSentinels: bool=True, 

7275 useSentinels: bool=True, 

7276) -> str: 

7277 """ 

7278 Return the expansion of the selected text of node p. 

7279 Return the expansion of all of node p's body text if 

7280 p is not the current node or if there is no text selection. 

7281 """ 

7282 w = c.frame.body.wrapper 

7283 if not p: 

7284 p = c.p 

7285 try: 

7286 if g.app.inBridge: 

7287 s = p.b 

7288 elif w and p == c.p and useSelectedText and w.hasSelection(): 

7289 s = w.getSelectedText() 

7290 else: 

7291 s = p.b 

7292 # Remove extra leading whitespace so the user may execute indented code. 

7293 s = textwrap.dedent(s) 

7294 s = g.extractExecutableString(c, p, s) 

7295 script = g.composeScript(c, p, s, 

7296 forcePythonSentinels=forcePythonSentinels, 

7297 useSentinels=useSentinels) 

7298 except Exception: 

7299 g.es_print("unexpected exception in g.getScript") 

7300 g.es_exception() 

7301 script = '' 

7302 return script 

7303#@+node:ekr.20170228082641.1: *4* g.composeScript 

7304def composeScript( 

7305 c: Cmdr, 

7306 p: Pos, 

7307 s: str, 

7308 forcePythonSentinels: bool=True, 

7309 useSentinels: bool=True, 

7310) -> str: 

7311 """Compose a script from p.b.""" 

7312 # This causes too many special cases. 

7313 # if not g.unitTesting and forceEncoding: 

7314 # aList = g.get_directives_dict_list(p) 

7315 # encoding = scanAtEncodingDirectives(aList) or 'utf-8' 

7316 # s = g.insertCodingLine(encoding,s) 

7317 if not s.strip(): 

7318 return '' 

7319 at = c.atFileCommands # type:ignore 

7320 old_in_script = g.app.inScript 

7321 try: 

7322 # #1297: set inScript flags. 

7323 g.app.inScript = g.inScript = True 

7324 g.app.scriptDict["script1"] = s 

7325 # Important: converts unicode to utf-8 encoded strings. 

7326 script = at.stringToString(p.copy(), s, 

7327 forcePythonSentinels=forcePythonSentinels, 

7328 sentinels=useSentinels) 

7329 script = script.replace("\r\n", "\n") # Use brute force. 

7330 # Important, the script is an **encoded string**, not a unicode string. 

7331 g.app.scriptDict["script2"] = script 

7332 finally: 

7333 g.app.inScript = g.inScript = old_in_script 

7334 return script 

7335#@+node:ekr.20170123074946.1: *4* g.extractExecutableString 

7336def extractExecutableString(c: Cmdr, p: Pos, s: str) -> str: 

7337 """ 

7338 Return all lines for the given @language directive. 

7339 

7340 Ignore all lines under control of any other @language directive. 

7341 """ 

7342 # 

7343 # Rewritten to fix #1071. 

7344 if g.unitTesting: 

7345 return s # Regretable, but necessary. 

7346 # 

7347 # Return s if no @language in effect. Should never happen. 

7348 language = g.scanForAtLanguage(c, p) 

7349 if not language: 

7350 return s 

7351 # 

7352 # Return s if @language is unambiguous. 

7353 pattern = r'^@language\s+(\w+)' 

7354 matches = list(re.finditer(pattern, s, re.MULTILINE)) 

7355 if len(matches) < 2: 

7356 return s 

7357 # 

7358 # Scan the lines, extracting only the valid lines. 

7359 extracting, result = False, [] 

7360 for i, line in enumerate(g.splitLines(s)): 

7361 m = re.match(pattern, line) 

7362 if m: 

7363 # g.trace(language, m.group(1)) 

7364 extracting = m.group(1) == language 

7365 elif extracting: 

7366 result.append(line) 

7367 return ''.join(result) 

7368#@+node:ekr.20060624085200: *3* g.handleScriptException 

7369def handleScriptException(c: Cmdr, p: Pos, script: str, script1: str) -> None: 

7370 g.warning("exception executing script") 

7371 full = c.config.getBool('show-full-tracebacks-in-scripts') 

7372 fileName, n = g.es_exception(full=full) 

7373 # Careful: this test is no longer guaranteed. 

7374 if p.v.context == c: 

7375 try: 

7376 c.goToScriptLineNumber(n, p) 

7377 #@+<< dump the lines near the error >> 

7378 #@+node:EKR.20040612215018: *4* << dump the lines near the error >> 

7379 if g.os_path_exists(fileName): 

7380 with open(fileName) as f: 

7381 lines = f.readlines() 

7382 else: 

7383 lines = g.splitLines(script) 

7384 s = '-' * 20 

7385 g.es_print('', s) 

7386 # Print surrounding lines. 

7387 i = max(0, n - 2) 

7388 j = min(n + 2, len(lines)) 

7389 while i < j: 

7390 ch = '*' if i == n - 1 else ' ' 

7391 s = f"{ch} line {i+1:d}: {lines[i]}" 

7392 g.es('', s, newline=False) 

7393 i += 1 

7394 #@-<< dump the lines near the error >> 

7395 except Exception: 

7396 g.es_print('Unexpected exception in g.handleScriptException') 

7397 g.es_exception() 

7398#@+node:ekr.20140209065845.16767: *3* g.insertCodingLine 

7399def insertCodingLine(encoding: str, script: str) -> str: 

7400 """ 

7401 Insert a coding line at the start of script s if no such line exists. 

7402 The coding line must start with @first because it will be passed to 

7403 at.stringToString. 

7404 """ 

7405 if script: 

7406 tag = '@first # -*- coding:' 

7407 lines = g.splitLines(script) 

7408 for s in lines: 

7409 if s.startswith(tag): 

7410 break 

7411 else: 

7412 lines.insert(0, f"{tag} {encoding} -*-\n") 

7413 script = ''.join(lines) 

7414 return script 

7415#@+node:ekr.20070524083513: ** g.Unit Tests 

7416#@+node:ekr.20210901071523.1: *3* g.run_coverage_tests 

7417def run_coverage_tests(module: str='', filename: str='') -> None: 

7418 """ 

7419 Run the coverage tests given by the module and filename strings. 

7420 """ 

7421 leo_editor_dir = os.path.join(g.app.loadDir, '..', '..') 

7422 os.chdir(leo_editor_dir) 

7423 prefix = r"python -m pytest --cov-report html --cov-report term-missing --cov " 

7424 command = f"{prefix} {module} {filename}" 

7425 g.execute_shell_commands(command, trace=False) 

7426#@+node:ekr.20200221050038.1: *3* g.run_unit_test_in_separate_process 

7427def run_unit_test_in_separate_process(command: str) -> None: 

7428 """ 

7429 A script to be run from unitTest.leo. 

7430 

7431 Run the unit testing command (say `python -m leo.core.leoAst`) in a separate process. 

7432 """ 

7433 leo_editor_dir = os.path.join(g.app.loadDir, '..', '..') 

7434 os.chdir(leo_editor_dir) 

7435 p = subprocess.Popen( 

7436 shlex.split(command), 

7437 stdout=subprocess.PIPE, 

7438 stderr=subprocess.PIPE, 

7439 shell=sys.platform.startswith('win'), 

7440 ) 

7441 out, err = p.communicate() 

7442 err = g.toUnicode(err) 

7443 out = g.toUnicode(out) 

7444 print('') 

7445 print(command) 

7446 if out.strip(): 

7447 # print('traces...') 

7448 print(out.rstrip()) 

7449 print(err.rstrip()) 

7450 # There may be skipped tests... 

7451 err_lines = g.splitLines(err.rstrip()) 

7452 if not err_lines[-1].startswith('OK'): 

7453 g.trace('Test failed') 

7454 g.printObj(err_lines, tag='err_lines') 

7455 assert False 

7456#@+node:ekr.20210901065224.1: *3* g.run_unit_tests 

7457def run_unit_tests(tests: str=None, verbose: bool=False) -> None: 

7458 """ 

7459 Run the unit tests given by the "tests" string. 

7460 

7461 Run *all* unit tests if "tests" is not given. 

7462 """ 

7463 leo_editor_dir = g.os_path_finalize_join(g.app.loadDir, '..', '..') 

7464 os.chdir(leo_editor_dir) 

7465 verbosity = '-v' if verbose else '' 

7466 command = f"python -m unittest {verbosity} {tests or ''} " 

7467 # pytest reports too many errors. 

7468 # command = f"python -m pytest --pdb {tests or ''}" 

7469 g.execute_shell_commands(command, trace=False) 

7470#@+node:ekr.20120311151914.9916: ** g.Urls & UNLs 

7471unl_regex = re.compile(r'\bunl:.*$') 

7472 

7473kinds = '(file|ftp|gopher|http|https|mailto|news|nntp|prospero|telnet|wais)' 

7474url_regex = re.compile(fr"""{kinds}://[^\s'"]+[\w=/]""") 

7475#@+node:ekr.20120320053907.9776: *3* g.computeFileUrl 

7476def computeFileUrl(fn: str, c: Cmdr=None, p: Pos=None) -> str: 

7477 """ 

7478 Compute finalized url for filename fn. 

7479 """ 

7480 # First, replace special characters (especially %20, by their equivalent). 

7481 url = urllib.parse.unquote(fn) 

7482 # Finalize the path *before* parsing the url. 

7483 i = url.find('~') 

7484 if i > -1: 

7485 # Expand '~'. 

7486 path = url[i:] 

7487 path = g.os_path_expanduser(path) 

7488 # #1338: This is way too dangerous, and a serious security violation. 

7489 # path = c.os_path_expandExpression(path) 

7490 path = g.os_path_finalize(path) 

7491 url = url[:i] + path 

7492 else: 

7493 tag = 'file://' 

7494 tag2 = 'file:///' 

7495 if sys.platform.startswith('win') and url.startswith(tag2): 

7496 path = url[len(tag2) :].lstrip() 

7497 elif url.startswith(tag): 

7498 path = url[len(tag) :].lstrip() 

7499 else: 

7500 path = url 

7501 # #1338: This is way too dangerous, and a serious security violation. 

7502 # path = c.os_path_expandExpression(path) 

7503 # Handle ancestor @path directives. 

7504 if c and c.openDirectory: 

7505 base = c.getNodePath(p) 

7506 path = g.os_path_finalize_join(c.openDirectory, base, path) 

7507 else: 

7508 path = g.os_path_finalize(path) 

7509 url = f"{tag}{path}" 

7510 return url 

7511#@+node:ekr.20190608090856.1: *3* g.es_clickable_link 

7512def es_clickable_link(c: Cmdr, p: Pos, line_number: int, message: str) -> None: 

7513 """ 

7514 Write a clickable message to the given line number of p.b. 

7515 

7516 Negative line numbers indicate global lines. 

7517 

7518 """ 

7519 unl = p.get_UNL() 

7520 c.frame.log.put(message.strip() + '\n', nodeLink=f"{unl}::{line_number}") 

7521#@+node:tbrown.20140311095634.15188: *3* g.findUNL & helpers 

7522def findUNL(unlList1: List[str], c: Cmdr) -> Optional[Pos]: 

7523 """ 

7524 Find and move to the unl given by the unlList in the commander c. 

7525 Return the found position, or None. 

7526 """ 

7527 # Define the unl patterns. 

7528 old_pat = re.compile(r'^(.*):(\d+),?(\d+)?,?([-\d]+)?,?(\d+)?$') # ':' is the separator. 

7529 new_pat = re.compile(r'^(.*?)(::)([-\d]+)?$') # '::' is the separator. 

7530 

7531 #@+others # Define helper functions 

7532 #@+node:ekr.20220213142925.1: *4* function: convert_unl_list 

7533 def convert_unl_list(aList: List[str]) -> List[str]: 

7534 """ 

7535 Convert old-style UNLs to new UNLs, retaining line numbers if possible. 

7536 """ 

7537 result = [] 

7538 for s in aList: 

7539 # Try to get the line number. 

7540 for m, line_group in ( 

7541 (old_pat.match(s), 4), 

7542 (new_pat.match(s), 3), 

7543 ): 

7544 if m: 

7545 try: 

7546 n = int(m.group(line_group)) 

7547 result.append(f"{m.group(1)}::{n}") 

7548 continue 

7549 except Exception: 

7550 pass 

7551 # Finally, just add the whole UNL. 

7552 result.append(s) 

7553 return result 

7554 #@+node:ekr.20220213142735.1: *4* function: full_match 

7555 def full_match(p: Pos) -> bool: 

7556 """Return True if the headlines of p and all p's parents match unlList.""" 

7557 # Careful: make copies. 

7558 aList, p1 = unlList[:], p.copy() 

7559 while aList and p1: 

7560 m = new_pat.match(aList[-1]) 

7561 if m and m.group(1) != p1.h: 

7562 return False 

7563 if not m and aList[-1] != p1.h: 

7564 return False 

7565 aList.pop() 

7566 p1.moveToParent() 

7567 return not aList 

7568 #@-others 

7569 

7570 unlList = convert_unl_list(unlList1) 

7571 if not unlList: 

7572 return None 

7573 # Find all target headlines. 

7574 targets = [] 

7575 m = new_pat.match(unlList[-1]) 

7576 target = m and m.group(1) or unlList[-1] 

7577 targets.append(target) 

7578 targets.extend(unlList[:-1]) 

7579 # Find all target positions. Prefer later positions. 

7580 positions = list(reversed(list(z for z in c.all_positions() if z.h in targets))) 

7581 while unlList: 

7582 for p in positions: 

7583 p1 = p.copy() 

7584 if full_match(p): 

7585 assert p == p1, (p, p1) 

7586 n = 0 # The default line number. 

7587 # Parse the last target. 

7588 m = new_pat.match(unlList[-1]) 

7589 if m: 

7590 line = m.group(3) 

7591 try: 

7592 n = int(line) 

7593 except(TypeError, ValueError): 

7594 g.trace('bad line number', line) 

7595 if n == 0: 

7596 c.redraw(p) 

7597 elif n < 0: 

7598 p, offset, ok = c.gotoCommands.find_file_line(-n, p) # Calls c.redraw(). 

7599 return p if ok else None 

7600 elif n > 0: 

7601 insert_point = sum(len(i) + 1 for i in p.b.split('\n')[: n - 1]) 

7602 c.redraw(p) 

7603 c.frame.body.wrapper.setInsertPoint(insert_point) 

7604 c.frame.bringToFront() 

7605 c.bodyWantsFocusNow() 

7606 return p 

7607 # Not found. Pop the first parent from unlList. 

7608 unlList.pop(0) 

7609 return None 

7610#@+node:ekr.20120311151914.9917: *3* g.getUrlFromNode 

7611def getUrlFromNode(p: Pos) -> Optional[str]: 

7612 """ 

7613 Get an url from node p: 

7614 1. Use the headline if it contains a valid url. 

7615 2. Otherwise, look *only* at the first line of the body. 

7616 """ 

7617 if not p: 

7618 return None 

7619 c = p.v.context 

7620 assert c 

7621 table = [p.h, g.splitLines(p.b)[0] if p.b else ''] 

7622 table = [s[4:] if g.match_word(s, 0, '@url') else s for s in table] 

7623 table = [s.strip() for s in table if s.strip()] 

7624 # First, check for url's with an explicit scheme. 

7625 for s in table: 

7626 if g.isValidUrl(s): 

7627 return s 

7628 # Next check for existing file and add a file:// scheme. 

7629 for s in table: 

7630 tag = 'file://' 

7631 url = computeFileUrl(s, c=c, p=p) 

7632 if url.startswith(tag): 

7633 fn = url[len(tag) :].lstrip() 

7634 fn = fn.split('#', 1)[0] 

7635 if g.os_path_isfile(fn): 

7636 # Return the *original* url, with a file:// scheme. 

7637 # g.handleUrl will call computeFileUrl again. 

7638 return 'file://' + s 

7639 # Finally, check for local url's. 

7640 for s in table: 

7641 if s.startswith("#"): 

7642 return s 

7643 return None 

7644#@+node:ekr.20170221063527.1: *3* g.handleUnl 

7645def handleUnl(unl: str, c: Cmdr) -> Any: 

7646 """ 

7647 Handle a Leo UNL. This must *never* open a browser. 

7648 

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

7650  

7651 Redraw the commander if the UNL is found. 

7652 """ 

7653 if not unl: 

7654 return None 

7655 unll = unl.lower() 

7656 if unll.startswith('unl://'): 

7657 unl = unl[6:] 

7658 elif unll.startswith('file://'): 

7659 unl = unl[7:] 

7660 unl = unl.strip() 

7661 if not unl: 

7662 return None 

7663 unl = g.unquoteUrl(unl) 

7664 # Compute path and unl. 

7665 if '#' not in unl and '-->' not in unl: 

7666 # The path is the entire unl. 

7667 path, unl = unl, None 

7668 elif '#' not in unl: 

7669 # The path is empty. 

7670 # Move to the unl in *this* commander. 

7671 p = g.findUNL(unl.split("-->"), c) 

7672 if p: 

7673 c.redraw(p) 

7674 return c 

7675 else: 

7676 path, unl = unl.split('#', 1) 

7677 if unl and not path: # #2407 

7678 # Move to the unl in *this* commander. 

7679 p = g.findUNL(unl.split("-->"), c) 

7680 if p: 

7681 c.redraw(p) 

7682 return c 

7683 if c: 

7684 base = g.os_path_dirname(c.fileName()) 

7685 c_path = g.os_path_finalize_join(base, path) 

7686 else: 

7687 c_path = None 

7688 # Look for the file in various places. 

7689 table = ( 

7690 c_path, 

7691 g.os_path_finalize_join(g.app.loadDir, '..', path), 

7692 g.os_path_finalize_join(g.app.loadDir, '..', '..', path), 

7693 g.os_path_finalize_join(g.app.loadDir, '..', 'core', path), 

7694 g.os_path_finalize_join(g.app.loadDir, '..', 'config', path), 

7695 g.os_path_finalize_join(g.app.loadDir, '..', 'dist', path), 

7696 g.os_path_finalize_join(g.app.loadDir, '..', 'doc', path), 

7697 g.os_path_finalize_join(g.app.loadDir, '..', 'test', path), 

7698 g.app.loadDir, 

7699 g.app.homeDir, 

7700 ) 

7701 for path2 in table: 

7702 if path2 and path2.lower().endswith('.leo') and os.path.exists(path2): 

7703 path = path2 

7704 break 

7705 else: 

7706 g.es_print('path not found', repr(path)) 

7707 return None 

7708 # End editing in *this* outline, so typing in the new outline works. 

7709 c.endEditing() 

7710 c.redraw() 

7711 # Open the path. 

7712 c2 = g.openWithFileName(path, old_c=c) 

7713 if not c2: 

7714 return None 

7715 # Find the UNL, select the node, and redraw. 

7716 p = g.findUNL(unl.split("-->"), c2) 

7717 if not p: 

7718 return None 

7719 c2.redraw(p) 

7720 c2.bringToFront() 

7721 c2.bodyWantsFocusNow() 

7722 return c2 

7723#@+node:tbrown.20090219095555.63: *3* g.handleUrl & helpers 

7724def handleUrl(url: str, c: Cmdr=None, p: Pos=None) -> Any: 

7725 """Open a url or a unl.""" 

7726 if c and not p: 

7727 p = c.p 

7728 urll = url.lower() 

7729 if urll.startswith('@url'): 

7730 url = url[4:].lstrip() 

7731 if ( 

7732 urll.startswith('unl://') or 

7733 urll.startswith('file://') and url.find('-->') > -1 or 

7734 urll.startswith('#') 

7735 ): 

7736 return g.handleUnl(url, c) 

7737 try: 

7738 return g.handleUrlHelper(url, c, p) 

7739 except Exception: 

7740 g.es_print("g.handleUrl: exception opening", repr(url)) 

7741 g.es_exception() 

7742 return None 

7743#@+node:ekr.20170226054459.1: *4* g.handleUrlHelper 

7744def handleUrlHelper(url: str, c: Cmdr, p: Pos) -> None: 

7745 """Open a url. Most browsers should handle: 

7746 ftp://ftp.uu.net/public/whatever 

7747 http://localhost/MySiteUnderDevelopment/index.html 

7748 file:///home/me/todolist.html 

7749 """ 

7750 tag = 'file://' 

7751 original_url = url 

7752 if url.startswith(tag) and not url.startswith(tag + '#'): 

7753 # Finalize the path *before* parsing the url. 

7754 url = g.computeFileUrl(url, c=c, p=p) 

7755 parsed = urlparse.urlparse(url) 

7756 if parsed.netloc: 

7757 leo_path = os.path.join(parsed.netloc, parsed.path) 

7758 # "readme.txt" gets parsed into .netloc... 

7759 else: 

7760 leo_path = parsed.path 

7761 if leo_path.endswith('\\'): 

7762 leo_path = leo_path[:-1] 

7763 if leo_path.endswith('/'): 

7764 leo_path = leo_path[:-1] 

7765 if parsed.scheme == 'file' and leo_path.endswith('.leo'): 

7766 g.handleUnl(original_url, c) 

7767 elif parsed.scheme in ('', 'file'): 

7768 unquote_path = g.unquoteUrl(leo_path) 

7769 if g.unitTesting: 

7770 pass 

7771 elif g.os_path_exists(leo_path): 

7772 g.os_startfile(unquote_path) 

7773 else: 

7774 g.es(f"File '{leo_path}' does not exist") 

7775 else: 

7776 if g.unitTesting: 

7777 pass 

7778 else: 

7779 # Mozilla throws a weird exception, then opens the file! 

7780 try: 

7781 webbrowser.open(url) 

7782 except Exception: 

7783 pass 

7784#@+node:ekr.20170226060816.1: *4* g.traceUrl 

7785def traceUrl(c: Cmdr, path: str, parsed: Any, url: str) -> None: 

7786 

7787 print() 

7788 g.trace('url ', url) 

7789 g.trace('c.frame.title', c.frame.title) 

7790 g.trace('path ', path) 

7791 g.trace('parsed.fragment', parsed.fragment) 

7792 g.trace('parsed.netloc', parsed.netloc) 

7793 g.trace('parsed.path ', parsed.path) 

7794 g.trace('parsed.scheme', repr(parsed.scheme)) 

7795#@+node:ekr.20120311151914.9918: *3* g.isValidUrl 

7796def isValidUrl(url: str) -> bool: 

7797 """Return true if url *looks* like a valid url.""" 

7798 table = ( 

7799 'file', 'ftp', 'gopher', 'hdl', 'http', 'https', 'imap', 

7800 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu', 

7801 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', 'wais', 

7802 ) 

7803 if url.lower().startswith('unl://') or url.startswith('#'): 

7804 # All Leo UNL's. 

7805 return True 

7806 if url.startswith('@'): 

7807 return False 

7808 parsed = urlparse.urlparse(url) 

7809 scheme = parsed.scheme 

7810 for s in table: 

7811 if scheme.startswith(s): 

7812 return True 

7813 return False 

7814#@+node:ekr.20120315062642.9744: *3* g.openUrl 

7815def openUrl(p: Pos) -> None: 

7816 """ 

7817 Open the url of node p. 

7818 Use the headline if it contains a valid url. 

7819 Otherwise, look *only* at the first line of the body. 

7820 """ 

7821 if p: 

7822 url = g.getUrlFromNode(p) 

7823 if url: 

7824 c = p.v.context 

7825 if not g.doHook("@url1", c=c, p=p, url=url): 

7826 g.handleUrl(url, c=c, p=p) 

7827 g.doHook("@url2", c=c, p=p, url=url) 

7828#@+node:ekr.20110605121601.18135: *3* g.openUrlOnClick (open-url-under-cursor) 

7829def openUrlOnClick(event: Any, url: str=None) -> Optional[str]: 

7830 """Open the URL under the cursor. Return it for unit testing.""" 

7831 # This can be called outside Leo's command logic, so catch all exceptions. 

7832 try: 

7833 return openUrlHelper(event, url) 

7834 except Exception: 

7835 g.es_exception() 

7836 return None 

7837#@+node:ekr.20170216091704.1: *4* g.openUrlHelper 

7838def openUrlHelper(event: Any, url: str=None) -> Optional[str]: 

7839 """Open the UNL or URL under the cursor. Return it for unit testing.""" 

7840 c = getattr(event, 'c', None) 

7841 if not c: 

7842 return None 

7843 w = getattr(event, 'w', c.frame.body.wrapper) 

7844 if not g.app.gui.isTextWrapper(w): 

7845 g.internalError('must be a text wrapper', w) 

7846 return None 

7847 setattr(event, 'widget', w) 

7848 # Part 1: get the url. 

7849 if url is None: 

7850 s = w.getAllText() 

7851 ins = w.getInsertPoint() 

7852 i, j = w.getSelectionRange() 

7853 if i != j: 

7854 return None # So find doesn't open the url. 

7855 row, col = g.convertPythonIndexToRowCol(s, ins) 

7856 i, j = g.getLine(s, ins) 

7857 line = s[i:j] 

7858 # Find the url on the line. 

7859 for match in g.url_regex.finditer(line): 

7860 # Don't open if we click after the url. 

7861 if match.start() <= col < match.end(): 

7862 url = match.group() 

7863 if g.isValidUrl(url): 

7864 break 

7865 else: 

7866 # Look for the unl: 

7867 for match in g.unl_regex.finditer(line): 

7868 # Don't open if we click after the unl. 

7869 if match.start() <= col < match.end(): 

7870 unl = match.group() 

7871 g.handleUnl(unl, c) 

7872 return None 

7873 elif not isinstance(url, str): 

7874 url = url.toString() 

7875 url = g.toUnicode(url) 

7876 # Fix #571 

7877 if url and g.isValidUrl(url): 

7878 # Part 2: handle the url 

7879 p = c.p 

7880 if not g.doHook("@url1", c=c, p=p, url=url): 

7881 g.handleUrl(url, c=c, p=p) 

7882 g.doHook("@url2", c=c, p=p) 

7883 return url 

7884 # Part 3: call find-def. 

7885 if not w.hasSelection(): 

7886 c.editCommands.extendToWord(event, select=True) 

7887 word = w.getSelectedText().strip() 

7888 if word: 

7889 c.findCommands.find_def_strict(event) 

7890 return None 

7891#@+node:ekr.20170226093349.1: *3* g.unquoteUrl 

7892def unquoteUrl(url: str) -> str: 

7893 """Replace special characters (especially %20, by their equivalent).""" 

7894 return urllib.parse.unquote(url) 

7895#@-others 

7896# set g when the import is about to complete. 

7897g: Any = sys.modules.get('leo.core.leoGlobals') 

7898assert g, sorted(sys.modules.keys()) 

7899if __name__ == '__main__': 

7900 unittest.main() 

7901 

7902#@@language python 

7903#@@tabwidth -4 

7904#@@pagewidth 70 

7905#@-leo