Coverage for C:\leo.repo\leo-editor\leo\core\leoGlobals.py: 42%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

4917 statements  

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

2#@+leo-ver=5-thin 

3#@+node:ekr.20031218072017.3093: * @file leoGlobals.py 

4#@@first 

5""" 

6Global constants, variables and utility functions used throughout Leo. 

7 

8Important: This module imports no other Leo module. 

9""" 

10#@+<< imports >> 

11#@+node:ekr.20050208101229: ** << imports >> (leoGlobals) 

12import binascii 

13import codecs 

14import fnmatch 

15from functools import reduce 

16import gc 

17import gettext 

18import glob 

19import importlib 

20import inspect 

21import io 

22import operator 

23import os 

24from pathlib import Path 

25 

26# import pdb # Do NOT import pdb here! 

27 # We shall define pdb as a _function_ below. 

28import re 

29import shlex 

30import string 

31import sys 

32import subprocess 

33import tempfile 

34import textwrap 

35import time 

36import traceback 

37import types 

38from typing import TYPE_CHECKING 

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

40import unittest 

41import urllib 

42import urllib.parse as urlparse 

43import webbrowser 

44try: 

45 import tkinter as Tk 

46except Exception: 

47 Tk = None 

48# 

49# Leo never imports any other Leo module. 

50if TYPE_CHECKING: # Always False at runtime. 

51 from leo.core.leoCommands import Commands as Cmdr 

52 from leo.core.leoNodes import Position as Pos 

53 from leo.core.leoNodes import VNode 

54else: 

55 Cmdr = Pos = VNode = Any 

56# 

57# Abbreviations... 

58StringIO = io.StringIO 

59#@-<< imports >> 

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

61in_vs_code = False # #2098. 

62minimum_python_version = '3.6' # #1215. 

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

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

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

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

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

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

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

70globalDirectiveList = [ 

71 # Order does not matter. 

72 'all', 

73 'beautify', 

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

75 'delims', 'doc', 

76 'encoding', 

77 # 'end_raw', # #2276. 

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

79 'killbeautify', 'killcolor', 

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

81 'markup', 

82 'nobeautify', 

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

84 'nopyflakes', # Leo 6.1. 

85 'nosearch', # Leo 5.3. 

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

87 # 'raw', # #2276. 

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

89 'silent', 

90 'tabwidth', 'terse', 

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

92] 

93 

94directives_pat = None # Set below. 

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

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

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

98#@@nobeautify 

99#@@language rest 

100#@+at 

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

102# following appears in leo.commands. 

103# 

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

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

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

107# 

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

109#@@c 

110#@@language python 

111 

112global_commands_dict = {} 

113 

114cmd_instance_dict = { 

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

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

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

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

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

120 'Commands': ['c'], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

140} 

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

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

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

144#@+others 

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

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

147 """ 

148 Check g.check_cmd_instance_dict. 

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

150 """ 

151 d = cmd_instance_dict 

152 for key in d: 

153 ivars = d.get(key) 

154 # Produces warnings. 

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

156 if obj: 

157 name = obj.__class__.__name__ 

158 if name != key: 

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

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

161class Command: 

162 """ 

163 A global decorator for creating commands. 

164 

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

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

167 

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

169 def A_Command(event): 

170 c = event.get('c') 

171 ... 

172 

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

174 """ 

175 

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

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

178 self.name = name 

179 

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

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

182 global_commands_dict[self.name] = func 

183 if app: 

184 for c in app.commanders(): 

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

186 # Inject ivars for plugins_menu.py. 

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

188 func.is_command = True 

189 func.command_name = self.name 

190 return func 

191 

192command = Command 

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

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

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

196 from leo.core import leoCommands 

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

198 funcToMethod(func, leoCommands.Commands, alias) 

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

200class CommanderCommand: 

201 """ 

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

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

204 

205 Usage: 

206 

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

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

209 ... 

210 

211 The decorator injects command_name into the Commander class and calls 

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

213 

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

215 """ 

216 

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

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

219 self.name = name 

220 

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

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

223 

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

225 c = event.get('c') 

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

227 method(event=event) 

228 

229 # Inject ivars for plugins_menu.py. 

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

231 commander_command_wrapper.__name__ = self.name 

232 commander_command_wrapper.__doc__ = func.__doc__ 

233 global_commands_dict[self.name] = commander_command_wrapper 

234 if app: 

235 from leo.core import leoCommands 

236 funcToMethod(func, leoCommands.Commands) 

237 for c in app.commanders(): 

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

239 # Inject ivars for plugins_menu.py. 

240 func.is_command = True 

241 func.command_name = self.name 

242 return func 

243 

244commander_command = CommanderCommand 

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

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

247 """ 

248 Return the instance of c given by ivars. 

249 ivars is a list of strings. 

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

251 """ 

252 if not ivars: 

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

254 return None 

255 ivar = ivars[0] 

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

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

258 return None 

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

260 for ivar in ivars[1:]: 

261 obj = getattr(obj, ivar, None) 

262 if not obj: 

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

264 break 

265 return obj 

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

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

268 """ 

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

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

271 

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

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

274 """ 

275 

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

277 

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

279 if isinstance(event, dict): 

280 c = event.get('c') 

281 else: 

282 c = event.c 

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

284 try: 

285 func(self, event=event) 

286 # Don't use a keyword for self. 

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

288 except Exception: 

289 g.es_exception() 

290 

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

292 new_cmd_wrapper.__name__ = name 

293 new_cmd_wrapper.__doc__ = func.__doc__ 

294 global_commands_dict[name] = new_cmd_wrapper 

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

296 return func 

297 # The decorator must return the func itself. 

298 

299 return _decorator 

300#@-others 

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

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

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

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

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

306# 

307# Patterns used only in this module... 

308 

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

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

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

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

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

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

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

316 

317# Regex to find GNX 

318USERCHAR = r"""[^.,"'\s]""" # from LeoApp.cleanLeoID() 

319USERID = f'{USERCHAR}{{2}}{USERCHAR}+' # At least three USERCHARs 

320GNXre = re.compile(rf"""{USERID}\. 

321 [0-9]+\. # timestamp 

322 [0-9]+""", re.VERBOSE) # NodeIndices.lastIndex 

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

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

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

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

327# Global status vars. 

328inScript = False # A synonym for app.inScript 

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

330#@+others 

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

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

333def standard_timestamp() -> str: 

334 """Return a reasonable timestamp.""" 

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

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

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

338 """ 

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

340 

341 The main backup directory is computed as follows: 

342 

343 1. os.environ['LEO_BACKUP'] 

344 2. ~/Backup 

345 """ 

346 # Compute the main backup directory. 

347 # First, try the LEO_BACKUP directory. 

348 backup = None 

349 try: 

350 backup = os.environ['LEO_BACKUP'] 

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

352 backup = None 

353 except KeyError: 

354 pass 

355 except Exception: 

356 g.es_exception() 

357 # Second, try ~/Backup. 

358 if not backup: 

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

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

361 backup = None 

362 if not backup: 

363 return None 

364 # Compute the path to backup/sub_directory 

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

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

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

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

369class BindingInfo: 

370 """ 

371 A class representing any kind of key binding line. 

372 

373 This includes other information besides just the KeyStroke. 

374 """ 

375 # Important: The startup code uses this class, 

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

377 #@+others 

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

379 def __init__( 

380 self, 

381 kind: str, 

382 commandName: str='', 

383 func: Any=None, 

384 nextMode: Any=None, 

385 pane: Any=None, 

386 stroke: Any=None, 

387 ) -> None: 

388 if not g.isStrokeOrNone(stroke): 

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

390 self.kind = kind 

391 self.commandName = commandName 

392 self.func = func 

393 self.nextMode = nextMode 

394 self.pane = pane 

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

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

397 def __hash__(self) -> Any: 

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

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

400 def __repr__(self) -> str: 

401 return self.dump() 

402 

403 __str__ = __repr__ 

404 

405 def dump(self) -> str: 

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

407 # Print all existing ivars. 

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

409 for ivar in table: 

410 if hasattr(self, ivar): 

411 val = getattr(self, ivar) 

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

413 if ivar == 'func': 

414 # pylint: disable=no-member 

415 val = val.__name__ 

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

417 result.append(s) 

418 # Clearer w/o f-string. 

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

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

421 def isModeBinding(self) -> bool: 

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

423 #@-others 

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

425 return isinstance(obj, BindingInfo) 

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

427class Bunch: 

428 """ 

429 From The Python Cookbook: 

430 

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

432 

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

434 

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

436 del some of them, etc:: 

437 

438 if point.squared > threshold: 

439 point.isok = True 

440 """ 

441 

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

443 self.__dict__.update(keywords) 

444 

445 def __repr__(self) -> str: 

446 return self.toString() 

447 

448 def ivars(self) -> List: 

449 return sorted(self.__dict__) 

450 

451 def keys(self) -> List: 

452 return sorted(self.__dict__) 

453 

454 def toString(self) -> str: 

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

456 entries = [ 

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

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

459 ] 

460 # Fail. 

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

462 result.extend(entries) 

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

464 

465 # Used by new undo code. 

466 

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

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

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

470 

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

472 """Support aBunch[key]""" 

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

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

475 

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

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

478 

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

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

481 return key in self.__dict__ 

482 

483bunch = Bunch 

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

485class EmergencyDialog: 

486 """ 

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

488  

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

490 passed to the ctor. 

491  

492 """ 

493 #@+others 

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

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

496 """Constructor for the leoTkinterDialog class.""" 

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

498 self.title = title 

499 self.message = message 

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

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

502 self.defaultButtonCommand = None 

503 self.frame = None # The outermost frame. 

504 self.root = None # Created in createTopFrame. 

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

506 if Tk: # #2512. 

507 self.createTopFrame() 

508 buttons = [{ 

509 "text": "OK", 

510 "command": self.okButton, 

511 "default": True, 

512 }] 

513 self.createButtons(buttons) 

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

515 else: 

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

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

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

519 """Create a row of buttons. 

520 

521 buttons is a list of dictionaries containing 

522 the properties of each button. 

523 """ 

524 assert self.frame 

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

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

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

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

529 buttonList = [] 

530 for d in buttons: 

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

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

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

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

535 bd = 4 if isDefault else 2 

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

537 underline=underline, command=command) 

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

539 buttonList.append(b) 

540 if isDefault and command: 

541 self.defaultButtonCommand = command 

542 return buttonList 

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

544 def createTopFrame(self) -> None: 

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

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

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

548 self.top.title(self.title) 

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

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

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

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

553 label.pack(pady=10) 

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

555 def okButton(self) -> None: 

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

557 self.top.destroy() 

558 self.top = None 

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

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

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

562 self.okButton() 

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

564 def run(self) -> None: 

565 """Run the modal emergency dialog.""" 

566 # Suppress f-stringify. 

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

568 self.top.lift() 

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

570 self.root.wait_window(self.top) 

571 #@-others 

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

573# Important: The startup code uses this class, 

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

575 

576 

577class GeneralSetting: 

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

579 

580 def __init__( 

581 self, 

582 kind: str, 

583 encoding: str=None, 

584 ivar: str=None, 

585 setting: str=None, 

586 val: Any=None, 

587 path: str=None, 

588 tag: str='setting', 

589 unl: str=None, 

590 ) -> None: 

591 self.encoding = encoding 

592 self.ivar = ivar 

593 self.kind = kind 

594 self.path = path 

595 self.unl = unl 

596 self.setting = setting 

597 self.val = val 

598 self.tag = tag 

599 

600 def __repr__(self) -> str: 

601 # Better for g.printObj. 

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

603 return ( 

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

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

606 

607 dump = __repr__ 

608 __str__ = __repr__ 

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

610class KeyStroke: 

611 """ 

612 A class that represent any key stroke or binding. 

613 

614 stroke.s is the "canonicalized" stroke. 

615 """ 

616 #@+others 

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

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

619 

620 if binding: 

621 self.s = self.finalize_binding(binding) 

622 else: 

623 self.s = None # type:ignore 

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

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

626 # for key in sorted(d) 

627 # where the keys of d are KeyStroke objects. 

628 #@@c 

629 

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

631 if not other: 

632 return False 

633 if hasattr(other, 's'): 

634 return self.s == other.s 

635 return self.s == other 

636 

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

638 if not other: 

639 return False 

640 if hasattr(other, 's'): 

641 return self.s < other.s 

642 return self.s < other 

643 

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

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

646 

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

648 return not self.__eq__(other) 

649 

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

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

652 

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

654 return not self.__lt__(other) 

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

656 # Allow KeyStroke objects to be keys in dictionaries. 

657 

658 def __hash__(self) -> Any: 

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

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

661 def __repr__(self) -> str: 

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

663 

664 def __str__(self) -> str: 

665 return repr(self.s) 

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

667 def dump(self) -> None: 

668 """Show results of printable chars.""" 

669 for i in range(128): 

670 s = chr(i) 

671 stroke = g.KeyStroke(s) 

672 if stroke.s != s: 

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

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

675 stroke = g.KeyStroke(ch) 

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

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

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

679 

680 # This trace is good for devs only. 

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

682 self.mods = self.find_mods(binding) 

683 s = self.strip_mods(binding) 

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

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

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

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

688 return mods + s 

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

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

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

692 # 

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

694 shift_d = { 

695 'bksp': 'BackSpace', 

696 'backspace': 'BackSpace', 

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

698 'linefeed': 'Return', 

699 '\r': 'Return', 

700 'return': 'Return', 

701 'tab': 'Tab', 

702 } 

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

704 # Returning '' breaks existing code. 

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

706 # 

707 # Make all other translations... 

708 # 

709 # This dict ensures proper capitalization. 

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

711 translate_d = { 

712 # 

713 # The gang of four... 

714 'bksp': 'BackSpace', 

715 'backspace': 'BackSpace', 

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

717 'linefeed': '\n', 

718 '\r': '\n', 

719 'return': '\n', 

720 'tab': 'Tab', 

721 # 

722 # Special chars... 

723 'delete': 'Delete', 

724 'down': 'Down', 

725 'end': 'End', 

726 'enter': 'Enter', 

727 'escape': 'Escape', 

728 'home': 'Home', 

729 'insert': 'Insert', 

730 'left': 'Left', 

731 'next': 'Next', 

732 'prior': 'Prior', 

733 'right': 'Right', 

734 'up': 'Up', 

735 # 

736 # Qt key names... 

737 'del': 'Delete', 

738 'dnarrow': 'Down', 

739 'esc': 'Escape', 

740 'ins': 'Insert', 

741 'ltarrow': 'Left', 

742 'pagedn': 'Next', 

743 'pageup': 'Prior', 

744 'pgdown': 'Next', 

745 'pgup': 'Prior', 

746 'rtarrow': 'Right', 

747 'uparrow': 'Up', 

748 # 

749 # Legacy Tk binding names... 

750 "ampersand": "&", 

751 "asciicircum": "^", 

752 "asciitilde": "~", 

753 "asterisk": "*", 

754 "at": "@", 

755 "backslash": "\\", 

756 "bar": "|", 

757 "braceleft": "{", 

758 "braceright": "}", 

759 "bracketleft": "[", 

760 "bracketright": "]", 

761 "colon": ":", 

762 "comma": ",", 

763 "dollar": "$", 

764 "equal": "=", 

765 "exclam": "!", 

766 "greater": ">", 

767 "less": "<", 

768 "minus": "-", 

769 "numbersign": "#", 

770 "quotedbl": '"', 

771 "quoteright": "'", 

772 "parenleft": "(", 

773 "parenright": ")", 

774 "percent": "%", 

775 "period": ".", 

776 "plus": "+", 

777 "question": "?", 

778 "quoteleft": "`", 

779 "semicolon": ";", 

780 "slash": "/", 

781 "space": " ", 

782 "underscore": "_", 

783 } 

784 # 

785 # pylint: disable=undefined-loop-variable 

786 # Looks like a pylint bug. 

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

788 return 'None' 

789 if s.lower() in translate_d: 

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

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

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

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

794 return '' 

795 if s.isalpha(): 

796 if len(s) == 1: 

797 if 'shift' in self.mods: 

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

799 self.mods.remove('shift') 

800 s = s.upper() 

801 else: 

802 s = s.lower() 

803 elif self.mods: 

804 s = s.lower() 

805 else: 

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

807 s = '' 

808 if 0: 

809 # Make sure all special chars are in translate_d. 

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

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

812 s = s.capitalize() 

813 return s 

814 # 

815 # Translate shifted keys to their appropriate alternatives. 

816 return self.strip_shift(s) 

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

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

819 """ 

820 Handle supposedly shifted keys. 

821 

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

823 

824 The legacy Tk binding names have already been translated, 

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

826 """ 

827 # 

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

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

830 shift_d = { 

831 # Top row of keyboard. 

832 "`": "~", "~": "~", 

833 "1": "!", "!": "!", 

834 "2": "@", "@": "@", 

835 "3": "#", "#": "#", 

836 "4": "$", "$": "$", 

837 "5": "%", "%": "%", 

838 "6": "^", "^": "^", 

839 "7": "&", "&": "&", 

840 "8": "*", "*": "*", 

841 "9": "(", "(": "(", 

842 "0": ")", ")": ")", 

843 "-": "_", "_": "_", 

844 "=": "+", "+": "+", 

845 # Second row of keyboard. 

846 "[": "{", "{": "{", 

847 "]": "}", "}": "}", 

848 "\\": '|', "|": "|", 

849 # Third row of keyboard. 

850 ";": ":", ":": ":", 

851 "'": '"', '"': '"', 

852 # Fourth row of keyboard. 

853 ".": "<", "<": "<", 

854 ",": ">", ">": ">", 

855 "//": "?", "?": "?", 

856 } 

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

858 self.mods.remove('shift') 

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

860 return s 

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

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

863 

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

865 return self.s.find(pattern) 

866 

867 def lower(self) -> str: 

868 return self.s.lower() 

869 

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

871 return self.s.startswith(s) 

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

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

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

875 s = s.lower() 

876 table = ( 

877 ['alt',], 

878 ['command', 'cmd',], 

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

880 ['meta',], 

881 ['shift', 'shft',], 

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

883 # 868: Allow alternative spellings. 

884 ) 

885 result = [] 

886 for aList in table: 

887 kind = aList[0] 

888 for mod in aList: 

889 for suffix in '+-': 

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

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

892 result.append(kind) 

893 break 

894 return result 

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

896 def isAltCtrl(self) -> bool: 

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

898 mods = self.find_mods(self.s) 

899 return 'alt' in mods and 'ctrl' in mods 

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

901 def isFKey(self) -> bool: 

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

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

904 def isPlainKey(self) -> bool: 

905 """ 

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

907 

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

909 

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

911 """ 

912 s = self.s 

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

914 # For unit tests. 

915 return False 

916 # #868: 

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

918 # Enable bindings. 

919 return False 

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

921 return False 

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

923 return False 

924 if s == 'BackSpace': 

925 return False 

926 return True 

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

928 def isNumPadKey(self) -> bool: 

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

930 

931 def isPlainNumPad(self) -> bool: 

932 return ( 

933 self.isNumPadKey() and 

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

935 ) 

936 

937 def removeNumPadModifier(self) -> None: 

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

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

940 def prettyPrint(self) -> str: 

941 

942 s = self.s 

943 if not s: 

944 return '<None>' 

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

946 ch = s[-1] 

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

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

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

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

951 table = ( 

952 'alt', 

953 'cmd', 'command', 

954 'control', 'ctrl', 

955 'keypad', 'key_pad', # 868: 

956 'meta', 

957 'shift', 'shft', 

958 ) 

959 for mod in table: 

960 for suffix in '+-': 

961 target = mod + suffix 

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

963 if i > -1: 

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

965 break 

966 return s 

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

968 def toGuiChar(self) -> str: 

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

970 s = self.s.lower() 

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

972 s = '\n' 

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

974 s = '\t' 

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

976 s = '\b' 

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

978 s = '.' 

979 return s 

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

981 def toInsertableChar(self) -> str: 

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

983 # pylint: disable=len-as-condition 

984 s = self.s 

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

986 return '' 

987 # Handle the "Gang of Four" 

988 d = { 

989 'BackSpace': '\b', 

990 'LineFeed': '\n', 

991 # 'Insert': '\n', 

992 'Return': '\n', 

993 'Tab': '\t', 

994 } 

995 if s in d: 

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

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

998 #@-others 

999 

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

1001 return isinstance(obj, KeyStroke) 

1002 

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

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

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

1006class MatchBrackets: 

1007 """ 

1008 A class implementing the match-brackets command. 

1009 

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

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

1012 javascript) regex. 

1013 """ 

1014 #@+others 

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

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

1017 """Ctor for MatchBrackets class.""" 

1018 self.c = c 

1019 self.p = p.copy() 

1020 self.language = language 

1021 # Constants. 

1022 self.close_brackets = ")]}>" 

1023 self.open_brackets = "([{<" 

1024 self.brackets = self.open_brackets + self.close_brackets 

1025 self.matching_brackets = self.close_brackets + self.open_brackets 

1026 # Language dependent. 

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

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

1029 # to track expanding selection 

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

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

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

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

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

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

1036 assert s[i] == '/' 

1037 offset = 1 if self.forward else -1 

1038 i += offset 

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

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

1041 return True 

1042 i += offset 

1043 return False 

1044 return False 

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

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

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

1048 assert s[i] == '/' 

1049 offset = 1 if self.forward else -1 

1050 i1 = i 

1051 i += offset 

1052 found: Union[int, bool] = False 

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

1054 ch = s[i] 

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

1056 i += offset 

1057 if ch == '/': 

1058 # Count the preceding backslashes. 

1059 n = 0 

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

1061 n += 1 

1062 i2 -= 1 

1063 if (n % 2) == 0: 

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

1065 found = i 

1066 else: 

1067 found = i 

1068 break 

1069 if found is None: 

1070 self.oops('unmatched regex delim') 

1071 return i1 + offset 

1072 return found 

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

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

1075 """ 

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

1077 Return the index of the next character. 

1078 """ 

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

1080 delim = s[i] 

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

1082 offset = 1 if self.forward else -1 

1083 i += offset 

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

1085 ch = s[i] 

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

1087 i += offset 

1088 if ch == delim: 

1089 # Count the preceding backslashes. 

1090 n = 0 

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

1092 n += 1 

1093 i2 -= 1 

1094 if (n % 2) == 0: 

1095 return i 

1096 # Annoying when matching brackets on the fly. 

1097 # self.oops('unmatched string') 

1098 return i + offset 

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

1100 def expand_range( 

1101 self, 

1102 s: str, 

1103 left: int, 

1104 right: int, 

1105 max_right: int, 

1106 expand: bool=False, 

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

1108 """ 

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

1110 

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

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

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

1114 or s[right] being a bracket. 

1115 

1116 Returns 

1117 new_left, new_right, bracket_char, index_of_bracket_char 

1118 if expansion succeeds, otherwise 

1119 None, None, None, None 

1120 

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

1122 bracket, but index_of_bracket_char will definitely be a bracket. 

1123 """ 

1124 expanded: Union[bool, str] = False 

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

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

1127 orig_left = left 

1128 orig_right = right 

1129 while ( 

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

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

1132 and (left > 0 or right < max_right) 

1133 ): 

1134 expanded = False 

1135 if left > 0: 

1136 left -= 1 

1137 if s[left] in self.brackets: 

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

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

1140 expanded = 'left' 

1141 if right < max_right: 

1142 right += 1 

1143 if s[right] in self.brackets: 

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

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

1146 expanded = 'right' 

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

1148 return left, right, s[left], left 

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

1150 return left, right, s[right], right 

1151 return None, None, None, None 

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

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

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

1155 self.forward = ch1 in self.open_brackets 

1156 # Find the character matching the initial bracket. 

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

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

1159 target = self.matching_brackets[n] 

1160 break 

1161 else: 

1162 return None 

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

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

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

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

1167 """Scan forward for target.""" 

1168 level = 0 

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

1170 progress = i 

1171 ch = s[i] 

1172 if ch in '"\'': 

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

1174 i = self.scan_string(s, i) 

1175 elif self.starts_comment(s, i): 

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

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

1178 i = self.scan_regex(s, i) 

1179 elif ch == ch1: 

1180 level += 1 

1181 i += 1 

1182 elif ch == target: 

1183 level -= 1 

1184 if level <= 0: 

1185 return i 

1186 i += 1 

1187 else: 

1188 i += 1 

1189 assert i > progress 

1190 # Not found 

1191 return None 

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

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

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

1195 i1 = i 

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

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

1198 offset = 1 if self.forward else -1 

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

1200 if not self.forward: 

1201 i1 += len(end) 

1202 i += offset 

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

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

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

1206 return i 

1207 i += offset 

1208 self.oops('unmatched multiline comment') 

1209 elif self.forward: 

1210 # Scan to the newline. 

1211 target = '\n' 

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

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

1214 i += 1 

1215 return i 

1216 i += 1 

1217 else: 

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

1219 target = self.single_comment 

1220 found = None 

1221 i -= 1 

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

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

1224 found = i 

1225 i -= 1 

1226 if found is None: 

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

1228 found = 0 

1229 return found 

1230 return i 

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

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

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

1234 assert 0 <= i < len(s) 

1235 if self.forward: 

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

1237 return True 

1238 return ( 

1239 self.start_comment and self.end_comment and 

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

1241 ) 

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

1243 if self.single_comment: 

1244 # Scan backward for any single-comment delim. 

1245 i -= 1 

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

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

1248 return True 

1249 i -= 1 

1250 return False 

1251 return ( 

1252 self.start_comment and self.end_comment and 

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

1254 ) 

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

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

1257 """Scan backwards for delim.""" 

1258 level = 0 

1259 while i >= 0: 

1260 progress = i 

1261 ch = s[i] 

1262 if self.ends_comment(s, i): 

1263 i = self.back_scan_comment(s, i) 

1264 elif ch in '"\'': 

1265 # Scan to the beginning of the string. 

1266 i = self.scan_string(s, i) 

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

1268 i = self.scan_regex(s, i) 

1269 elif ch == ch1: 

1270 level += 1 

1271 i -= 1 

1272 elif ch == target: 

1273 level -= 1 

1274 if level <= 0: 

1275 return i 

1276 i -= 1 

1277 else: 

1278 i -= 1 

1279 assert i < progress 

1280 # Not found 

1281 return None 

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

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

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

1285 i1 = i 

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

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

1288 i -= 1 

1289 while i >= 0: 

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

1291 i -= 1 

1292 return i 

1293 i -= 1 

1294 self.oops('unmatched multiline comment') 

1295 return i 

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

1297 found = None 

1298 i -= 1 

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

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

1301 found = i - 1 

1302 i -= 1 

1303 if found is None: 

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

1305 found = 0 

1306 return found 

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

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

1309 """ 

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

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

1312 """ 

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

1314 # This is the hard (dubious) case. 

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

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

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

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

1319 # 

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

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

1322 if self.single_comment: 

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

1324 quote = None 

1325 i -= 1 

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

1327 progress = i 

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

1329 quote = None 

1330 i -= 1 

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

1332 if not quote: 

1333 quote = s[i] 

1334 i -= 1 

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

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

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

1338 if quote: 

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

1340 if s[i] == quote: 

1341 return False 

1342 i -= 1 

1343 return True 

1344 else: 

1345 i -= 1 

1346 assert progress > i 

1347 return False 

1348 return ( 

1349 self.start_comment and 

1350 self.end_comment and 

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

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

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

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

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

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

1357 #@@nobeautify 

1358 

1359 def run(self) -> None: 

1360 """The driver for the MatchBrackets class. 

1361 

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

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

1364 

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

1366 range. The second time, select enclosing range. 

1367 """ 

1368 # 

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

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

1371 s = w.getAllText() 

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

1373 sel_range = w.getSelectionRange() 

1374 if not w.hasSelection(): 

1375 _mb['count'] = 1 

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

1377 # haven't been to other end yet 

1378 _mb['count'] += 1 

1379 # move insert point to other end of selection 

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

1381 w.setSelectionRange( 

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

1383 return 

1384 

1385 # Find the bracket nearest the cursor. 

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

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

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

1389 if left is None: 

1390 g.es("Bracket not found") 

1391 return 

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

1393 if index2 is None: 

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

1395 return 

1396 

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

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

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

1400 # to any enclosing brackets 

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

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

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

1404 _mb['count'] += 1 

1405 else: 

1406 _mb['count'] = 1 

1407 _mb['range'] = minmax 

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

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

1410 s, 

1411 max(minmax[0], 0), 

1412 min(minmax[1], max_right), 

1413 max_right, expand=True 

1414 ) 

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

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

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

1418 index, index2 = index3, index4 

1419 _mb['count'] = 1 

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

1421 

1422 if index2 is not None: 

1423 if index2 < index: 

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

1425 else: 

1426 w.setSelectionRange( 

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

1428 w.see(index2) 

1429 else: 

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

1431 #@-others 

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

1433class PosList(list): 

1434 #@+<< docstring for PosList >> 

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

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

1437 

1438 This is deprecated, use leoNodes.PosList instead! 

1439 

1440 # Creates a PosList containing all positions in c. 

1441 aList = g.PosList(c) 

1442 

1443 # Creates a PosList from aList2. 

1444 aList = g.PosList(c,aList2) 

1445 

1446 # Creates a PosList containing all positions p in aList 

1447 # such that p.h matches the pattern. 

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

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

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

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

1452 

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

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

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

1456 """ 

1457 #@-<< docstring for PosList >> 

1458 #@+others 

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

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

1461 self.c = c 

1462 super().__init__() 

1463 if aList is None: 

1464 for p in c.all_positions(): 

1465 self.append(p.copy()) 

1466 else: 

1467 for p in aList: 

1468 self.append(p.copy()) 

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

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

1471 if verbose: 

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

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

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

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

1476 """ 

1477 Return a new PosList containing all positions 

1478 in self that match the given pattern. 

1479 """ 

1480 c = self.c 

1481 

1482 aList = [] 

1483 if regex: 

1484 for p in self: 

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

1486 aList.append(p.copy()) 

1487 else: 

1488 for p in self: 

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

1490 aList.append(p.copy()) 

1491 if removeClones: 

1492 aList = self.removeClones(aList) 

1493 return PosList(c, aList) 

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

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

1496 seen = {} 

1497 aList2: List[Pos] = [] 

1498 for p in aList: 

1499 if p.v not in seen: 

1500 seen[p.v] = p.v 

1501 aList2.append(p) 

1502 return aList2 

1503 #@-others 

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

1505class ReadLinesClass: 

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

1507 

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

1509 self.lines = g.splitLines(s) 

1510 self.i = 0 

1511 

1512 def next(self) -> str: 

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

1514 line = self.lines[self.i] 

1515 self.i += 1 

1516 else: 

1517 line = '' 

1518 return line 

1519 

1520 __next__ = next 

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

1522class RedirectClass: 

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

1524 #@+<< RedirectClass methods >> 

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

1526 #@+others 

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

1528 def __init__(self) -> None: 

1529 self.old = None 

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

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

1532 def isRedirected(self) -> bool: 

1533 return self.old is not None 

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

1535 # For LeoN: just for compatibility. 

1536 

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

1538 return 

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

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

1541 if self.old: 

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

1543 else: 

1544 g.pr(s) 

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

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

1547 if g.app.batchMode: 

1548 # Redirection is futile in batch mode. 

1549 return 

1550 if not self.old: 

1551 if stdout: 

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

1553 else: 

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

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

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

1557 if self.old: 

1558 if stdout: 

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

1560 else: 

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

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

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

1564 

1565 if self.old: 

1566 if app.log: 

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

1568 else: 

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

1570 else: 

1571 # Can happen when g.batchMode is True. 

1572 g.pr(s) 

1573 #@-others 

1574 #@-<< RedirectClass methods >> 

1575 

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

1577 

1578redirectStdErrObj = RedirectClass() 

1579redirectStdOutObj = RedirectClass() 

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

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

1582#@+others 

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

1584# Redirect streams to the current log window. 

1585 

1586def redirectStderr() -> None: 

1587 global redirectStdErrObj 

1588 redirectStdErrObj.redirect(stdout=False) 

1589 

1590def redirectStdout() -> None: 

1591 global redirectStdOutObj 

1592 redirectStdOutObj.redirect() 

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

1594# Restore standard streams. 

1595 

1596def restoreStderr() -> None: 

1597 global redirectStdErrObj 

1598 redirectStdErrObj.undirect(stdout=False) 

1599 

1600def restoreStdout() -> None: 

1601 global redirectStdOutObj 

1602 redirectStdOutObj.undirect() 

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

1604def stdErrIsRedirected() -> bool: 

1605 global redirectStdErrObj 

1606 return redirectStdErrObj.isRedirected() 

1607 

1608def stdOutIsRedirected() -> bool: 

1609 global redirectStdOutObj 

1610 return redirectStdOutObj.isRedirected() 

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

1612# Send output to original stdout. 

1613 

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

1615 global redirectStdOutObj 

1616 redirectStdOutObj.rawPrint(s) 

1617#@-others 

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

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

1620class SherlockTracer: 

1621 """ 

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

1623 

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

1625 

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

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

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

1629 

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

1631 matches the regular expression x. 

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

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

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

1635 

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

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

1638 

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

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

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

1642 

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

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

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

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

1647 

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

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

1650 code needs to be inserted anywhere. 

1651 

1652 Usage: 

1653 

1654 g.SherlockTracer(patterns).run() 

1655 """ 

1656 #@+others 

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

1658 def __init__( 

1659 self, 

1660 patterns: List[Any], 

1661 dots: bool=True, 

1662 show_args: bool=True, 

1663 show_return: bool=True, 

1664 verbose: bool=True, 

1665 ) -> None: 

1666 """SherlockTracer ctor.""" 

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

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

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

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

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

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

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

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

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

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

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

1678 self.set_patterns(patterns) 

1679 from leo.core.leoQt import QtCore 

1680 if QtCore: 

1681 # pylint: disable=no-member 

1682 QtCore.pyqtRemoveInputHook() 

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

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

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

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

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

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

1689 """Report a bad Sherlock pattern.""" 

1690 if pattern not in self.bad_patterns: 

1691 self.bad_patterns.append(pattern) 

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

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

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

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

1696 try: 

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

1698 if pattern.startswith(prefix): 

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

1700 return True 

1701 self.bad_pattern(pattern) 

1702 return False 

1703 except Exception: 

1704 self.bad_pattern(pattern) 

1705 return False 

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

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

1708 """The dispatch method.""" 

1709 if event == 'call': 

1710 self.do_call(frame, arg) 

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

1712 self.do_return(frame, arg) 

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

1714 self.do_line(frame, arg) 

1715 # Queue the SherlockTracer instance again. 

1716 return self 

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

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

1719 """Trace through a function call.""" 

1720 frame1 = frame 

1721 code = frame.f_code 

1722 file_name = code.co_filename 

1723 locals_ = frame.f_locals 

1724 function_name = code.co_name 

1725 try: 

1726 full_name = self.get_full_name(locals_, function_name) 

1727 except Exception: 

1728 full_name = function_name 

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

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

1731 return 

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

1733 while frame: 

1734 frame = frame.f_back 

1735 n += 1 

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

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

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

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

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

1741 # Always update stats. 

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

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

1744 self.stats[file_name] = d 

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

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

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

1748 code = frame.f_code 

1749 locals_ = frame.f_locals 

1750 name = code.co_name 

1751 n = code.co_argcount 

1752 if code.co_flags & 4: 

1753 n = n + 1 

1754 if code.co_flags & 8: 

1755 n = n + 1 

1756 result = [] 

1757 for i in range(n): 

1758 name = code.co_varnames[i] 

1759 if name != 'self': 

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

1761 if arg: 

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

1763 # Clearer w/o f-string 

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

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

1766 else: 

1767 val = self.show(arg) 

1768 if val: 

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

1770 return ','.join(result) 

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

1772 bad_fns: List[str] = [] 

1773 

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

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

1776 if 1: 

1777 return 

1778 code = frame.f_code 

1779 file_name = code.co_filename 

1780 locals_ = frame.f_locals 

1781 name = code.co_name 

1782 full_name = self.get_full_name(locals_, name) 

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

1784 return 

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

1786 d = self.contents_d 

1787 lines = d.get(file_name) 

1788 if not lines: 

1789 print(file_name) 

1790 try: 

1791 with open(file_name) as f: 

1792 s = f.read() 

1793 except Exception: 

1794 if file_name not in self.bad_fns: 

1795 self.bad_fns.append(file_name) 

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

1797 return 

1798 lines = g.splitLines(s) 

1799 d[file_name] = lines 

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

1801 if 0: 

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

1803 else: 

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

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

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

1807 """Trace a return statement.""" 

1808 code = frame.f_code 

1809 fn = code.co_filename 

1810 locals_ = frame.f_locals 

1811 name = code.co_name 

1812 full_name = self.get_full_name(locals_, name) 

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

1814 n = 0 

1815 while frame: 

1816 frame = frame.f_back 

1817 n += 1 

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

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

1820 if name and name == '__init__': 

1821 try: 

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

1823 ret = self.format_ret(ret1) 

1824 except NameError: 

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

1826 else: 

1827 ret = self.format_ret(arg) 

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

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

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

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

1832 try: 

1833 if isinstance(arg, types.GeneratorType): 

1834 ret = '<generator>' 

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

1836 # Clearer w/o f-string. 

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

1838 if len(ret) > 40: 

1839 # Clearer w/o f-string. 

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

1841 elif arg: 

1842 ret = self.show(arg) 

1843 if len(ret) > 40: 

1844 ret = f"\n {ret}" 

1845 else: 

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

1847 except Exception: 

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

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

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

1851 return f" -> {ret}" 

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

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

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

1855 if func in self.ignored_functions: 

1856 return False 

1857 

1858 def ignore_function() -> None: 

1859 if func not in self.ignored_functions: 

1860 self.ignored_functions.append(func) 

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

1862 # 

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

1864 table = ( 

1865 '_deepcopy.*', 

1866 # Unicode primitives. 

1867 'encode\b', 'decode\b', 

1868 # System functions 

1869 '.*__next\b', 

1870 '<frozen>', '<genexpr>', '<listcomp>', 

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

1872 'get\b', 

1873 # String primitives. 

1874 'append\b', 'split\b', 'join\b', 

1875 # File primitives... 

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

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

1878 ) 

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

1880 for z in table: 

1881 if re.match(z, func): 

1882 ignore_function() 

1883 return False 

1884 # 

1885 # Legacy code. 

1886 try: 

1887 enabled, pattern = False, None 

1888 for pattern in patterns: 

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

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

1891 enabled = True 

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

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

1894 enabled = False 

1895 return enabled 

1896 except Exception: 

1897 self.bad_pattern(pattern) 

1898 return False 

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

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

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

1902 full_name = name 

1903 try: 

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

1905 if user_self: 

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

1907 except Exception: 

1908 pass 

1909 return full_name 

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

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

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

1913 

1914 def is_enabled( 

1915 self, 

1916 file_name: str, 

1917 function_name: str, 

1918 patterns: List[str]=None, 

1919 ) -> bool: 

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

1921 # 

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

1923 if not os: 

1924 return False # Shutting down. 

1925 base_name = os.path.basename(file_name) 

1926 if base_name in self.ignored_files: 

1927 return False 

1928 

1929 def ignore_file() -> None: 

1930 if not base_name in self.ignored_files: 

1931 self.ignored_files.append(base_name) 

1932 

1933 def ignore_function() -> None: 

1934 if function_name not in self.ignored_functions: 

1935 self.ignored_functions.append(function_name) 

1936 

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

1938 ignore_file() 

1939 return False 

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

1941 ignore_file() 

1942 return False 

1943 # 

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

1945 table = ( 

1946 '_deepcopy.*', 

1947 # Unicode primitives. 

1948 'encode\b', 'decode\b', 

1949 # System functions 

1950 '.*__next\b', 

1951 '<frozen>', '<genexpr>', '<listcomp>', 

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

1953 'get\b', 

1954 # String primitives. 

1955 'append\b', 'split\b', 'join\b', 

1956 # File primitives... 

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

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

1959 ) 

1960 for z in table: 

1961 if re.match(z, function_name): 

1962 ignore_function() 

1963 return False 

1964 # 

1965 # Legacy code. 

1966 enabled = False 

1967 if patterns is None: 

1968 patterns = self.patterns 

1969 for pattern in patterns: 

1970 try: 

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

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

1973 enabled = True 

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

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

1976 enabled = False 

1977 elif pattern.startswith('+'): 

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

1979 enabled = True 

1980 elif pattern.startswith('-'): 

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

1982 enabled = False 

1983 else: 

1984 self.bad_pattern(pattern) 

1985 except Exception: 

1986 self.bad_pattern(pattern) 

1987 return enabled 

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

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

1990 """Print all accumulated statisitics.""" 

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

1992 if not patterns: 

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

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

1995 d = self.stats.get(fn) 

1996 if self.fn_is_enabled(fn, patterns): 

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

1998 else: 

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

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

2001 if result: 

2002 print('') 

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

2004 parts = fn.split('/') 

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

2006 for key in result: 

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

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

2009 # Modified from pdb.Pdb.set_trace. 

2010 

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

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

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

2014 if frame is None: 

2015 frame = sys._getframe().f_back 

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

2017 self.n = 0 

2018 while frame: 

2019 frame = frame.f_back 

2020 self.n += 1 

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

2022 sys.settrace(self) 

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

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

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

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

2027 self.set_patterns(patterns) 

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

2029 

2030 def pop(self) -> None: 

2031 """Restore the pushed patterns.""" 

2032 if self.pattern_stack: 

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

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

2035 else: 

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

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

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

2039 """Set the patterns in effect.""" 

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

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

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

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

2044 if not item: 

2045 return repr(item) 

2046 if isinstance(item, dict): 

2047 return 'dict' 

2048 if isinstance(item, str): 

2049 s = repr(item) 

2050 if len(s) <= 20: 

2051 return s 

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

2053 return repr(item) 

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

2055 def stop(self) -> None: 

2056 """Stop all tracing.""" 

2057 sys.settrace(None) 

2058 #@-others 

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

2060class TkIDDialog(EmergencyDialog): 

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

2062 

2063 message = ( 

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

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

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

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

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

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

2070 

2071 title = 'Enter Leo id' 

2072 

2073 def __init__(self) -> None: 

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

2075 self.val = '' 

2076 

2077 #@+others 

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

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

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

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

2082 self.okButton() 

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

2084 def createTopFrame(self) -> None: 

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

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

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

2088 self.top.title(self.title) 

2089 self.root.withdraw() 

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

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

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

2093 label.pack(pady=10) 

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

2095 self.entry.pack() 

2096 self.entry.focus_set() 

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

2098 def okButton(self) -> None: 

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

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

2101 self.top.destroy() 

2102 self.top = None 

2103 #@-others 

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

2105class Tracer: 

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

2107 

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

2109 

2110 g.startTracer() 

2111 """ 

2112 #@+others 

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

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

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

2116 # Keys are function names. 

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

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

2119 # Keys are function names. 

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

2121 self.count = 0 

2122 self.inited = False 

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

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

2125 self.trace = trace 

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

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

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

2129 if not frame: 

2130 return '' 

2131 code = frame.f_code 

2132 result = [] 

2133 module = inspect.getmodule(code) 

2134 if module: 

2135 module_name = module.__name__ 

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

2137 result.append('g') 

2138 else: 

2139 tag = 'leo.core.' 

2140 if module_name.startswith(tag): 

2141 module_name = module_name[len(tag) :] 

2142 result.append(module_name) 

2143 try: 

2144 # This can fail during startup. 

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

2146 if self_obj: 

2147 result.append(self_obj.__class__.__name__) 

2148 except Exception: 

2149 pass 

2150 result.append(code.co_name) 

2151 return '.'.join(result) 

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

2153 def report(self) -> None: 

2154 if 0: 

2155 g.pr('\nstack') 

2156 for z in self.stack: 

2157 g.pr(z) 

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

2159 for key in sorted(self.callDict): 

2160 # Print the calling function. 

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

2162 # Print the called functions. 

2163 d = self.callDict.get(key) 

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

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

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

2167 def stop(self) -> None: 

2168 sys.settrace(None) 

2169 self.report() 

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

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

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

2173 n = len(self.stack) 

2174 if event == 'return': 

2175 n = max(0, n - 1) 

2176 pad = '.' * n 

2177 if event == 'call': 

2178 if not self.inited: 

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

2180 self.inited = True 

2181 name = self.computeName(frame.f_back) 

2182 self.updateStats(name) 

2183 self.stack.append(name) 

2184 name = self.computeName(frame) 

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

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

2187 self.updateStats(name) 

2188 self.stack.append(name) 

2189 return self.tracer 

2190 if event == 'return': 

2191 if self.stack: 

2192 name = self.stack.pop() 

2193 if ( 

2194 self.trace and 

2195 self.verbose and 

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

2197 ): 

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

2199 else: 

2200 g.trace('return underflow') 

2201 self.stop() 

2202 return None 

2203 if self.stack: 

2204 return self.tracer 

2205 self.stop() 

2206 return None 

2207 return self.tracer 

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

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

2210 if not self.stack: 

2211 return 

2212 caller = self.stack[-1] 

2213 # d is a dict reprenting the called functions. 

2214 # Keys are called functions, values are counts. 

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

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

2217 self.callDict[caller] = d 

2218 # Update the total counts. 

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

2220 #@-others 

2221 

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

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

2224 sys.settrace(t.tracer) 

2225 return t 

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

2227#@@nobeautify 

2228 

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

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

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

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

2233 

2234class NullObject: 

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

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

2237 if isinstance(ivars, str): 

2238 ivars = [ivars] 

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

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

2241 return self 

2242 def __repr__(self) -> str: 

2243 return "NullObject" 

2244 def __str__(self) -> str: 

2245 return "NullObject" 

2246 # Attribute access... 

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

2248 return None 

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

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

2251 return getattr(self, attr, None) 

2252 return self # Required. 

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

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

2255 object.__setattr__(self, attr, val) 

2256 # Container methods.. 

2257 def __bool__(self) -> bool: 

2258 return False 

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

2260 return False 

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

2262 raise KeyError 

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

2264 pass 

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

2266 return self 

2267 def __len__(self) -> int: 

2268 return 0 

2269 # Iteration methods: 

2270 def __next__(self) -> None: 

2271 raise StopIteration 

2272 

2273 

2274class TracingNullObject: 

2275 """Tracing NullObject.""" 

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

2277 tracing_tags [id(self)] = tag 

2278 if isinstance(ivars, str): 

2279 ivars = [ivars] 

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

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

2282 return self 

2283 def __repr__(self) -> str: 

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

2285 def __str__(self) -> str: 

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

2287 # 

2288 # Attribute access... 

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

2290 return None 

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

2292 null_object_print_attr(id(self), attr) 

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

2294 return getattr(self, attr, None) 

2295 return self # Required. 

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

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

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

2299 object.__setattr__(self, attr, val) 

2300 # 

2301 # All other methods... 

2302 def __bool__(self) -> bool: 

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

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

2305 callers = g.callers(2) 

2306 if not callers.endswith(suppress): 

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

2308 return False 

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

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

2311 return False 

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

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

2314 # pylint doesn't like trailing return None. 

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

2316 g.null_object_print(id(self), '__iter__') 

2317 return self 

2318 def __len__(self) -> int: 

2319 # g.null_object_print(id(self), '__len__') 

2320 return 0 

2321 def __next__(self) -> None: 

2322 g.null_object_print(id(self), '__next__') 

2323 raise StopIteration 

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

2325 g.null_object_print(id(self), '__setitem__') 

2326 # pylint doesn't like trailing return None. 

2327#@+node:ekr.20190330062625.1: *4* g.null_object_print_attr 

2328def null_object_print_attr(id_: int, attr: str) -> None: 

2329 suppress = True 

2330 suppress_callers: List[str] = [] 

2331 suppress_attrs: List[str] = [] 

2332 if suppress: 

2333 #@+<< define suppression lists >> 

2334 #@+node:ekr.20190330072026.1: *5* << define suppression lists >> 

2335 suppress_callers = [ 

2336 'drawNode', 'drawTopTree', 'drawTree', 

2337 'contractItem', 'getCurrentItem', 

2338 'declutter_node', 

2339 'finishCreate', 

2340 'initAfterLoad', 

2341 'show_tips', 

2342 'writeWaitingLog', 

2343 # 'set_focus', 'show_tips', 

2344 ] 

2345 suppress_attrs = [ 

2346 # Leo... 

2347 'c.frame.body.wrapper', 

2348 'c.frame.getIconBar.add', 

2349 'c.frame.log.createTab', 

2350 'c.frame.log.enable', 

2351 'c.frame.log.finishCreate', 

2352 'c.frame.menu.createMenuBar', 

2353 'c.frame.menu.finishCreate', 

2354 # 'c.frame.menu.getMenu', 

2355 'currentItem', 

2356 'dw.leo_master.windowTitle', 

2357 # Pyzo... 

2358 'pyzo.keyMapper.connect', 

2359 'pyzo.keyMapper.keyMappingChanged', 

2360 'pyzo.keyMapper.setShortcut', 

2361 ] 

2362 #@-<< define suppression lists >> 

2363 tag = tracing_tags.get(id_, "<NO TAG>") 

2364 callers = g.callers(3).split(',') 

2365 callers = ','.join(callers[:-1]) 

2366 in_callers = any(z in callers for z in suppress_callers) 

2367 s = f"{tag}.{attr}" 

2368 if suppress: 

2369 # Filter traces. 

2370 if not in_callers and s not in suppress_attrs: 

2371 g.pr(f"{s:40} {callers}") 

2372 else: 

2373 # Print each signature once. No need to filter! 

2374 signature = f"{tag}.{attr}:{callers}" 

2375 if signature not in tracing_signatures: 

2376 tracing_signatures[signature] = True 

2377 g.pr(f"{s:40} {callers}") 

2378#@+node:ekr.20190330072832.1: *4* g.null_object_print 

2379def null_object_print(id_: int, kind: Any, *args: Any) -> None: 

2380 tag = tracing_tags.get(id_, "<NO TAG>") 

2381 callers = g.callers(3).split(',') 

2382 callers = ','.join(callers[:-1]) 

2383 s = f"{kind}.{tag}" 

2384 signature = f"{s}:{callers}" 

2385 if 1: 

2386 # Always print: 

2387 if args: 

2388 args_s = ', '.join([repr(z) for z in args]) 

2389 g.pr(f"{s:40} {callers}\n\t\t\targs: {args_s}") 

2390 else: 

2391 g.pr(f"{s:40} {callers}") 

2392 elif signature not in tracing_signatures: 

2393 # Print each signature once. 

2394 tracing_signatures[signature] = True 

2395 g.pr(f"{s:40} {callers}") 

2396#@+node:ekr.20120129181245.10220: *3* class g.TypedDict 

2397class TypedDict: 

2398 """ 

2399 A class providing additional dictionary-related methods: 

2400 

2401 __init__: Specifies types and the dict's name. 

2402 __repr__: Compatible with g.printObj, based on g.objToString. 

2403 __setitem__: Type checks its arguments. 

2404 __str__: A concise summary of the inner dict. 

2405 add_to_list: A convenience method that adds a value to its key's list. 

2406 name: The dict's name. 

2407 setName: Sets the dict's name, for use by __repr__. 

2408 

2409 Overrides the following standard methods: 

2410 

2411 copy: A thin wrapper for copy.deepcopy. 

2412 get: Returns self.d.get 

2413 items: Returns self.d.items 

2414 keys: Returns self.d.keys 

2415 update: Updates self.d from either a dict or a TypedDict. 

2416 """ 

2417 

2418 def __init__(self, name: str, keyType: Any, valType: Any) -> None: 

2419 self.d: Dict[str, Any] = {} 

2420 self._name = name # For __repr__ only. 

2421 self.keyType = keyType 

2422 self.valType = valType 

2423 #@+others 

2424 #@+node:ekr.20120205022040.17770: *4* td.__repr__ & __str__ 

2425 def __str__(self) -> str: 

2426 """Concise: used by repr.""" 

2427 return ( 

2428 f"<TypedDict name:{self._name} " 

2429 f"keys:{self.keyType.__name__} " 

2430 f"values:{self.valType.__name__} " 

2431 f"len(keys): {len(list(self.keys()))}>" 

2432 ) 

2433 

2434 def __repr__(self) -> str: 

2435 """Suitable for g.printObj""" 

2436 return f"{g.dictToString(self.d)}\n{str(self)}\n" 

2437 #@+node:ekr.20120205022040.17774: *4* td.__setitem__ 

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

2439 """Allow d[key] = val""" 

2440 if key is None: 

2441 g.trace('TypeDict: None is not a valid key', g.callers()) 

2442 return 

2443 self._checkKeyType(key) 

2444 try: 

2445 for z in val: 

2446 self._checkValType(z) 

2447 except TypeError: 

2448 self._checkValType(val) # val is not iterable. 

2449 self.d[key] = val 

2450 #@+node:ekr.20190904052828.1: *4* td.add_to_list 

2451 def add_to_list(self, key: Any, val: Any) -> None: 

2452 """Update the *list*, self.d [key]""" 

2453 if key is None: 

2454 g.trace('TypeDict: None is not a valid key', g.callers()) 

2455 return 

2456 self._checkKeyType(key) 

2457 self._checkValType(val) 

2458 aList = self.d.get(key, []) 

2459 if val not in aList: 

2460 aList.append(val) 

2461 self.d[key] = aList 

2462 #@+node:ekr.20120206134955.10150: *4* td.checking 

2463 def _checkKeyType(self, key: str) -> None: 

2464 if key and key.__class__ != self.keyType: 

2465 self._reportTypeError(key, self.keyType) 

2466 

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

2468 if val.__class__ != self.valType: 

2469 self._reportTypeError(val, self.valType) 

2470 

2471 def _reportTypeError(self, obj: Any, objType: Any) -> str: 

2472 return ( 

2473 f"{self._name}\n" 

2474 f"expected: {obj.__class__.__name__}\n" 

2475 f" got: {objType.__name__}") 

2476 #@+node:ekr.20120223062418.10422: *4* td.copy 

2477 def copy(self, name: str=None) -> Any: 

2478 """Return a new dict with the same contents.""" 

2479 import copy 

2480 return copy.deepcopy(self) 

2481 #@+node:ekr.20120205022040.17771: *4* td.get & keys & values 

2482 def get(self, key: Any, default: Any=None) -> Any: 

2483 return self.d.get(key, default) 

2484 

2485 def items(self) -> Any: 

2486 return self.d.items() 

2487 

2488 def keys(self) -> Any: 

2489 return self.d.keys() 

2490 

2491 def values(self) -> Any: 

2492 return self.d.values() 

2493 #@+node:ekr.20190903181030.1: *4* td.get_setting & get_string_setting 

2494 def get_setting(self, key: str) -> Any: 

2495 key = key.replace('-', '').replace('_', '') 

2496 gs = self.get(key) 

2497 val = gs and gs.val 

2498 return val 

2499 

2500 def get_string_setting(self, key: str) -> Optional[str]: 

2501 val = self.get_setting(key) 

2502 return val if val and isinstance(val, str) else None 

2503 #@+node:ekr.20190904103552.1: *4* td.name & setName 

2504 def name(self) -> str: 

2505 return self._name 

2506 

2507 def setName(self, name: str) -> None: 

2508 self._name = name 

2509 #@+node:ekr.20120205022040.17807: *4* td.update 

2510 def update(self, d: Dict[Any, Any]) -> None: 

2511 """Update self.d from a the appropriate dict.""" 

2512 if isinstance(d, TypedDict): 

2513 self.d.update(d.d) 

2514 else: 

2515 self.d.update(d) 

2516 #@-others 

2517#@+node:ville.20090827174345.9963: *3* class g.UiTypeException & g.assertui 

2518class UiTypeException(Exception): 

2519 pass 

2520 

2521def assertUi(uitype: Any) -> None: 

2522 if not g.app.gui.guiName() == uitype: 

2523 raise UiTypeException 

2524#@+node:ekr.20200219071828.1: *3* class TestLeoGlobals (leoGlobals.py) 

2525class TestLeoGlobals(unittest.TestCase): 

2526 """Tests for leoGlobals.py.""" 

2527 #@+others 

2528 #@+node:ekr.20200219071958.1: *4* test_comment_delims_from_extension 

2529 def test_comment_delims_from_extension(self) -> None: 

2530 

2531 # pylint: disable=import-self 

2532 from leo.core import leoGlobals as leo_g 

2533 from leo.core import leoApp 

2534 leo_g.app = leoApp.LeoApp() 

2535 assert leo_g.comment_delims_from_extension(".py") == ('#', '', '') 

2536 assert leo_g.comment_delims_from_extension(".c") == ('//', '/*', '*/') 

2537 assert leo_g.comment_delims_from_extension(".html") == ('', '<!--', '-->') 

2538 #@+node:ekr.20200219072957.1: *4* test_is_sentinel 

2539 def test_is_sentinel(self) -> None: 

2540 

2541 # pylint: disable=import-self 

2542 from leo.core import leoGlobals as leo_g 

2543 # Python. 

2544 py_delims = leo_g.comment_delims_from_extension('.py') 

2545 assert leo_g.is_sentinel("#@+node", py_delims) 

2546 assert not leo_g.is_sentinel("#comment", py_delims) 

2547 # C. 

2548 c_delims = leo_g.comment_delims_from_extension('.c') 

2549 assert leo_g.is_sentinel("//@+node", c_delims) 

2550 assert not g.is_sentinel("//comment", c_delims) 

2551 # Html. 

2552 html_delims = leo_g.comment_delims_from_extension('.html') 

2553 assert leo_g.is_sentinel("<!--@+node-->", html_delims) 

2554 assert not leo_g.is_sentinel("<!--comment-->", html_delims) 

2555 #@-others 

2556#@+node:ekr.20140904112935.18526: *3* g.isTextWrapper & isTextWidget 

2557def isTextWidget(w: Any) -> bool: 

2558 return g.app.gui.isTextWidget(w) 

2559 

2560def isTextWrapper(w: Any) -> bool: 

2561 return g.app.gui.isTextWrapper(w) 

2562#@+node:ekr.20160518074224.1: *3* class g.LinterTable 

2563class LinterTable(): 

2564 """A class to encapsulate lists of leo modules under test.""" 

2565 

2566 def __init__(self) -> None: 

2567 """Ctor for LinterTable class.""" 

2568 # Define self. relative to leo.core.leoGlobals 

2569 self.loadDir = g.os_path_finalize_join(g.__file__, '..', '..') 

2570 #@+others 

2571 #@+node:ekr.20160518074545.2: *4* commands 

2572 def commands(self) -> List: 

2573 """Return list of all command modules in leo/commands.""" 

2574 pattern = g.os_path_finalize_join(self.loadDir, 'commands', '*.py') 

2575 return self.get_files(pattern) 

2576 #@+node:ekr.20160518074545.3: *4* core 

2577 def core(self) -> List: 

2578 """Return list of all of Leo's core files.""" 

2579 pattern = g.os_path_finalize_join(self.loadDir, 'core', 'leo*.py') 

2580 aList = self.get_files(pattern) 

2581 for fn in ['runLeo.py',]: 

2582 aList.append(g.os_path_finalize_join(self.loadDir, 'core', fn)) 

2583 return sorted(aList) 

2584 #@+node:ekr.20160518074545.4: *4* external 

2585 def external(self) -> List: 

2586 """Return list of files in leo/external""" 

2587 pattern = g.os_path_finalize_join(self.loadDir, 'external', 'leo*.py') 

2588 aList = self.get_files(pattern) 

2589 remove = [ 

2590 'leoSAGlobals.py', 

2591 'leoftsindex.py', 

2592 ] 

2593 remove = [g.os_path_finalize_join(self.loadDir, 'external', fn) for fn in remove] 

2594 return sorted([z for z in aList if z not in remove]) 

2595 #@+node:ekr.20160520093506.1: *4* get_files (LinterTable) 

2596 def get_files(self, pattern: str) -> List: 

2597 """Return the list of absolute file names matching the pattern.""" 

2598 aList = sorted([ 

2599 fn for fn in g.glob_glob(pattern) 

2600 if g.os_path_isfile(fn) and g.shortFileName(fn) != '__init__.py']) 

2601 return aList 

2602 #@+node:ekr.20160518074545.9: *4* get_files_for_scope 

2603 def get_files_for_scope(self, scope: str, fn: str) -> List: 

2604 """Return a list of absolute filenames for external linters.""" 

2605 d = { 

2606 'all': [self.core, self.commands, self.external, self.plugins], 

2607 'commands': [self.commands], 

2608 'core': [self.core, self.commands, self.external, self.gui_plugins], 

2609 'external': [self.external], 

2610 'file': [fn], 

2611 'gui': [self.gui_plugins], 

2612 'modes': [self.modes], 

2613 'plugins': [self.plugins], 

2614 'tests': [self.tests], 

2615 } 

2616 suppress_list = ['freewin.py',] 

2617 functions = d.get(scope) 

2618 paths = [] 

2619 if functions: 

2620 for func in functions: 

2621 files = [func] if isinstance(func, str) else func() 

2622 # Bug fix: 2016/10/15 

2623 for fn in files: 

2624 fn = g.os_path_abspath(fn) 

2625 if g.shortFileName(fn) in suppress_list: 

2626 print(f"\npylint-leo: skip {fn}") 

2627 continue 

2628 if g.os_path_exists(fn): 

2629 if g.os_path_isfile(fn): 

2630 paths.append(fn) 

2631 else: 

2632 print(f"does not exist: {fn}") 

2633 paths = sorted(set(paths)) 

2634 return paths 

2635 print('LinterTable.get_table: bad scope', scope) 

2636 return [] 

2637 #@+node:ekr.20160518074545.5: *4* gui_plugins 

2638 def gui_plugins(self) -> List: 

2639 """Return list of all of Leo's gui-related files.""" 

2640 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', 'qt_*.py') 

2641 aList = self.get_files(pattern) 

2642 # These are not included, because they don't start with 'qt_': 

2643 add = ['free_layout.py', 'nested_splitter.py',] 

2644 remove = [ 

2645 'qt_main.py', # auto-generated file. 

2646 ] 

2647 for fn in add: 

2648 aList.append(g.os_path_finalize_join(self.loadDir, 'plugins', fn)) 

2649 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove] 

2650 return sorted(set([z for z in aList if z not in remove])) 

2651 #@+node:ekr.20160518074545.6: *4* modes 

2652 def modes(self) -> List: 

2653 """Return list of all files in leo/modes""" 

2654 pattern = g.os_path_finalize_join(self.loadDir, 'modes', '*.py') 

2655 return self.get_files(pattern) 

2656 #@+node:ekr.20160518074545.8: *4* plugins (LinterTable) 

2657 def plugins(self) -> List: 

2658 """Return a list of all important plugins.""" 

2659 aList = [] 

2660 for theDir in ('', 'importers', 'writers'): 

2661 pattern = g.os_path_finalize_join(self.loadDir, 'plugins', theDir, '*.py') 

2662 aList.extend(self.get_files(pattern)) 

2663 # Don't use get_files here. 

2664 # for fn in g.glob_glob(pattern): 

2665 # sfn = g.shortFileName(fn) 

2666 # if sfn != '__init__.py': 

2667 # sfn = os.sep.join([theDir, sfn]) if theDir else sfn 

2668 # aList.append(sfn) 

2669 remove = [ 

2670 # 2016/05/20: *do* include gui-related plugins. 

2671 # This allows the -a option not to doubly-include gui-related plugins. 

2672 # 'free_layout.py', # Gui-related. 

2673 # 'nested_splitter.py', # Gui-related. 

2674 'gtkDialogs.py', # Many errors, not important. 

2675 'leofts.py', # Not (yet) in leoPlugins.leo. 

2676 'qtGui.py', # Dummy file 

2677 'qt_main.py', # Created automatically. 

2678 'viewrendered2.py', # To be removed. 

2679 'rst3.py', # Obsolete 

2680 ] 

2681 remove = [g.os_path_finalize_join(self.loadDir, 'plugins', fn) for fn in remove] 

2682 aList = sorted([z for z in aList if z not in remove]) 

2683 return sorted(set(aList)) 

2684 #@+node:ekr.20211115103929.1: *4* tests (LinterTable) 

2685 def tests(self) -> List: 

2686 """Return list of files in leo/unittests""" 

2687 aList = [] 

2688 for theDir in ('', 'commands', 'core', 'plugins'): 

2689 pattern = g.os_path_finalize_join(self.loadDir, 'unittests', theDir, '*.py') 

2690 aList.extend(self.get_files(pattern)) 

2691 remove = [ 

2692 'py3_test_grammar.py', 

2693 ] 

2694 remove = [g.os_path_finalize_join(self.loadDir, 'unittests', fn) for fn in remove] 

2695 return sorted([z for z in aList if z not in remove]) 

2696 #@-others 

2697#@+node:ekr.20140711071454.17649: ** g.Debugging, GC, Stats & Timing 

2698#@+node:ekr.20031218072017.3104: *3* g.Debugging 

2699#@+node:ekr.20180415144534.1: *4* g.assert_is 

2700def assert_is(obj: Any, list_or_class: Any, warn: bool=True) -> bool: 

2701 

2702 if warn: 

2703 ok = isinstance(obj, list_or_class) 

2704 if not ok: 

2705 g.es_print( 

2706 f"can not happen. {obj !r}: " 

2707 f"expected {list_or_class}, " 

2708 f"got: {obj.__class__.__name__}") 

2709 g.es_print(g.callers()) 

2710 return ok 

2711 ok = isinstance(obj, list_or_class) 

2712 assert ok, (obj, obj.__class__.__name__, g.callers()) 

2713 return ok 

2714#@+node:ekr.20180420081530.1: *4* g._assert 

2715def _assert(condition: Any, show_callers: bool=True) -> bool: 

2716 """A safer alternative to a bare assert.""" 

2717 if g.unitTesting: 

2718 assert condition 

2719 return True 

2720 ok = bool(condition) 

2721 if ok: 

2722 return True 

2723 g.es_print('\n===== g._assert failed =====\n') 

2724 if show_callers: 

2725 g.es_print(g.callers()) 

2726 return False 

2727#@+node:ekr.20051023083258: *4* g.callers & g.caller & _callerName 

2728def callers(n: int=4, count: int=0, excludeCaller: bool=True, verbose: bool=False) -> str: 

2729 """ 

2730 Return a string containing a comma-separated list of the callers 

2731 of the function that called g.callerList. 

2732 

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

2734 

2735 If the `verbose` keyword is True, return a list separated by newlines. 

2736 """ 

2737 # Be careful to call g._callerName with smaller values of i first: 

2738 # sys._getframe throws ValueError if there are less than i entries. 

2739 result = [] 

2740 i = 3 if excludeCaller else 2 

2741 while 1: 

2742 s = _callerName(n=i, verbose=verbose) 

2743 if s: 

2744 result.append(s) 

2745 if not s or len(result) >= n: 

2746 break 

2747 i += 1 

2748 result.reverse() 

2749 if count > 0: 

2750 result = result[:count] 

2751 if verbose: 

2752 return ''.join([f"\n {z}" for z in result]) 

2753 return ','.join(result) 

2754#@+node:ekr.20031218072017.3107: *5* g._callerName 

2755def _callerName(n: int, verbose: bool=False) -> str: 

2756 try: 

2757 # get the function name from the call stack. 

2758 f1 = sys._getframe(n) # The stack frame, n levels up. 

2759 code1 = f1.f_code # The code object 

2760 sfn = shortFilename(code1.co_filename) # The file name. 

2761 locals_ = f1.f_locals # The local namespace. 

2762 name = code1.co_name 

2763 line = code1.co_firstlineno 

2764 if verbose: 

2765 obj = locals_.get('self') 

2766 full_name = f"{obj.__class__.__name__}.{name}" if obj else name 

2767 return f"line {line:4} {sfn:>30} {full_name}" 

2768 return name 

2769 except ValueError: 

2770 return '' 

2771 # The stack is not deep enough OR 

2772 # sys._getframe does not exist on this platform. 

2773 except Exception: 

2774 es_exception() 

2775 return '' # "<no caller name>" 

2776#@+node:ekr.20180328170441.1: *5* g.caller 

2777def caller(i: int=1) -> str: 

2778 """Return the caller name i levels up the stack.""" 

2779 return g.callers(i + 1).split(',')[0] 

2780#@+node:ekr.20031218072017.3109: *4* g.dump 

2781def dump(s: str) -> str: 

2782 out = "" 

2783 for i in s: 

2784 out += str(ord(i)) + "," 

2785 return out 

2786 

2787def oldDump(s: str) -> str: 

2788 out = "" 

2789 for i in s: 

2790 if i == '\n': 

2791 out += "[" 

2792 out += "n" 

2793 out += "]" 

2794 if i == '\t': 

2795 out += "[" 

2796 out += "t" 

2797 out += "]" 

2798 elif i == ' ': 

2799 out += "[" 

2800 out += " " 

2801 out += "]" 

2802 else: 

2803 out += i 

2804 return out 

2805#@+node:ekr.20210904114446.1: *4* g.dump_tree & g.tree_to_string 

2806def dump_tree(c: Cmdr, dump_body: bool=False, msg: str=None) -> None: 

2807 if msg: 

2808 print(msg.rstrip()) 

2809 else: 

2810 print('') 

2811 for p in c.all_positions(): 

2812 print(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}") 

2813 if dump_body: 

2814 for z in g.splitLines(p.b): 

2815 print(z.rstrip()) 

2816 

2817def tree_to_string(c: Cmdr, dump_body: bool=False, msg: str=None) -> str: 

2818 result = ['\n'] 

2819 if msg: 

2820 result.append(msg) 

2821 for p in c.all_positions(): 

2822 result.append(f"clone? {int(p.isCloned())} {' '*p.level()} {p.h}") 

2823 if dump_body: 

2824 for z in g.splitLines(p.b): 

2825 result.append(z.rstrip()) 

2826 return '\n'.join(result) 

2827#@+node:ekr.20150227102835.8: *4* g.dump_encoded_string 

2828def dump_encoded_string(encoding: str, s: str) -> None: 

2829 """Dump s, assumed to be an encoded string.""" 

2830 # Can't use g.trace here: it calls this function! 

2831 print(f"dump_encoded_string: {g.callers()}") 

2832 print(f"dump_encoded_string: encoding {encoding}\n") 

2833 print(s) 

2834 in_comment = False 

2835 for ch in s: 

2836 if ch == '#': 

2837 in_comment = True 

2838 elif not in_comment: 

2839 print(f"{ord(ch):02x} {repr(ch)}") 

2840 elif ch == '\n': 

2841 in_comment = False 

2842#@+node:ekr.20031218072017.1317: *4* g.file/module/plugin_date 

2843def module_date(mod: Any, format: str=None) -> str: 

2844 theFile = g.os_path_join(app.loadDir, mod.__file__) 

2845 root, ext = g.os_path_splitext(theFile) 

2846 return g.file_date(root + ".py", format=format) 

2847 

2848def plugin_date(plugin_mod: Any, format: str=None) -> str: 

2849 theFile = g.os_path_join(app.loadDir, "..", "plugins", plugin_mod.__file__) 

2850 root, ext = g.os_path_splitext(theFile) 

2851 return g.file_date(root + ".py", format=str) 

2852 

2853def file_date(theFile: Any, format: str=None) -> str: 

2854 if theFile and g.os_path_exists(theFile): 

2855 try: 

2856 n = g.os_path_getmtime(theFile) 

2857 if format is None: 

2858 format = "%m/%d/%y %H:%M:%S" 

2859 return time.strftime(format, time.gmtime(n)) 

2860 except(ImportError, NameError): 

2861 pass # Time module is platform dependent. 

2862 return "" 

2863#@+node:ekr.20031218072017.3127: *4* g.get_line & get_line__after 

2864# Very useful for tracing. 

2865 

2866def get_line(s: str, i: int) -> str: 

2867 nl = "" 

2868 if g.is_nl(s, i): 

2869 i = g.skip_nl(s, i) 

2870 nl = "[nl]" 

2871 j = g.find_line_start(s, i) 

2872 k = g.skip_to_end_of_line(s, i) 

2873 return nl + s[j:k] 

2874 

2875# Important: getLine is a completely different function. 

2876# getLine = get_line 

2877 

2878def get_line_after(s: str, i: int) -> str: 

2879 nl = "" 

2880 if g.is_nl(s, i): 

2881 i = g.skip_nl(s, i) 

2882 nl = "[nl]" 

2883 k = g.skip_to_end_of_line(s, i) 

2884 return nl + s[i:k] 

2885 

2886getLineAfter = get_line_after 

2887#@+node:ekr.20080729142651.1: *4* g.getIvarsDict and checkUnchangedIvars 

2888def getIvarsDict(obj: Any) -> Dict[str, Any]: 

2889 """Return a dictionary of ivars:values for non-methods of obj.""" 

2890 d: Dict[str, Any] = dict( 

2891 [[key, getattr(obj, key)] for key in dir(obj) # type:ignore 

2892 if not isinstance(getattr(obj, key), types.MethodType)]) 

2893 return d 

2894 

2895def checkUnchangedIvars( 

2896 obj: Any, 

2897 d: Dict[str, Any], 

2898 exceptions: Sequence[str]=None, 

2899) -> bool: 

2900 if not exceptions: 

2901 exceptions = [] 

2902 ok = True 

2903 for key in d: 

2904 if key not in exceptions: 

2905 if getattr(obj, key) != d.get(key): 

2906 g.trace( 

2907 f"changed ivar: {key} " 

2908 f"old: {repr(d.get(key))} " 

2909 f"new: {repr(getattr(obj, key))}") 

2910 ok = False 

2911 return ok 

2912#@+node:ekr.20031218072017.3128: *4* g.pause 

2913def pause(s: str) -> None: 

2914 g.pr(s) 

2915 i = 0 

2916 while i < 1000 * 1000: 

2917 i += 1 

2918#@+node:ekr.20041105091148: *4* g.pdb 

2919def pdb(message: str='') -> None: 

2920 """Fall into pdb.""" 

2921 import pdb # Required: we have just defined pdb as a function! 

2922 if app and not app.useIpython: 

2923 try: 

2924 from leo.core.leoQt import QtCore 

2925 QtCore.pyqtRemoveInputHook() 

2926 except Exception: 

2927 pass 

2928 if message: 

2929 print(message) 

2930 # pylint: disable=forgotten-debug-statement 

2931 pdb.set_trace() 

2932#@+node:ekr.20041224080039: *4* g.dictToString 

2933def dictToString(d: Dict[str, str], indent: str='', tag: str=None) -> str: 

2934 """Pretty print a Python dict to a string.""" 

2935 # pylint: disable=unnecessary-lambda 

2936 if not d: 

2937 return '{}' 

2938 result = ['{\n'] 

2939 indent2 = indent + ' ' * 4 

2940 n = 2 + len(indent) + max([len(repr(z)) for z in d.keys()]) 

2941 for i, key in enumerate(sorted(d, key=lambda z: repr(z))): 

2942 pad = ' ' * max(0, (n - len(repr(key)))) 

2943 result.append(f"{pad}{key}:") 

2944 result.append(objToString(d.get(key), indent=indent2)) 

2945 if i + 1 < len(d.keys()): 

2946 result.append(',') 

2947 result.append('\n') 

2948 result.append(indent + '}') 

2949 s = ''.join(result) 

2950 return f"{tag}...\n{s}\n" if tag else s 

2951#@+node:ekr.20041126060136: *4* g.listToString 

2952def listToString(obj: Any, indent: str='', tag: str=None) -> str: 

2953 """Pretty print a Python list to a string.""" 

2954 if not obj: 

2955 return '[]' 

2956 result = ['['] 

2957 indent2 = indent + ' ' * 4 

2958 # I prefer not to compress lists. 

2959 for i, obj2 in enumerate(obj): 

2960 result.append('\n' + indent2) 

2961 result.append(objToString(obj2, indent=indent2)) 

2962 if i + 1 < len(obj) > 1: 

2963 result.append(',') 

2964 else: 

2965 result.append('\n' + indent) 

2966 result.append(']') 

2967 s = ''.join(result) 

2968 return f"{tag}...\n{s}\n" if tag else s 

2969#@+node:ekr.20050819064157: *4* g.objToSTring & g.toString 

2970def objToString(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> str: 

2971 """Pretty print any Python object to a string.""" 

2972 # pylint: disable=undefined-loop-variable 

2973 # Looks like a a pylint bug. 

2974 # 

2975 # Compute s. 

2976 if isinstance(obj, dict): 

2977 s = dictToString(obj, indent=indent) 

2978 elif isinstance(obj, list): 

2979 s = listToString(obj, indent=indent) 

2980 elif isinstance(obj, tuple): 

2981 s = tupleToString(obj, indent=indent) 

2982 elif isinstance(obj, str): 

2983 # Print multi-line strings as lists. 

2984 s = obj 

2985 lines = g.splitLines(s) 

2986 if len(lines) > 1: 

2987 s = listToString(lines, indent=indent) 

2988 else: 

2989 s = repr(s) 

2990 else: 

2991 s = repr(obj) 

2992 # 

2993 # Compute the return value. 

2994 if printCaller and tag: 

2995 prefix = f"{g.caller()}: {tag}" 

2996 elif printCaller or tag: 

2997 prefix = g.caller() if printCaller else tag 

2998 else: 

2999 prefix = '' 

3000 if prefix: 

3001 sep = '\n' if '\n' in s else ' ' 

3002 return f"{prefix}:{sep}{s}" 

3003 return s 

3004 

3005toString = objToString 

3006#@+node:ekr.20140401054342.16844: *4* g.run_pylint 

3007def run_pylint( 

3008 fn: str, # Path to file under test. 

3009 rc: str, # Path to settings file. 

3010 dots: bool=True, # Show level dots in Sherlock traces. 

3011 patterns: List[str]=None, # List of Sherlock trace patterns. 

3012 sherlock: bool=False, # Enable Sherlock tracing. 

3013 show_return: bool=True, # Show returns in Sherlock traces. 

3014 stats_patterns: bool=None, # Patterns for Sherlock statistics. 

3015 verbose: bool=True, # Show filenames in Sherlock traces. 

3016) -> None: 

3017 """ 

3018 Run pylint with the given args, with Sherlock tracing if requested. 

3019 

3020 **Do not assume g.app exists.** 

3021 

3022 run() in pylint-leo.py and PylintCommand.run_pylint *optionally* call this function. 

3023 """ 

3024 try: 

3025 from pylint import lint #type:ignore 

3026 except ImportError: 

3027 g.trace('can not import pylint') 

3028 return 

3029 if not g.os_path_exists(fn): 

3030 g.trace('does not exist:', fn) 

3031 return 

3032 if not g.os_path_exists(rc): 

3033 g.trace('does not exist', rc) 

3034 return 

3035 args = [f"--rcfile={rc}"] 

3036 # Prints error number. 

3037 # args.append('--msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}') 

3038 args.append(fn) 

3039 if sherlock: 

3040 sherlock = g.SherlockTracer( 

3041 dots=dots, 

3042 show_return=show_return, 

3043 verbose=True, # verbose: show filenames. 

3044 patterns=patterns or [], 

3045 ) 

3046 try: 

3047 sherlock.run() 

3048 lint.Run(args) 

3049 finally: 

3050 sherlock.stop() 

3051 sherlock.print_stats(patterns=stats_patterns or []) 

3052 else: 

3053 # print('run_pylint: %s' % g.shortFileName(fn)) 

3054 try: 

3055 lint.Run(args) # does sys.exit 

3056 finally: 

3057 # Printing does not work well here. 

3058 # When not waiting, printing from severl process can be interspersed. 

3059 pass 

3060#@+node:ekr.20120912153732.10597: *4* g.wait 

3061def sleep(n: float) -> None: 

3062 """Wait about n milliseconds.""" 

3063 from time import sleep # type:ignore 

3064 sleep(n) # type:ignore 

3065#@+node:ekr.20171023140544.1: *4* g.printObj & aliases 

3066def printObj(obj: Any, indent: str='', printCaller: bool=False, tag: str=None) -> None: 

3067 """Pretty print any Python object using g.pr.""" 

3068 g.pr(objToString(obj, indent=indent, printCaller=printCaller, tag=tag)) 

3069 

3070printDict = printObj 

3071printList = printObj 

3072printTuple = printObj 

3073#@+node:ekr.20171023110057.1: *4* g.tupleToString 

3074def tupleToString(obj: Any, indent: str='', tag: str=None) -> str: 

3075 """Pretty print a Python tuple to a string.""" 

3076 if not obj: 

3077 return '(),' 

3078 result = ['('] 

3079 indent2 = indent + ' ' * 4 

3080 for i, obj2 in enumerate(obj): 

3081 if len(obj) > 1: 

3082 result.append('\n' + indent2) 

3083 result.append(objToString(obj2, indent=indent2)) 

3084 if len(obj) == 1 or i + 1 < len(obj): 

3085 result.append(',') 

3086 elif len(obj) > 1: 

3087 result.append('\n' + indent) 

3088 result.append(')') 

3089 s = ''.join(result) 

3090 return f"{tag}...\n{s}\n" if tag else s 

3091#@+node:ekr.20031218072017.1588: *3* g.Garbage Collection 

3092#@+node:ekr.20031218072017.1589: *4* g.clearAllIvars 

3093def clearAllIvars(o: Any) -> None: 

3094 """Clear all ivars of o, a member of some class.""" 

3095 if o: 

3096 o.__dict__.clear() 

3097#@+node:ekr.20060127162818: *4* g.enable_gc_debug 

3098def enable_gc_debug() -> None: 

3099 

3100 gc.set_debug( 

3101 gc.DEBUG_STATS | # prints statistics. 

3102 gc.DEBUG_LEAK | # Same as all below. 

3103 gc.DEBUG_COLLECTABLE | 

3104 gc.DEBUG_UNCOLLECTABLE | 

3105 # gc.DEBUG_INSTANCES | 

3106 # gc.DEBUG_OBJECTS | 

3107 gc.DEBUG_SAVEALL) 

3108#@+node:ekr.20031218072017.1592: *4* g.printGc 

3109# Formerly called from unit tests. 

3110 

3111def printGc() -> None: 

3112 """Called from trace_gc_plugin.""" 

3113 g.printGcSummary() 

3114 g.printGcObjects() 

3115 g.printGcRefs() 

3116#@+node:ekr.20060127164729.1: *4* g.printGcObjects 

3117lastObjectCount = 0 

3118 

3119def printGcObjects() -> int: 

3120 """Print a summary of GC statistics.""" 

3121 global lastObjectCount 

3122 n = len(gc.garbage) 

3123 n2 = len(gc.get_objects()) 

3124 delta = n2 - lastObjectCount 

3125 print('-' * 30) 

3126 print(f"garbage: {n}") 

3127 print(f"{delta:6d} = {n2:7d} totals") 

3128 # print number of each type of object. 

3129 d: Dict[str, int] = {} 

3130 count = 0 

3131 for obj in gc.get_objects(): 

3132 key = str(type(obj)) 

3133 n = d.get(key, 0) 

3134 d[key] = n + 1 

3135 count += 1 

3136 print(f"{count:7} objects...") 

3137 # Invert the dict. 

3138 d2: Dict[int, str] = {v: k for k, v in d.items()} 

3139 for key in reversed(sorted(d2.keys())): # type:ignore 

3140 val = d2.get(key) # type:ignore 

3141 print(f"{key:7} {val}") 

3142 lastObjectCount = count 

3143 return delta 

3144#@+node:ekr.20031218072017.1593: *4* g.printGcRefs 

3145def printGcRefs() -> None: 

3146 

3147 refs = gc.get_referrers(app.windowList[0]) 

3148 print(f"{len(refs):d} referers") 

3149#@+node:ekr.20060205043324.1: *4* g.printGcSummary 

3150def printGcSummary() -> None: 

3151 

3152 g.enable_gc_debug() 

3153 try: 

3154 n = len(gc.garbage) 

3155 n2 = len(gc.get_objects()) 

3156 s = f"printGCSummary: garbage: {n}, objects: {n2}" 

3157 print(s) 

3158 except Exception: 

3159 traceback.print_exc() 

3160#@+node:ekr.20180528151850.1: *3* g.printTimes 

3161def printTimes(times: List) -> None: 

3162 """ 

3163 Print the differences in the times array. 

3164 

3165 times: an array of times (calls to time.process_time()). 

3166 """ 

3167 for n, junk in enumerate(times[:-1]): 

3168 t = times[n + 1] - times[n] 

3169 if t > 0.1: 

3170 g.trace(f"*** {n} {t:5.4f} sec.") 

3171#@+node:ekr.20031218072017.3133: *3* g.Statistics 

3172#@+node:ekr.20031218072017.3134: *4* g.clearStats 

3173def clearStats() -> None: 

3174 

3175 g.app.statsDict = {} 

3176#@+node:ekr.20031218072017.3135: *4* g.printStats 

3177@command('show-stats') 

3178def printStats(event: Any=None, name: str=None) -> None: 

3179 """ 

3180 Print all gathered statistics. 

3181 

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

3183 

3184 if not g.app.statsLockout: 

3185 g.app.statsLockout = True 

3186 try: 

3187 d = g.app.statsDict 

3188 key = 'g.isUnicode:' + g.callers() 

3189 d [key] = d.get(key, 0) + 1 

3190 finally: 

3191 g.app.statsLockout = False 

3192 """ 

3193 if name: 

3194 if not isinstance(name, str): 

3195 name = repr(name) 

3196 else: 

3197 # Get caller name 2 levels back. 

3198 name = g._callerName(n=2) 

3199 # Print the stats, organized by number of calls. 

3200 d = g.app.statsDict 

3201 print('g.app.statsDict...') 

3202 for key in reversed(sorted(d)): 

3203 print(f"{key:7} {d.get(key)}") 

3204#@+node:ekr.20031218072017.3136: *4* g.stat 

3205def stat(name: str=None) -> None: 

3206 """Increments the statistic for name in g.app.statsDict 

3207 The caller's name is used by default. 

3208 """ 

3209 d = g.app.statsDict 

3210 if name: 

3211 if not isinstance(name, str): 

3212 name = repr(name) 

3213 else: 

3214 name = g._callerName(n=2) # Get caller name 2 levels back. 

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

3216#@+node:ekr.20031218072017.3137: *3* g.Timing 

3217def getTime() -> float: 

3218 return time.time() 

3219 

3220def esDiffTime(message: str, start: float) -> float: 

3221 delta = time.time() - start 

3222 g.es('', f"{message} {delta:5.2f} sec.") 

3223 return time.time() 

3224 

3225def printDiffTime(message: str, start: float) -> float: 

3226 delta = time.time() - start 

3227 g.pr(f"{message} {delta:5.2f} sec.") 

3228 return time.time() 

3229 

3230def timeSince(start: float) -> str: 

3231 return f"{time.time()-start:5.2f} sec." 

3232#@+node:ekr.20031218072017.1380: ** g.Directives 

3233# Weird pylint bug, activated by TestLeoGlobals class. 

3234# Disabling this will be safe, because pyflakes will still warn about true redefinitions 

3235# pylint: disable=function-redefined 

3236#@+node:EKR.20040504150046.4: *3* g.comment_delims_from_extension 

3237def comment_delims_from_extension(filename: str) -> Tuple[str, str, str]: 

3238 """ 

3239 Return the comment delims corresponding to the filename's extension. 

3240 """ 

3241 if filename.startswith('.'): 

3242 root, ext = None, filename 

3243 else: 

3244 root, ext = os.path.splitext(filename) 

3245 if ext == '.tmp': 

3246 root, ext = os.path.splitext(root) 

3247 language = g.app.extension_dict.get(ext[1:]) 

3248 if ext: 

3249 return g.set_delims_from_language(language) 

3250 g.trace( 

3251 f"unknown extension: {ext!r}, " 

3252 f"filename: {filename!r}, " 

3253 f"root: {root!r}") 

3254 return '', '', '' 

3255#@+node:ekr.20170201150505.1: *3* g.findAllValidLanguageDirectives 

3256def findAllValidLanguageDirectives(s: str) -> List: 

3257 """Return list of all valid @language directives in p.b""" 

3258 if not s.strip(): 

3259 return [] 

3260 languages = set() 

3261 for m in g.g_language_pat.finditer(s): 

3262 language = m.group(1) 

3263 if g.isValidLanguage(language): 

3264 languages.add(language) 

3265 return list(sorted(languages)) 

3266#@+node:ekr.20090214075058.8: *3* g.findAtTabWidthDirectives (must be fast) 

3267def findTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[str]: 

3268 """Return the language in effect at position p.""" 

3269 if c is None: 

3270 return None # c may be None for testing. 

3271 w = None 

3272 # 2009/10/02: no need for copy arg to iter 

3273 for p in p.self_and_parents(copy=False): 

3274 if w: 

3275 break 

3276 for s in p.h, p.b: 

3277 if w: 

3278 break 

3279 anIter = g_tabwidth_pat.finditer(s) 

3280 for m in anIter: 

3281 word = m.group(0) 

3282 i = m.start(0) 

3283 j = g.skip_ws(s, i + len(word)) 

3284 junk, w = g.skip_long(s, j) 

3285 if w == 0: 

3286 w = None 

3287 return w 

3288#@+node:ekr.20170127142001.5: *3* g.findFirstAtLanguageDirective 

3289def findFirstValidAtLanguageDirective(s: str) -> Optional[str]: 

3290 """Return the first *valid* @language directive ins.""" 

3291 if not s.strip(): 

3292 return None 

3293 for m in g.g_language_pat.finditer(s): 

3294 language = m.group(1) 

3295 if g.isValidLanguage(language): 

3296 return language 

3297 return None 

3298#@+node:ekr.20090214075058.6: *3* g.findLanguageDirectives (must be fast) 

3299def findLanguageDirectives(c: Cmdr, p: Pos) -> Optional[str]: 

3300 """Return the language in effect at position p.""" 

3301 if c is None or p is None: 

3302 return None # c may be None for testing. 

3303 

3304 v0 = p.v 

3305 

3306 def find_language(p_or_v: Any) -> Optional[str]: 

3307 for s in p_or_v.h, p_or_v.b: 

3308 for m in g_language_pat.finditer(s): 

3309 language = m.group(1) 

3310 if g.isValidLanguage(language): 

3311 return language 

3312 return None 

3313 

3314 # First, search up the tree. 

3315 for p in p.self_and_parents(copy=False): 

3316 language = find_language(p) 

3317 if language: 

3318 return language 

3319 # #1625: Second, expand the search for cloned nodes. 

3320 seen = [] # vnodes that have already been searched. 

3321 parents = v0.parents[:] # vnodes whose ancestors are to be searched. 

3322 while parents: 

3323 parent_v = parents.pop() 

3324 if parent_v in seen: 

3325 continue 

3326 seen.append(parent_v) 

3327 language = find_language(parent_v) 

3328 if language: 

3329 return language 

3330 for grand_parent_v in parent_v.parents: 

3331 if grand_parent_v not in seen: 

3332 parents.append(grand_parent_v) 

3333 # Finally, fall back to the defaults. 

3334 return c.target_language.lower() if c.target_language else 'python' 

3335#@+node:ekr.20031218072017.1385: *3* g.findReference 

3336# Called from the syntax coloring method that colorizes section references. 

3337# Also called from write at.putRefAt. 

3338 

3339def findReference(name: str, root: Pos) -> Optional[Pos]: 

3340 """Return the position containing the section definition for name.""" 

3341 for p in root.subtree(copy=False): 

3342 assert p != root 

3343 if p.matchHeadline(name) and not p.isAtIgnoreNode(): 

3344 return p.copy() 

3345 return None 

3346#@+node:ekr.20090214075058.9: *3* g.get_directives_dict (must be fast) 

3347# The caller passes [root_node] or None as the second arg. 

3348# This allows us to distinguish between None and [None]. 

3349 

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

3351 """ 

3352 Scan p for Leo directives found in globalDirectiveList. 

3353 

3354 Returns a dict containing the stripped remainder of the line 

3355 following the first occurrence of each recognized directive 

3356 """ 

3357 if root: 

3358 root_node = root[0] 

3359 d = {} 

3360 # 

3361 # #1688: legacy: Always compute the pattern. 

3362 # g.directives_pat is updated whenever loading a plugin. 

3363 # 

3364 # The headline has higher precedence because it is more visible. 

3365 for kind, s in (('head', p.h), ('body', p.b)): 

3366 anIter = g.directives_pat.finditer(s) 

3367 for m in anIter: 

3368 word = m.group(1).strip() 

3369 i = m.start(1) 

3370 if word in d: 

3371 continue 

3372 j = i + len(word) 

3373 if j < len(s) and s[j] not in ' \t\n': 

3374 continue 

3375 # Not a valid directive: just ignore it. 

3376 # A unit test tests that @path:any is invalid. 

3377 k = g.skip_line(s, j) 

3378 val = s[j:k].strip() 

3379 d[word] = val 

3380 if root: 

3381 anIter = g_noweb_root.finditer(p.b) 

3382 for m in anIter: 

3383 if root_node: 

3384 d["root"] = 0 # value not immportant 

3385 else: 

3386 g.es(f'{g.angleBrackets("*")} may only occur in a topmost node (i.e., without a parent)') 

3387 break 

3388 return d 

3389#@+node:ekr.20080827175609.1: *3* g.get_directives_dict_list (must be fast) 

3390def get_directives_dict_list(p: Pos) -> List[Dict]: 

3391 """Scans p and all its ancestors for directives. 

3392 

3393 Returns a list of dicts containing pointers to 

3394 the start of each directive""" 

3395 result = [] 

3396 p1 = p.copy() 

3397 for p in p1.self_and_parents(copy=False): 

3398 # No copy necessary: g.get_directives_dict does not change p. 

3399 root = None if p.hasParent() else [p] 

3400 result.append(g.get_directives_dict(p, root=root)) 

3401 return result 

3402#@+node:ekr.20111010082822.15545: *3* g.getLanguageFromAncestorAtFileNode 

3403def getLanguageFromAncestorAtFileNode(p: Pos) -> Optional[str]: 

3404 """ 

3405 Return the language in effect at node p. 

3406  

3407 1. Use an unambiguous @language directive in p itself. 

3408 2. Search p's "extended parents" for an @<file> node. 

3409 3. Search p's "extended parents" for an unambiguous @language directive. 

3410 """ 

3411 v0 = p.v 

3412 seen: Set[VNode] 

3413 

3414 # The same generator as in v.setAllAncestorAtFileNodesDirty. 

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

3416 # Modified by EKR. 

3417 

3418 def v_and_parents(v: "VNode") -> Generator: 

3419 if v in seen: 

3420 return 

3421 seen.add(v) 

3422 yield v 

3423 for parent_v in v.parents: 

3424 if parent_v not in seen: 

3425 yield from v_and_parents(parent_v) 

3426 

3427 def find_language(v: "VNode", phase: int) -> Optional[str]: 

3428 """ 

3429 A helper for all searches. 

3430 Phase one searches only @<file> nodes. 

3431 """ 

3432 if phase == 1 and not v.isAnyAtFileNode(): 

3433 return None 

3434 # #1693: Scan v.b for an *unambiguous* @language directive. 

3435 languages = g.findAllValidLanguageDirectives(v.b) 

3436 if len(languages) == 1: # An unambiguous language 

3437 return languages[0] 

3438 if v.isAnyAtFileNode(): 

3439 # Use the file's extension. 

3440 name = v.anyAtFileNodeName() 

3441 junk, ext = g.os_path_splitext(name) 

3442 ext = ext[1:] # strip the leading period. 

3443 language = g.app.extension_dict.get(ext) 

3444 if g.isValidLanguage(language): 

3445 return language 

3446 return None 

3447 

3448 # First, see if p contains any @language directive. 

3449 language = g.findFirstValidAtLanguageDirective(p.b) 

3450 if language: 

3451 return language 

3452 # 

3453 # Phase 1: search only @<file> nodes: #2308. 

3454 # Phase 2: search all nodes. 

3455 for phase in (1, 2): 

3456 # Search direct parents. 

3457 for p2 in p.self_and_parents(copy=False): 

3458 language = find_language(p2.v, phase) 

3459 if language: 

3460 return language 

3461 # Search all extended parents. 

3462 seen = set([v0.context.hiddenRootNode]) 

3463 for v in v_and_parents(v0): 

3464 language = find_language(v, phase) 

3465 if language: 

3466 return language 

3467 return None 

3468#@+node:ekr.20150325075144.1: *3* g.getLanguageFromPosition 

3469def getLanguageAtPosition(c: Cmdr, p: Pos) -> str: 

3470 """ 

3471 Return the language in effect at position p. 

3472 This is always a lowercase language name, never None. 

3473 """ 

3474 aList = g.get_directives_dict_list(p) 

3475 d = g.scanAtCommentAndAtLanguageDirectives(aList) 

3476 language = ( 

3477 d and d.get('language') or 

3478 g.getLanguageFromAncestorAtFileNode(p) or 

3479 c.config.getString('target-language') or 

3480 'python' 

3481 ) 

3482 return language.lower() 

3483#@+node:ekr.20031218072017.1386: *3* g.getOutputNewline 

3484def getOutputNewline(c: Cmdr=None, name: str=None) -> str: 

3485 """Convert the name of a line ending to the line ending itself. 

3486 

3487 Priority: 

3488 - Use name if name given 

3489 - Use c.config.output_newline if c given, 

3490 - Otherwise use g.app.config.output_newline. 

3491 """ 

3492 if name: 

3493 s = name 

3494 elif c: 

3495 s = c.config.output_newline 

3496 else: 

3497 s = app.config.output_newline 

3498 if not s: 

3499 s = '' 

3500 s = s.lower() 

3501 if s in ("nl", "lf"): 

3502 s = '\n' 

3503 elif s == "cr": 

3504 s = '\r' 

3505 elif s == "platform": 

3506 s = os.linesep # 12/2/03: emakital 

3507 elif s == "crlf": 

3508 s = "\r\n" 

3509 else: 

3510 s = '\n' # Default for erroneous values. 

3511 assert isinstance(s, str), repr(s) 

3512 return s 

3513#@+node:ekr.20200521075143.1: *3* g.inAtNosearch 

3514def inAtNosearch(p: Pos) -> bool: 

3515 """Return True if p or p's ancestors contain an @nosearch directive.""" 

3516 if not p: 

3517 return False # #2288. 

3518 for p in p.self_and_parents(): 

3519 if p.is_at_ignore() or re.search(r'(^@|\n@)nosearch\b', p.b): 

3520 return True 

3521 return False 

3522#@+node:ekr.20131230090121.16528: *3* g.isDirective 

3523def isDirective(s: str) -> bool: 

3524 """Return True if s starts with a directive.""" 

3525 m = g_is_directive_pattern.match(s) 

3526 if m: 

3527 s2 = s[m.end(1) :] 

3528 if s2 and s2[0] in ".(": 

3529 return False 

3530 return bool(m.group(1) in g.globalDirectiveList) 

3531 return False 

3532#@+node:ekr.20200810074755.1: *3* g.isValidLanguage 

3533def isValidLanguage(language: str) -> bool: 

3534 """True if language exists in leo/modes.""" 

3535 # 2020/08/12: A hack for c++ 

3536 if language in ('c++', 'cpp'): 

3537 language = 'cplusplus' 

3538 fn = g.os_path_join(g.app.loadDir, '..', 'modes', f"{language}.py") 

3539 return g.os_path_exists(fn) 

3540#@+node:ekr.20080827175609.52: *3* g.scanAtCommentAndLanguageDirectives 

3541def scanAtCommentAndAtLanguageDirectives(aList: List) -> Optional[Dict[str, str]]: 

3542 """ 

3543 Scan aList for @comment and @language directives. 

3544 

3545 @comment should follow @language if both appear in the same node. 

3546 """ 

3547 lang = None 

3548 for d in aList: 

3549 comment = d.get('comment') 

3550 language = d.get('language') 

3551 # Important: assume @comment follows @language. 

3552 if language: 

3553 lang, delim1, delim2, delim3 = g.set_language(language, 0) 

3554 if comment: 

3555 delim1, delim2, delim3 = g.set_delims_from_string(comment) 

3556 if comment or language: 

3557 delims = delim1, delim2, delim3 

3558 d = {'language': lang, 'comment': comment, 'delims': delims} 

3559 return d 

3560 return None 

3561#@+node:ekr.20080827175609.32: *3* g.scanAtEncodingDirectives 

3562def scanAtEncodingDirectives(aList: List) -> Optional[str]: 

3563 """Scan aList for @encoding directives.""" 

3564 for d in aList: 

3565 encoding = d.get('encoding') 

3566 if encoding and g.isValidEncoding(encoding): 

3567 return encoding 

3568 if encoding and not g.unitTesting: 

3569 g.error("invalid @encoding:", encoding) 

3570 return None 

3571#@+node:ekr.20080827175609.53: *3* g.scanAtHeaderDirectives 

3572def scanAtHeaderDirectives(aList: List) -> None: 

3573 """scan aList for @header and @noheader directives.""" 

3574 for d in aList: 

3575 if d.get('header') and d.get('noheader'): 

3576 g.error("conflicting @header and @noheader directives") 

3577#@+node:ekr.20080827175609.33: *3* g.scanAtLineendingDirectives 

3578def scanAtLineendingDirectives(aList: List) -> Optional[str]: 

3579 """Scan aList for @lineending directives.""" 

3580 for d in aList: 

3581 e = d.get('lineending') 

3582 if e in ("cr", "crlf", "lf", "nl", "platform"): 

3583 lineending = g.getOutputNewline(name=e) 

3584 return lineending 

3585 # else: 

3586 # g.error("invalid @lineending directive:",e) 

3587 return None 

3588#@+node:ekr.20080827175609.34: *3* g.scanAtPagewidthDirectives 

3589def scanAtPagewidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[str]: 

3590 """Scan aList for @pagewidth directives.""" 

3591 for d in aList: 

3592 s = d.get('pagewidth') 

3593 if s is not None: 

3594 i, val = g.skip_long(s, 0) 

3595 if val is not None and val > 0: 

3596 return val 

3597 if issue_error_flag and not g.unitTesting: 

3598 g.error("ignoring @pagewidth", s) 

3599 return None 

3600#@+node:ekr.20101022172109.6108: *3* g.scanAtPathDirectives 

3601def scanAtPathDirectives(c: Cmdr, aList: List) -> str: 

3602 path = c.scanAtPathDirectives(aList) 

3603 return path 

3604 

3605def scanAllAtPathDirectives(c: Cmdr, p: Pos) -> str: 

3606 aList = g.get_directives_dict_list(p) 

3607 path = c.scanAtPathDirectives(aList) 

3608 return path 

3609#@+node:ekr.20080827175609.37: *3* g.scanAtTabwidthDirectives 

3610def scanAtTabwidthDirectives(aList: List, issue_error_flag: bool=False) -> Optional[int]: 

3611 """Scan aList for @tabwidth directives.""" 

3612 for d in aList: 

3613 s = d.get('tabwidth') 

3614 if s is not None: 

3615 junk, val = g.skip_long(s, 0) 

3616 if val not in (None, 0): 

3617 return val 

3618 if issue_error_flag and not g.unitTesting: 

3619 g.error("ignoring @tabwidth", s) 

3620 return None 

3621 

3622def scanAllAtTabWidthDirectives(c: Cmdr, p: Pos) -> Optional[int]: 

3623 """Scan p and all ancestors looking for @tabwidth directives.""" 

3624 if c and p: 

3625 aList = g.get_directives_dict_list(p) 

3626 val = g.scanAtTabwidthDirectives(aList) 

3627 ret = c.tab_width if val is None else val 

3628 else: 

3629 ret = None 

3630 return ret 

3631#@+node:ekr.20080831084419.4: *3* g.scanAtWrapDirectives 

3632def scanAtWrapDirectives(aList: List, issue_error_flag: bool=False) -> Optional[bool]: 

3633 """Scan aList for @wrap and @nowrap directives.""" 

3634 for d in aList: 

3635 if d.get('wrap') is not None: 

3636 return True 

3637 if d.get('nowrap') is not None: 

3638 return False 

3639 return None 

3640 

3641def scanAllAtWrapDirectives(c: Cmdr, p: Pos) -> Optional[bool]: 

3642 """Scan p and all ancestors looking for @wrap/@nowrap directives.""" 

3643 if c and p: 

3644 default = bool(c and c.config.getBool("body-pane-wraps")) 

3645 aList = g.get_directives_dict_list(p) 

3646 val = g.scanAtWrapDirectives(aList) 

3647 ret = default if val is None else val 

3648 else: 

3649 ret = None 

3650 return ret 

3651#@+node:ekr.20040715155607: *3* g.scanForAtIgnore 

3652def scanForAtIgnore(c: Cmdr, p: Pos) -> bool: 

3653 """Scan position p and its ancestors looking for @ignore directives.""" 

3654 if g.unitTesting: 

3655 return False # For unit tests. 

3656 for p in p.self_and_parents(copy=False): 

3657 d = g.get_directives_dict(p) 

3658 if 'ignore' in d: 

3659 return True 

3660 return False 

3661#@+node:ekr.20040712084911.1: *3* g.scanForAtLanguage 

3662def scanForAtLanguage(c: Cmdr, p: Pos) -> str: 

3663 """Scan position p and p's ancestors looking only for @language and @ignore directives. 

3664 

3665 Returns the language found, or c.target_language.""" 

3666 # Unlike the code in x.scanAllDirectives, this code ignores @comment directives. 

3667 if c and p: 

3668 for p in p.self_and_parents(copy=False): 

3669 d = g.get_directives_dict(p) 

3670 if 'language' in d: 

3671 z = d["language"] 

3672 language, delim1, delim2, delim3 = g.set_language(z, 0) 

3673 return language 

3674 return c.target_language 

3675#@+node:ekr.20041123094807: *3* g.scanForAtSettings 

3676def scanForAtSettings(p: Pos) -> bool: 

3677 """Scan position p and its ancestors looking for @settings nodes.""" 

3678 for p in p.self_and_parents(copy=False): 

3679 h = p.h 

3680 h = g.app.config.canonicalizeSettingName(h) 

3681 if h.startswith("@settings"): 

3682 return True 

3683 return False 

3684#@+node:ekr.20031218072017.1382: *3* g.set_delims_from_language 

3685def set_delims_from_language(language: str) -> Tuple[str, str, str]: 

3686 """Return a tuple (single,start,end) of comment delims.""" 

3687 val = g.app.language_delims_dict.get(language) 

3688 if val: 

3689 delim1, delim2, delim3 = g.set_delims_from_string(val) 

3690 if delim2 and not delim3: 

3691 return '', delim1, delim2 

3692 # 0,1 or 3 params. 

3693 return delim1, delim2, delim3 

3694 return '', '', '' 

3695 # Indicate that no change should be made 

3696#@+node:ekr.20031218072017.1383: *3* g.set_delims_from_string 

3697def set_delims_from_string(s: str) -> Tuple[str, str, str]: 

3698 """ 

3699 Return (delim1, delim2, delim2), the delims following the @comment 

3700 directive. 

3701 

3702 This code can be called from @language logic, in which case s can 

3703 point at @comment 

3704 """ 

3705 # Skip an optional @comment 

3706 tag = "@comment" 

3707 i = 0 

3708 if g.match_word(s, i, tag): 

3709 i += len(tag) 

3710 count = 0 

3711 delims = ['', '', ''] 

3712 while count < 3 and i < len(s): 

3713 i = j = g.skip_ws(s, i) 

3714 while i < len(s) and not g.is_ws(s[i]) and not g.is_nl(s, i): 

3715 i += 1 

3716 if j == i: 

3717 break 

3718 delims[count] = s[j:i] or '' 

3719 count += 1 

3720 # 'rr 09/25/02 

3721 if count == 2: # delims[0] is always the single-line delim. 

3722 delims[2] = delims[1] 

3723 delims[1] = delims[0] 

3724 delims[0] = '' 

3725 for i in range(0, 3): 

3726 if delims[i]: 

3727 if delims[i].startswith("@0x"): 

3728 # Allow delimiter definition as @0x + hexadecimal encoded delimiter 

3729 # to avoid problems with duplicate delimiters on the @comment line. 

3730 # If used, whole delimiter must be encoded. 

3731 if len(delims[i]) == 3: 

3732 g.warning(f"'{delims[i]}' delimiter is invalid") 

3733 return None, None, None 

3734 try: 

3735 delims[i] = binascii.unhexlify(delims[i][3:]) # type:ignore 

3736 delims[i] = g.toUnicode(delims[i]) 

3737 except Exception as e: 

3738 g.warning(f"'{delims[i]}' delimiter is invalid: {e}") 

3739 return None, None, None 

3740 else: 

3741 # 7/8/02: The "REM hack": replace underscores by blanks. 

3742 # 9/25/02: The "perlpod hack": replace double underscores by newlines. 

3743 delims[i] = delims[i].replace("__", '\n').replace('_', ' ') 

3744 return delims[0], delims[1], delims[2] 

3745#@+node:ekr.20031218072017.1384: *3* g.set_language 

3746def set_language(s: str, i: int, issue_errors_flag: bool=False) -> Tuple: 

3747 """Scan the @language directive that appears at s[i:]. 

3748 

3749 The @language may have been stripped away. 

3750 

3751 Returns (language, delim1, delim2, delim3) 

3752 """ 

3753 tag = "@language" 

3754 assert i is not None 

3755 if g.match_word(s, i, tag): 

3756 i += len(tag) 

3757 # Get the argument. 

3758 i = g.skip_ws(s, i) 

3759 j = i 

3760 i = g.skip_c_id(s, i) 

3761 # Allow tcl/tk. 

3762 arg = s[j:i].lower() 

3763 if app.language_delims_dict.get(arg): 

3764 language = arg 

3765 delim1, delim2, delim3 = g.set_delims_from_language(language) 

3766 return language, delim1, delim2, delim3 

3767 if issue_errors_flag: 

3768 g.es("ignoring:", g.get_line(s, i)) 

3769 return None, None, None, None 

3770#@+node:ekr.20071109165315: *3* g.stripPathCruft 

3771def stripPathCruft(path: str) -> str: 

3772 """Strip cruft from a path name.""" 

3773 if not path: 

3774 return path # Retain empty paths for warnings. 

3775 if len(path) > 2 and ( 

3776 (path[0] == '<' and path[-1] == '>') or 

3777 (path[0] == '"' and path[-1] == '"') or 

3778 (path[0] == "'" and path[-1] == "'") 

3779 ): 

3780 path = path[1:-1].strip() 

3781 # We want a *relative* path, not an absolute path. 

3782 return path 

3783#@+node:ekr.20090214075058.10: *3* g.update_directives_pat 

3784def update_directives_pat() -> None: 

3785 """Init/update g.directives_pat""" 

3786 global globalDirectiveList, directives_pat 

3787 # Use a pattern that guarantees word matches. 

3788 aList = [ 

3789 fr"\b{z}\b" for z in globalDirectiveList if z != 'others' 

3790 ] 

3791 pat = "^@(%s)" % "|".join(aList) 

3792 directives_pat = re.compile(pat, re.MULTILINE) 

3793 

3794# #1688: Initialize g.directives_pat 

3795update_directives_pat() 

3796#@+node:ekr.20031218072017.3116: ** g.Files & Directories 

3797#@+node:ekr.20080606074139.2: *3* g.chdir 

3798def chdir(path: str) -> None: 

3799 if not g.os_path_isdir(path): 

3800 path = g.os_path_dirname(path) 

3801 if g.os_path_isdir(path) and g.os_path_exists(path): 

3802 os.chdir(path) 

3803#@+node:ekr.20120222084734.10287: *3* g.compute...Dir 

3804# For compatibility with old code. 

3805 

3806def computeGlobalConfigDir() -> str: 

3807 return g.app.loadManager.computeGlobalConfigDir() 

3808 

3809def computeHomeDir() -> str: 

3810 return g.app.loadManager.computeHomeDir() 

3811 

3812def computeLeoDir() -> str: 

3813 return g.app.loadManager.computeLeoDir() 

3814 

3815def computeLoadDir() -> str: 

3816 return g.app.loadManager.computeLoadDir() 

3817 

3818def computeMachineName() -> str: 

3819 return g.app.loadManager.computeMachineName() 

3820 

3821def computeStandardDirectories() -> str: 

3822 return g.app.loadManager.computeStandardDirectories() 

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

3824def computeWindowTitle(fileName: str) -> str: 

3825 

3826 branch, commit = g.gitInfoForFile(fileName) # #1616 

3827 if not fileName: 

3828 return branch + ": untitled" if branch else 'untitled' 

3829 path, fn = g.os_path_split(fileName) 

3830 if path: 

3831 title = fn + " in " + path 

3832 else: 

3833 title = fn 

3834 # Yet another fix for bug 1194209: regularize slashes. 

3835 if os.sep in '/\\': 

3836 title = title.replace('/', os.sep).replace('\\', os.sep) 

3837 if branch: 

3838 title = branch + ": " + title 

3839 return title 

3840#@+node:ekr.20031218072017.3117: *3* g.create_temp_file 

3841def create_temp_file(textMode: bool=False) -> Tuple[Any, str]: 

3842 """ 

3843 Return a tuple (theFile,theFileName) 

3844 

3845 theFile: a file object open for writing. 

3846 theFileName: the name of the temporary file. 

3847 """ 

3848 try: 

3849 # fd is an handle to an open file as would be returned by os.open() 

3850 fd, theFileName = tempfile.mkstemp(text=textMode) 

3851 mode = 'w' if textMode else 'wb' 

3852 theFile = os.fdopen(fd, mode) 

3853 except Exception: 

3854 g.error('unexpected exception in g.create_temp_file') 

3855 g.es_exception() 

3856 theFile, theFileName = None, '' 

3857 return theFile, theFileName 

3858#@+node:ekr.20210307060731.1: *3* g.createHiddenCommander 

3859def createHiddenCommander(fn: str) -> Optional[Cmdr]: 

3860 """Read the file into a hidden commander (Similar to g.openWithFileName).""" 

3861 from leo.core.leoCommands import Commands 

3862 c = Commands(fn, gui=g.app.nullGui) 

3863 theFile = g.app.loadManager.openAnyLeoFile(fn) 

3864 if theFile: 

3865 c.fileCommands.openLeoFile( # type:ignore 

3866 theFile, fn, readAtFileNodesFlag=True, silent=True) 

3867 return c 

3868 return None 

3869#@+node:vitalije.20170714085545.1: *3* g.defaultLeoFileExtension 

3870def defaultLeoFileExtension(c: Cmdr=None) -> str: 

3871 conf = c.config if c else g.app.config 

3872 return conf.getString('default-leo-extension') or '.leo' 

3873#@+node:ekr.20031218072017.3118: *3* g.ensure_extension 

3874def ensure_extension(name: str, ext: str) -> str: 

3875 

3876 theFile, old_ext = g.os_path_splitext(name) 

3877 if not name: 

3878 return name # don't add to an empty name. 

3879 if old_ext in ('.db', '.leo'): 

3880 return name 

3881 if old_ext and old_ext == ext: 

3882 return name 

3883 return name + ext 

3884#@+node:ekr.20150403150655.1: *3* g.fullPath 

3885def fullPath(c: Cmdr, p: Pos, simulate: bool=False) -> str: 

3886 """ 

3887 Return the full path (including fileName) in effect at p. Neither the 

3888 path nor the fileName will be created if it does not exist. 

3889 """ 

3890 # Search p and p's parents. 

3891 for p in p.self_and_parents(copy=False): 

3892 aList = g.get_directives_dict_list(p) 

3893 path = c.scanAtPathDirectives(aList) 

3894 fn = p.h if simulate else p.anyAtFileNodeName() # Use p.h for unit tests. 

3895 if fn: 

3896 # Fix #102: expand path expressions. 

3897 fn = c.expand_path_expression(fn) # #1341. 

3898 fn = os.path.expanduser(fn) # 1900. 

3899 return g.os_path_finalize_join(path, fn) # #1341. 

3900 return '' 

3901#@+node:ekr.20190327192721.1: *3* g.get_files_in_directory 

3902def get_files_in_directory(directory: str, kinds: List=None, recursive: bool=True) -> List[str]: 

3903 """ 

3904 Return a list of all files of the given file extensions in the directory. 

3905 Default kinds: ['*.py']. 

3906 """ 

3907 files: List[str] = [] 

3908 sep = os.path.sep 

3909 if not g.os.path.exists(directory): 

3910 g.es_print('does not exist', directory) 

3911 return files 

3912 try: 

3913 if kinds: 

3914 kinds = [z if z.startswith('*') else '*' + z for z in kinds] 

3915 else: 

3916 kinds = ['*.py'] 

3917 if recursive: 

3918 # Works for all versions of Python. 

3919 for root, dirnames, filenames in os.walk(directory): 

3920 for kind in kinds: 

3921 for filename in fnmatch.filter(filenames, kind): 

3922 files.append(os.path.join(root, filename)) 

3923 else: 

3924 for kind in kinds: 

3925 files.extend(glob.glob(directory + sep + kind)) 

3926 return list(set(sorted(files))) 

3927 except Exception: 

3928 g.es_exception() 

3929 return [] 

3930#@+node:ekr.20031218072017.1264: *3* g.getBaseDirectory 

3931# Handles the conventions applying to the "relative_path_base_directory" configuration option. 

3932 

3933def getBaseDirectory(c: Cmdr) -> str: 

3934 """Convert '!' or '.' to proper directory references.""" 

3935 base = app.config.relative_path_base_directory 

3936 if base and base == "!": 

3937 base = app.loadDir 

3938 elif base and base == ".": 

3939 base = c.openDirectory 

3940 if base and g.os_path_isabs(base): 

3941 # Set c.chdir_to_relative_path as needed. 

3942 if not hasattr(c, 'chdir_to_relative_path'): 

3943 c.chdir_to_relative_path = c.config.getBool('chdir-to-relative-path') 

3944 # Call os.chdir if requested. 

3945 if c.chdir_to_relative_path: 

3946 os.chdir(base) 

3947 return base # base need not exist yet. 

3948 return "" # No relative base given. 

3949#@+node:ekr.20170223093758.1: *3* g.getEncodingAt 

3950def getEncodingAt(p: Pos, s: str=None) -> str: 

3951 """ 

3952 Return the encoding in effect at p and/or for string s. 

3953 

3954 Read logic: s is not None. 

3955 Write logic: s is None. 

3956 """ 

3957 # A BOM overrides everything. 

3958 if s: 

3959 e, junk_s = g.stripBOM(s) 

3960 if e: 

3961 return e 

3962 aList = g.get_directives_dict_list(p) 

3963 e = g.scanAtEncodingDirectives(aList) 

3964 if s and s.strip() and not e: 

3965 e = 'utf-8' 

3966 return e 

3967#@+node:ville.20090701144325.14942: *3* g.guessExternalEditor 

3968def guessExternalEditor(c: Cmdr=None) -> Optional[str]: 

3969 """ Return a 'sensible' external editor """ 

3970 editor = ( 

3971 os.environ.get("LEO_EDITOR") or 

3972 os.environ.get("EDITOR") or 

3973 g.app.db and g.app.db.get("LEO_EDITOR") or 

3974 c and c.config.getString('external-editor')) 

3975 if editor: 

3976 return editor 

3977 # fallbacks 

3978 platform = sys.platform.lower() 

3979 if platform.startswith('win'): 

3980 return "notepad" 

3981 if platform.startswith('linux'): 

3982 return 'gedit' 

3983 g.es( 

3984 '''No editor set. 

3985Please set LEO_EDITOR or EDITOR environment variable, 

3986or do g.app.db['LEO_EDITOR'] = "gvim"''', 

3987 ) 

3988 return None 

3989#@+node:ekr.20160330204014.1: *3* g.init_dialog_folder 

3990def init_dialog_folder(c: Cmdr, p: Pos, use_at_path: bool=True) -> str: 

3991 """Return the most convenient folder to open or save a file.""" 

3992 if c and p and use_at_path: 

3993 path = g.fullPath(c, p) 

3994 if path: 

3995 dir_ = g.os_path_dirname(path) 

3996 if dir_ and g.os_path_exists(dir_): 

3997 return dir_ 

3998 table = ( 

3999 ('c.last_dir', c and c.last_dir), 

4000 ('os.curdir', g.os_path_abspath(os.curdir)), 

4001 ) 

4002 for kind, dir_ in table: 

4003 if dir_ and g.os_path_exists(dir_): 

4004 return dir_ 

4005 return '' 

4006#@+node:ekr.20100329071036.5744: *3* g.is_binary_file/external_file/string 

4007def is_binary_file(f: Any) -> bool: 

4008 return f and isinstance(f, io.BufferedIOBase) 

4009 

4010def is_binary_external_file(fileName: str) -> bool: 

4011 try: 

4012 with open(fileName, 'rb') as f: 

4013 s = f.read(1024) # bytes, in Python 3. 

4014 return g.is_binary_string(s) 

4015 except IOError: 

4016 return False 

4017 except Exception: 

4018 g.es_exception() 

4019 return False 

4020 

4021def is_binary_string(s: str) -> bool: 

4022 # http://stackoverflow.com/questions/898669 

4023 # aList is a list of all non-binary characters. 

4024 aList = [7, 8, 9, 10, 12, 13, 27] + list(range(0x20, 0x100)) 

4025 return bool(s.translate(None, bytes(aList))) # type:ignore 

4026#@+node:EKR.20040504154039: *3* g.is_sentinel 

4027def is_sentinel(line: str, delims: Sequence) -> bool: 

4028 """Return True if line starts with a sentinel comment.""" 

4029 delim1, delim2, delim3 = delims 

4030 line = line.lstrip() 

4031 if delim1: 

4032 return line.startswith(delim1 + '@') 

4033 if delim2 and delim3: 

4034 i = line.find(delim2 + '@') 

4035 j = line.find(delim3) 

4036 return 0 == i < j 

4037 g.error(f"is_sentinel: can not happen. delims: {repr(delims)}") 

4038 return False 

4039#@+node:ekr.20031218072017.3119: *3* g.makeAllNonExistentDirectories 

4040def makeAllNonExistentDirectories(theDir: str) -> Optional[str]: 

4041 """ 

4042 A wrapper from os.makedirs. 

4043 Attempt to make all non-existent directories. 

4044 

4045 Return True if the directory exists or was created successfully. 

4046 """ 

4047 # Return True if the directory already exists. 

4048 theDir = g.os_path_normpath(theDir) 

4049 ok = g.os_path_isdir(theDir) and g.os_path_exists(theDir) 

4050 if ok: 

4051 return theDir 

4052 # #1450: Create the directory with os.makedirs. 

4053 try: 

4054 os.makedirs(theDir, mode=0o777, exist_ok=False) 

4055 return theDir 

4056 except Exception: 

4057 return None 

4058#@+node:ekr.20071114113736: *3* g.makePathRelativeTo 

4059def makePathRelativeTo(fullPath: str, basePath: str) -> str: 

4060 if fullPath.startswith(basePath): 

4061 s = fullPath[len(basePath) :] 

4062 if s.startswith(os.path.sep): 

4063 s = s[len(os.path.sep) :] 

4064 return s 

4065 return fullPath 

4066#@+node:ekr.20090520055433.5945: *3* g.openWithFileName 

4067def openWithFileName(fileName: str, old_c: Cmdr=None, gui: str=None) -> Cmdr: 

4068 """ 

4069 Create a Leo Frame for the indicated fileName if the file exists. 

4070 

4071 Return the commander of the newly-opened outline. 

4072 """ 

4073 return g.app.loadManager.loadLocalFile(fileName, gui, old_c) 

4074#@+node:ekr.20150306035851.7: *3* g.readFileIntoEncodedString 

4075def readFileIntoEncodedString(fn: str, silent: bool=False) -> Optional[bytes]: 

4076 """Return the raw contents of the file whose full path is fn.""" 

4077 try: 

4078 with open(fn, 'rb') as f: 

4079 return f.read() 

4080 except IOError: 

4081 if not silent: 

4082 g.error('can not open', fn) 

4083 except Exception: 

4084 if not silent: 

4085 g.error(f"readFileIntoEncodedString: exception reading {fn}") 

4086 g.es_exception() 

4087 return None 

4088#@+node:ekr.20100125073206.8710: *3* g.readFileIntoString 

4089def readFileIntoString( 

4090 fileName: str, 

4091 encoding: str='utf-8', # BOM may override this. 

4092 kind: str=None, # @file, @edit, ... 

4093 verbose: bool=True, 

4094) -> Tuple[Any, Any]: 

4095 """ 

4096 Return the contents of the file whose full path is fileName. 

4097 

4098 Return (s,e) 

4099 s is the string, converted to unicode, or None if there was an error. 

4100 e is the encoding of s, computed in the following order: 

4101 - The BOM encoding if the file starts with a BOM mark. 

4102 - The encoding given in the # -*- coding: utf-8 -*- line for python files. 

4103 - The encoding given by the 'encoding' keyword arg. 

4104 - None, which typically means 'utf-8'. 

4105 """ 

4106 if not fileName: 

4107 if verbose: 

4108 g.trace('no fileName arg given') 

4109 return None, None 

4110 if g.os_path_isdir(fileName): 

4111 if verbose: 

4112 g.trace('not a file:', fileName) 

4113 return None, None 

4114 if not g.os_path_exists(fileName): 

4115 if verbose: 

4116 g.error('file not found:', fileName) 

4117 return None, None 

4118 try: 

4119 e = None 

4120 with open(fileName, 'rb') as f: 

4121 s = f.read() 

4122 # Fix #391. 

4123 if not s: 

4124 return '', None 

4125 # New in Leo 4.11: check for unicode BOM first. 

4126 e, s = g.stripBOM(s) 

4127 if not e: 

4128 # Python's encoding comments override everything else. 

4129 junk, ext = g.os_path_splitext(fileName) 

4130 if ext == '.py': 

4131 e = g.getPythonEncodingFromString(s) 

4132 s = g.toUnicode(s, encoding=e or encoding) 

4133 return s, e 

4134 except IOError: 

4135 # Translate 'can not open' and kind, but not fileName. 

4136 if verbose: 

4137 g.error('can not open', '', (kind or ''), fileName) 

4138 except Exception: 

4139 g.error(f"readFileIntoString: unexpected exception reading {fileName}") 

4140 g.es_exception() 

4141 return None, None 

4142#@+node:ekr.20160504062833.1: *3* g.readFileToUnicodeString 

4143def readFileIntoUnicodeString(fn: str, encoding: Optional[str]=None, silent: bool=False) -> Optional[str]: 

4144 """Return the raw contents of the file whose full path is fn.""" 

4145 try: 

4146 with open(fn, 'rb') as f: 

4147 s = f.read() 

4148 return g.toUnicode(s, encoding=encoding) 

4149 except IOError: 

4150 if not silent: 

4151 g.error('can not open', fn) 

4152 except Exception: 

4153 g.error(f"readFileIntoUnicodeString: unexpected exception reading {fn}") 

4154 g.es_exception() 

4155 return None 

4156#@+node:ekr.20031218072017.3120: *3* g.readlineForceUnixNewline 

4157#@+at Stephen P. Schaefer 9/7/2002 

4158# 

4159# The Unix readline() routine delivers "\r\n" line end strings verbatim, 

4160# while the windows versions force the string to use the Unix convention 

4161# of using only "\n". This routine causes the Unix readline to do the 

4162# same. 

4163#@@c 

4164 

4165def readlineForceUnixNewline(f: Any, fileName: Optional[str]=None) -> str: 

4166 try: 

4167 s = f.readline() 

4168 except UnicodeDecodeError: 

4169 g.trace(f"UnicodeDecodeError: {fileName}", f, g.callers()) 

4170 s = '' 

4171 if len(s) >= 2 and s[-2] == "\r" and s[-1] == "\n": 

4172 s = s[0:-2] + "\n" 

4173 return s 

4174#@+node:ekr.20031218072017.3124: *3* g.sanitize_filename 

4175def sanitize_filename(s: str) -> str: 

4176 """ 

4177 Prepares string s to be a valid file name: 

4178 

4179 - substitute '_' for whitespace and special path characters. 

4180 - eliminate all other non-alphabetic characters. 

4181 - convert double quotes to single quotes. 

4182 - strip leading and trailing whitespace. 

4183 - return at most 128 characters. 

4184 """ 

4185 result = [] 

4186 for ch in s: 

4187 if ch in string.ascii_letters: 

4188 result.append(ch) 

4189 elif ch == '\t': 

4190 result.append(' ') 

4191 elif ch == '"': 

4192 result.append("'") 

4193 elif ch in '\\/:|<>*:._': 

4194 result.append('_') 

4195 s = ''.join(result).strip() 

4196 while len(s) > 1: 

4197 n = len(s) 

4198 s = s.replace('__', '_') 

4199 if len(s) == n: 

4200 break 

4201 return s[:128] 

4202#@+node:ekr.20060328150113: *3* g.setGlobalOpenDir 

4203def setGlobalOpenDir(fileName: str) -> None: 

4204 if fileName: 

4205 g.app.globalOpenDir = g.os_path_dirname(fileName) 

4206 # g.es('current directory:',g.app.globalOpenDir) 

4207#@+node:ekr.20031218072017.3125: *3* g.shortFileName & shortFilename 

4208def shortFileName(fileName: str, n: int=None) -> str: 

4209 """Return the base name of a path.""" 

4210 if n is not None: 

4211 g.trace('"n" keyword argument is no longer used') 

4212 return g.os_path_basename(fileName) if fileName else '' 

4213 

4214shortFilename = shortFileName 

4215#@+node:ekr.20150610125813.1: *3* g.splitLongFileName 

4216def splitLongFileName(fn: str, limit: int=40) -> str: 

4217 """Return fn, split into lines at slash characters.""" 

4218 aList = fn.replace('\\', '/').split('/') 

4219 n, result = 0, [] 

4220 for i, s in enumerate(aList): 

4221 n += len(s) 

4222 result.append(s) 

4223 if i + 1 < len(aList): 

4224 result.append('/') 

4225 n += 1 

4226 if n > limit: 

4227 result.append('\n') 

4228 n = 0 

4229 return ''.join(result) 

4230#@+node:ekr.20190114061452.26: *3* g.writeFile 

4231def writeFile(contents: Union[bytes, str], encoding: str, fileName: str) -> bool: 

4232 """Create a file with the given contents.""" 

4233 try: 

4234 if isinstance(contents, str): 

4235 contents = g.toEncodedString(contents, encoding=encoding) 

4236 # 'wb' preserves line endings. 

4237 with open(fileName, 'wb') as f: 

4238 f.write(contents) # type:ignore 

4239 return True 

4240 except Exception as e: 

4241 print(f"exception writing: {fileName}:\n{e}") 

4242 # g.trace(g.callers()) 

4243 # g.es_exception() 

4244 return False 

4245#@+node:ekr.20031218072017.3151: ** g.Finding & Scanning 

4246#@+node:ekr.20140602083643.17659: *3* g.find_word 

4247def find_word(s: str, word: str, i: int=0) -> int: 

4248 """ 

4249 Return the index of the first occurance of word in s, or -1 if not found. 

4250 

4251 g.find_word is *not* the same as s.find(i,word); 

4252 g.find_word ensures that only word-matches are reported. 

4253 """ 

4254 while i < len(s): 

4255 progress = i 

4256 i = s.find(word, i) 

4257 if i == -1: 

4258 return -1 

4259 # Make sure we are at the start of a word. 

4260 if i > 0: 

4261 ch = s[i - 1] 

4262 if ch == '_' or ch.isalnum(): 

4263 i += len(word) 

4264 continue 

4265 if g.match_word(s, i, word): 

4266 return i 

4267 i += len(word) 

4268 assert progress < i 

4269 return -1 

4270#@+node:ekr.20211029090118.1: *3* g.findAncestorVnodeByPredicate 

4271def findAncestorVnodeByPredicate(p: Pos, v_predicate: Any) -> Optional["VNode"]: 

4272 """ 

4273 Return first ancestor vnode matching the predicate. 

4274  

4275 The predicate must must be a function of a single vnode argument. 

4276 """ 

4277 if not p: 

4278 return None 

4279 # First, look up the tree. 

4280 for p2 in p.self_and_parents(): 

4281 if v_predicate(p2.v): 

4282 return p2.v 

4283 # Look at parents of all cloned nodes. 

4284 if not p.isCloned(): 

4285 return None 

4286 seen = [] # vnodes that have already been searched. 

4287 parents = p.v.parents[:] # vnodes to be searched. 

4288 while parents: 

4289 parent_v = parents.pop() 

4290 if parent_v in seen: 

4291 continue 

4292 seen.append(parent_v) 

4293 if v_predicate(parent_v): 

4294 return parent_v 

4295 for grand_parent_v in parent_v.parents: 

4296 if grand_parent_v not in seen: 

4297 parents.append(grand_parent_v) 

4298 return None 

4299#@+node:ekr.20170220103251.1: *3* g.findRootsWithPredicate 

4300def findRootsWithPredicate(c: Cmdr, root: Pos, predicate: Any=None) -> List[Pos]: 

4301 """ 

4302 Commands often want to find one or more **roots**, given a position p. 

4303 A root is the position of any node matching a predicate. 

4304 

4305 This function formalizes the search order used by the black, 

4306 pylint, pyflakes and the rst3 commands, returning a list of zero 

4307 or more found roots. 

4308 """ 

4309 seen = [] 

4310 roots = [] 

4311 if predicate is None: 

4312 

4313 # A useful default predicate for python. 

4314 # pylint: disable=function-redefined 

4315 

4316 def predicate(p: Pos) -> bool: 

4317 return p.isAnyAtFileNode() and p.h.strip().endswith('.py') 

4318 

4319 # 1. Search p's tree. 

4320 for p in root.self_and_subtree(copy=False): 

4321 if predicate(p) and p.v not in seen: 

4322 seen.append(p.v) 

4323 roots.append(p.copy()) 

4324 if roots: 

4325 return roots 

4326 # 2. Look up the tree. 

4327 for p in root.parents(): 

4328 if predicate(p): 

4329 return [p.copy()] 

4330 # 3. Expand the search if root is a clone. 

4331 clones = [] 

4332 for p in root.self_and_parents(copy=False): 

4333 if p.isCloned(): 

4334 clones.append(p.v) 

4335 if clones: 

4336 for p in c.all_positions(copy=False): 

4337 if predicate(p): 

4338 # Match if any node in p's tree matches any clone. 

4339 for p2 in p.self_and_subtree(): 

4340 if p2.v in clones: 

4341 return [p.copy()] 

4342 return [] 

4343#@+node:ekr.20031218072017.3156: *3* g.scanError 

4344# It is dubious to bump the Tangle error count here, but it really doesn't hurt. 

4345 

4346def scanError(s: str) -> None: 

4347 """Bump the error count in the tangle command.""" 

4348 # New in Leo 4.4b1: just set this global. 

4349 g.app.scanErrors += 1 

4350 g.es('', s) 

4351#@+node:ekr.20031218072017.3157: *3* g.scanf 

4352# A quick and dirty sscanf. Understands only %s and %d. 

4353 

4354def scanf(s: str, pat: str) -> List[str]: 

4355 count = pat.count("%s") + pat.count("%d") 

4356 pat = pat.replace("%s", r"(\S+)") 

4357 pat = pat.replace("%d", r"(\d+)") 

4358 parts = re.split(pat, s) 

4359 result: List[str] = [] 

4360 for part in parts: 

4361 if part and len(result) < count: 

4362 result.append(part) 

4363 return result 

4364#@+node:ekr.20031218072017.3158: *3* g.Scanners: calling scanError 

4365#@+at These scanners all call g.scanError() directly or indirectly, so they 

4366# will call g.es if they find an error. g.scanError() also bumps 

4367# c.tangleCommands.errors, which is harmless if we aren't tangling, and 

4368# useful if we are. 

4369# 

4370# These routines are called by the Import routines and the Tangle routines. 

4371#@+node:ekr.20031218072017.3159: *4* g.skip_block_comment 

4372# Scans past a block comment (an old_style C comment). 

4373 

4374def skip_block_comment(s: str, i: int) -> int: 

4375 assert g.match(s, i, "/*") 

4376 j = i 

4377 i += 2 

4378 n = len(s) 

4379 k = s.find("*/", i) 

4380 if k == -1: 

4381 g.scanError("Run on block comment: " + s[j:i]) 

4382 return n 

4383 return k + 2 

4384#@+node:ekr.20031218072017.3160: *4* g.skip_braces 

4385#@+at This code is called only from the import logic, so we are allowed to 

4386# try some tricks. In particular, we assume all braces are matched in 

4387# if blocks. 

4388#@@c 

4389 

4390def skip_braces(s: str, i: int) -> int: 

4391 """ 

4392 Skips from the opening to the matching brace. 

4393 

4394 If no matching is found i is set to len(s) 

4395 """ 

4396 # start = g.get_line(s,i) 

4397 assert g.match(s, i, '{') 

4398 level = 0 

4399 n = len(s) 

4400 while i < n: 

4401 c = s[i] 

4402 if c == '{': 

4403 level += 1 

4404 i += 1 

4405 elif c == '}': 

4406 level -= 1 

4407 if level <= 0: 

4408 return i 

4409 i += 1 

4410 elif c == '\'' or c == '"': 

4411 i = g.skip_string(s, i) 

4412 elif g.match(s, i, '//'): 

4413 i = g.skip_to_end_of_line(s, i) 

4414 elif g.match(s, i, '/*'): 

4415 i = g.skip_block_comment(s, i) 

4416 # 7/29/02: be more careful handling conditional code. 

4417 elif ( 

4418 g.match_word(s, i, "#if") or 

4419 g.match_word(s, i, "#ifdef") or 

4420 g.match_word(s, i, "#ifndef") 

4421 ): 

4422 i, delta = g.skip_pp_if(s, i) 

4423 level += delta 

4424 else: i += 1 

4425 return i 

4426#@+node:ekr.20031218072017.3162: *4* g.skip_parens 

4427def skip_parens(s: str, i: int) -> int: 

4428 """ 

4429 Skips from the opening ( to the matching ). 

4430 

4431 If no matching is found i is set to len(s). 

4432 """ 

4433 level = 0 

4434 n = len(s) 

4435 assert g.match(s, i, '('), repr(s[i]) 

4436 while i < n: 

4437 c = s[i] 

4438 if c == '(': 

4439 level += 1 

4440 i += 1 

4441 elif c == ')': 

4442 level -= 1 

4443 if level <= 0: 

4444 return i 

4445 i += 1 

4446 elif c == '\'' or c == '"': 

4447 i = g.skip_string(s, i) 

4448 elif g.match(s, i, "//"): 

4449 i = g.skip_to_end_of_line(s, i) 

4450 elif g.match(s, i, "/*"): 

4451 i = g.skip_block_comment(s, i) 

4452 else: 

4453 i += 1 

4454 return i 

4455#@+node:ekr.20031218072017.3163: *4* g.skip_pascal_begin_end 

4456def skip_pascal_begin_end(s: str, i: int) -> int: 

4457 """ 

4458 Skips from begin to matching end. 

4459 If found, i points to the end. Otherwise, i >= len(s) 

4460 The end keyword matches begin, case, class, record, and try. 

4461 """ 

4462 assert g.match_c_word(s, i, "begin") 

4463 level = 1 

4464 i = g.skip_c_id(s, i) # Skip the opening begin. 

4465 while i < len(s): 

4466 ch = s[i] 

4467 if ch == '{': 

4468 i = g.skip_pascal_braces(s, i) 

4469 elif ch == '"' or ch == '\'': 

4470 i = g.skip_pascal_string(s, i) 

4471 elif g.match(s, i, "//"): 

4472 i = g.skip_line(s, i) 

4473 elif g.match(s, i, "(*"): 

4474 i = g.skip_pascal_block_comment(s, i) 

4475 elif g.match_c_word(s, i, "end"): 

4476 level -= 1 

4477 if level == 0: 

4478 return i 

4479 i = g.skip_c_id(s, i) 

4480 elif g.is_c_id(ch): 

4481 j = i 

4482 i = g.skip_c_id(s, i) 

4483 name = s[j:i] 

4484 if name in ["begin", "case", "class", "record", "try"]: 

4485 level += 1 

4486 else: 

4487 i += 1 

4488 return i 

4489#@+node:ekr.20031218072017.3164: *4* g.skip_pascal_block_comment 

4490def skip_pascal_block_comment(s: str, i: int) -> int: 

4491 """Scan past a pascal comment delimited by (* and *).""" 

4492 j = i 

4493 assert g.match(s, i, "(*") 

4494 i = s.find("*)", i) 

4495 if i > -1: 

4496 return i + 2 

4497 g.scanError("Run on comment" + s[j:i]) 

4498 return len(s) 

4499#@+node:ekr.20031218072017.3165: *4* g.skip_pascal_string 

4500def skip_pascal_string(s: str, i: int) -> int: 

4501 j = i 

4502 delim = s[i] 

4503 i += 1 

4504 assert delim == '"' or delim == '\'' 

4505 while i < len(s): 

4506 if s[i] == delim: 

4507 return i + 1 

4508 i += 1 

4509 g.scanError("Run on string: " + s[j:i]) 

4510 return i 

4511#@+node:ekr.20031218072017.3166: *4* g.skip_heredoc_string 

4512def skip_heredoc_string(s: str, i: int) -> int: 

4513 """ 

4514 08-SEP-2002 DTHEIN. 

4515 A heredoc string in PHP looks like: 

4516 

4517 <<<EOS 

4518 This is my string. 

4519 It is mine. I own it. 

4520 No one else has it. 

4521 EOS 

4522 

4523 It begins with <<< plus a token (naming same as PHP variable names). 

4524 It ends with the token on a line by itself (must start in first position. 

4525 """ 

4526 j = i 

4527 assert g.match(s, i, "<<<") 

4528 m = re.match(r"\<\<\<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)", s[i:]) 

4529 if m is None: 

4530 i += 3 

4531 return i 

4532 # 14-SEP-2002 DTHEIN: needed to add \n to find word, not just string 

4533 delim = m.group(1) + '\n' 

4534 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: look after \n, not before 

4535 n = len(s) 

4536 while i < n and not g.match(s, i, delim): 

4537 i = g.skip_line(s, i) # 14-SEP-2002 DTHEIN: move past \n 

4538 if i >= n: 

4539 g.scanError("Run on string: " + s[j:i]) 

4540 elif g.match(s, i, delim): 

4541 i += len(delim) 

4542 return i 

4543#@+node:ekr.20031218072017.3167: *4* g.skip_pp_directive 

4544def skip_pp_directive(s: str, i: int) -> int: 

4545 """Now handles continuation lines and block comments.""" 

4546 while i < len(s): 

4547 if g.is_nl(s, i): 

4548 if g.escaped(s, i): 

4549 i = g.skip_nl(s, i) 

4550 else: 

4551 break 

4552 elif g.match(s, i, "//"): 

4553 i = g.skip_to_end_of_line(s, i) 

4554 elif g.match(s, i, "/*"): 

4555 i = g.skip_block_comment(s, i) 

4556 else: 

4557 i += 1 

4558 return i 

4559#@+node:ekr.20031218072017.3168: *4* g.skip_pp_if 

4560# Skips an entire if or if def statement, including any nested statements. 

4561 

4562def skip_pp_if(s: str, i: int) -> Tuple[int, int]: 

4563 start_line = g.get_line(s, i) # used for error messages. 

4564 assert( 

4565 g.match_word(s, i, "#if") or 

4566 g.match_word(s, i, "#ifdef") or 

4567 g.match_word(s, i, "#ifndef")) 

4568 i = g.skip_line(s, i) 

4569 i, delta1 = g.skip_pp_part(s, i) 

4570 i = g.skip_ws(s, i) 

4571 if g.match_word(s, i, "#else"): 

4572 i = g.skip_line(s, i) 

4573 i = g.skip_ws(s, i) 

4574 i, delta2 = g.skip_pp_part(s, i) 

4575 if delta1 != delta2: 

4576 g.es("#if and #else parts have different braces:", start_line) 

4577 i = g.skip_ws(s, i) 

4578 if g.match_word(s, i, "#endif"): 

4579 i = g.skip_line(s, i) 

4580 else: 

4581 g.es("no matching #endif:", start_line) 

4582 return i, delta1 

4583#@+node:ekr.20031218072017.3169: *4* g.skip_pp_part 

4584# Skip to an #else or #endif. The caller has eaten the #if, #ifdef, #ifndef or #else 

4585 

4586def skip_pp_part(s: str, i: int) -> Tuple[int, int]: 

4587 

4588 delta = 0 

4589 while i < len(s): 

4590 c = s[i] 

4591 if ( 

4592 g.match_word(s, i, "#if") or 

4593 g.match_word(s, i, "#ifdef") or 

4594 g.match_word(s, i, "#ifndef") 

4595 ): 

4596 i, delta1 = g.skip_pp_if(s, i) 

4597 delta += delta1 

4598 elif g.match_word(s, i, "#else") or g.match_word(s, i, "#endif"): 

4599 return i, delta 

4600 elif c == '\'' or c == '"': 

4601 i = g.skip_string(s, i) 

4602 elif c == '{': 

4603 delta += 1 

4604 i += 1 

4605 elif c == '}': 

4606 delta -= 1 

4607 i += 1 

4608 elif g.match(s, i, "//"): 

4609 i = g.skip_line(s, i) 

4610 elif g.match(s, i, "/*"): 

4611 i = g.skip_block_comment(s, i) 

4612 else: 

4613 i += 1 

4614 return i, delta 

4615#@+node:ekr.20031218072017.3171: *4* g.skip_to_semicolon 

4616# Skips to the next semicolon that is not in a comment or a string. 

4617 

4618def skip_to_semicolon(s: str, i: int) -> int: 

4619 n = len(s) 

4620 while i < n: 

4621 c = s[i] 

4622 if c == ';': 

4623 return i 

4624 if c == '\'' or c == '"': 

4625 i = g.skip_string(s, i) 

4626 elif g.match(s, i, "//"): 

4627 i = g.skip_to_end_of_line(s, i) 

4628 elif g.match(s, i, "/*"): 

4629 i = g.skip_block_comment(s, i) 

4630 else: 

4631 i += 1 

4632 return i 

4633#@+node:ekr.20031218072017.3172: *4* g.skip_typedef 

4634def skip_typedef(s: str, i: int) -> int: 

4635 n = len(s) 

4636 while i < n and g.is_c_id(s[i]): 

4637 i = g.skip_c_id(s, i) 

4638 i = g.skip_ws_and_nl(s, i) 

4639 if g.match(s, i, '{'): 

4640 i = g.skip_braces(s, i) 

4641 i = g.skip_to_semicolon(s, i) 

4642 return i 

4643#@+node:ekr.20201127143342.1: *3* g.see_more_lines 

4644def see_more_lines(s: str, ins: int, n: int=4) -> int: 

4645 """ 

4646 Extend index i within string s to include n more lines. 

4647 """ 

4648 # Show more lines, if they exist. 

4649 if n > 0: 

4650 for z in range(n): 

4651 if ins >= len(s): 

4652 break 

4653 i, j = g.getLine(s, ins) 

4654 ins = j 

4655 return max(0, min(ins, len(s))) 

4656#@+node:ekr.20031218072017.3195: *3* g.splitLines 

4657def splitLines(s: str) -> List[str]: 

4658 """ 

4659 Split s into lines, preserving the number of lines and 

4660 the endings of all lines, including the last line. 

4661 """ 

4662 return s.splitlines(True) if s else [] # This is a Python string function! 

4663 

4664splitlines = splitLines 

4665#@+node:ekr.20031218072017.3173: *3* Scanners: no error messages 

4666#@+node:ekr.20031218072017.3174: *4* g.escaped 

4667# Returns True if s[i] is preceded by an odd number of backslashes. 

4668 

4669def escaped(s: str, i: int) -> bool: 

4670 count = 0 

4671 while i - 1 >= 0 and s[i - 1] == '\\': 

4672 count += 1 

4673 i -= 1 

4674 return (count % 2) == 1 

4675#@+node:ekr.20031218072017.3175: *4* g.find_line_start 

4676def find_line_start(s: str, i: int) -> int: 

4677 """Return the index in s of the start of the line containing s[i].""" 

4678 if i < 0: 

4679 return 0 # New in Leo 4.4.5: add this defensive code. 

4680 # bug fix: 11/2/02: change i to i+1 in rfind 

4681 i = s.rfind('\n', 0, i + 1) # Finds the highest index in the range. 

4682 return 0 if i == -1 else i + 1 

4683#@+node:ekr.20031218072017.3176: *4* g.find_on_line 

4684def find_on_line(s: str, i: int, pattern: str) -> int: 

4685 j = s.find('\n', i) 

4686 if j == -1: 

4687 j = len(s) 

4688 k = s.find(pattern, i, j) 

4689 return k 

4690#@+node:ekr.20031218072017.3179: *4* g.g.is_special 

4691def is_special(s: str, directive: str) -> Tuple[bool, int]: 

4692 """Return True if the body text contains the @ directive.""" 

4693 assert(directive and directive[0] == '@') 

4694 # Most directives must start the line. 

4695 lws = directive in ("@others", "@all") 

4696 pattern_s = r'^\s*(%s\b)' if lws else r'^(%s\b)' 

4697 pattern = re.compile(pattern_s % directive, re.MULTILINE) 

4698 m = re.search(pattern, s) 

4699 if m: 

4700 return True, m.start(1) 

4701 return False, -1 

4702#@+node:ekr.20031218072017.3177: *4* g.is_c_id 

4703def is_c_id(ch: str) -> bool: 

4704 return g.isWordChar(ch) 

4705#@+node:ekr.20031218072017.3178: *4* g.is_nl 

4706def is_nl(s: str, i: int) -> bool: 

4707 return i < len(s) and (s[i] == '\n' or s[i] == '\r') 

4708#@+node:ekr.20031218072017.3180: *4* g.is_ws & is_ws_or_nl 

4709def is_ws(ch: str) -> bool: 

4710 return ch == '\t' or ch == ' ' 

4711 

4712def is_ws_or_nl(s: str, i: int) -> bool: 

4713 return g.is_nl(s, i) or (i < len(s) and g.is_ws(s[i])) 

4714#@+node:ekr.20031218072017.3181: *4* g.match 

4715# Warning: this code makes no assumptions about what follows pattern. 

4716 

4717def match(s: str, i: int, pattern: str) -> bool: 

4718 return bool(s and pattern and s.find(pattern, i, i + len(pattern)) == i) 

4719#@+node:ekr.20031218072017.3182: *4* g.match_c_word 

4720def match_c_word(s: str, i: int, name: str) -> bool: 

4721 n = len(name) 

4722 return bool( 

4723 name and 

4724 name == s[i : i + n] and 

4725 (i + n == len(s) or not g.is_c_id(s[i + n])) 

4726 ) 

4727#@+node:ekr.20031218072017.3183: *4* g.match_ignoring_case 

4728def match_ignoring_case(s1: str, s2: str) -> bool: 

4729 return bool(s1 and s2 and s1.lower() == s2.lower()) 

4730#@+node:ekr.20031218072017.3184: *4* g.match_word & g.match_words 

4731def match_word(s: str, i: int, pattern: str) -> bool: 

4732 

4733 # Using a regex is surprisingly tricky. 

4734 if pattern is None: 

4735 return False 

4736 if i > 0 and g.isWordChar(s[i - 1]): # Bug fix: 2017/06/01. 

4737 return False 

4738 j = len(pattern) 

4739 if j == 0: 

4740 return False 

4741 if s.find(pattern, i, i + j) != i: 

4742 return False 

4743 if i + j >= len(s): 

4744 return True 

4745 ch = s[i + j] 

4746 return not g.isWordChar(ch) 

4747 

4748def match_words(s: str, i: int, patterns: Sequence[str]) -> bool: 

4749 return any(g.match_word(s, i, pattern) for pattern in patterns) 

4750#@+node:ekr.20031218072017.3185: *4* g.skip_blank_lines 

4751# This routine differs from skip_ws_and_nl in that 

4752# it does not advance over whitespace at the start 

4753# of a non-empty or non-nl terminated line 

4754 

4755def skip_blank_lines(s: str, i: int) -> int: 

4756 while i < len(s): 

4757 if g.is_nl(s, i): 

4758 i = g.skip_nl(s, i) 

4759 elif g.is_ws(s[i]): 

4760 j = g.skip_ws(s, i) 

4761 if g.is_nl(s, j): 

4762 i = j 

4763 else: break 

4764 else: break 

4765 return i 

4766#@+node:ekr.20031218072017.3186: *4* g.skip_c_id 

4767def skip_c_id(s: str, i: int) -> int: 

4768 n = len(s) 

4769 while i < n and g.isWordChar(s[i]): 

4770 i += 1 

4771 return i 

4772#@+node:ekr.20040705195048: *4* g.skip_id 

4773def skip_id(s: str, i: int, chars: str=None) -> int: 

4774 chars = g.toUnicode(chars) if chars else '' 

4775 n = len(s) 

4776 while i < n and (g.isWordChar(s[i]) or s[i] in chars): 

4777 i += 1 

4778 return i 

4779#@+node:ekr.20031218072017.3187: *4* g.skip_line, skip_to_start/end_of_line 

4780#@+at These methods skip to the next newline, regardless of whether the 

4781# newline may be preceeded by a backslash. Consequently, they should be 

4782# used only when we know that we are not in a preprocessor directive or 

4783# string. 

4784#@@c 

4785 

4786def skip_line(s: str, i: int) -> int: 

4787 if i >= len(s): 

4788 return len(s) 

4789 if i < 0: 

4790 i = 0 

4791 i = s.find('\n', i) 

4792 if i == -1: 

4793 return len(s) 

4794 return i + 1 

4795 

4796def skip_to_end_of_line(s: str, i: int) -> int: 

4797 if i >= len(s): 

4798 return len(s) 

4799 if i < 0: 

4800 i = 0 

4801 i = s.find('\n', i) 

4802 if i == -1: 

4803 return len(s) 

4804 return i 

4805 

4806def skip_to_start_of_line(s: str, i: int) -> int: 

4807 if i >= len(s): 

4808 return len(s) 

4809 if i <= 0: 

4810 return 0 

4811 # Don't find s[i], so it doesn't matter if s[i] is a newline. 

4812 i = s.rfind('\n', 0, i) 

4813 if i == -1: 

4814 return 0 

4815 return i + 1 

4816#@+node:ekr.20031218072017.3188: *4* g.skip_long 

4817def skip_long(s: str, i: int) -> Tuple[int, Optional[int]]: 

4818 """ 

4819 Scan s[i:] for a valid int. 

4820 Return (i, val) or (i, None) if s[i] does not point at a number. 

4821 """ 

4822 val = 0 

4823 i = g.skip_ws(s, i) 

4824 n = len(s) 

4825 if i >= n or (not s[i].isdigit() and s[i] not in '+-'): 

4826 return i, None 

4827 j = i 

4828 if s[i] in '+-': # Allow sign before the first digit 

4829 i += 1 

4830 while i < n and s[i].isdigit(): 

4831 i += 1 

4832 try: # There may be no digits. 

4833 val = int(s[j:i]) 

4834 return i, val 

4835 except Exception: 

4836 return i, None 

4837#@+node:ekr.20031218072017.3190: *4* g.skip_nl 

4838# We need this function because different systems have different end-of-line conventions. 

4839 

4840def skip_nl(s: str, i: int) -> int: 

4841 """Skips a single "logical" end-of-line character.""" 

4842 if g.match(s, i, "\r\n"): 

4843 return i + 2 

4844 if g.match(s, i, '\n') or g.match(s, i, '\r'): 

4845 return i + 1 

4846 return i 

4847#@+node:ekr.20031218072017.3191: *4* g.skip_non_ws 

4848def skip_non_ws(s: str, i: int) -> int: 

4849 n = len(s) 

4850 while i < n and not g.is_ws(s[i]): 

4851 i += 1 

4852 return i 

4853#@+node:ekr.20031218072017.3192: *4* g.skip_pascal_braces 

4854# Skips from the opening { to the matching }. 

4855 

4856def skip_pascal_braces(s: str, i: int) -> int: 

4857 # No constructs are recognized inside Pascal block comments! 

4858 if i == -1: 

4859 return len(s) 

4860 return s.find('}', i) 

4861#@+node:ekr.20031218072017.3170: *4* g.skip_python_string 

4862def skip_python_string(s: str, i: int) -> int: 

4863 if g.match(s, i, "'''") or g.match(s, i, '"""'): 

4864 delim = s[i] * 3 

4865 i += 3 

4866 k = s.find(delim, i) 

4867 if k > -1: 

4868 return k + 3 

4869 return len(s) 

4870 return g.skip_string(s, i) 

4871#@+node:ekr.20031218072017.2369: *4* g.skip_string 

4872def skip_string(s: str, i: int) -> int: 

4873 """Scan forward to the end of a string.""" 

4874 delim = s[i] 

4875 i += 1 

4876 assert delim in '\'"', (repr(delim), repr(s)) 

4877 n = len(s) 

4878 while i < n and s[i] != delim: 

4879 if s[i] == '\\': 

4880 i += 2 

4881 else: 

4882 i += 1 

4883 if i >= n: 

4884 pass 

4885 elif s[i] == delim: 

4886 i += 1 

4887 return i 

4888#@+node:ekr.20031218072017.3193: *4* g.skip_to_char 

4889def skip_to_char(s: str, i: int, ch: str) -> Tuple[int, str]: 

4890 j = s.find(ch, i) 

4891 if j == -1: 

4892 return len(s), s[i:] 

4893 return j, s[i:j] 

4894#@+node:ekr.20031218072017.3194: *4* g.skip_ws, skip_ws_and_nl 

4895def skip_ws(s: str, i: int) -> int: 

4896 n = len(s) 

4897 while i < n and g.is_ws(s[i]): 

4898 i += 1 

4899 return i 

4900 

4901def skip_ws_and_nl(s: str, i: int) -> int: 

4902 n = len(s) 

4903 while i < n and (g.is_ws(s[i]) or g.is_nl(s, i)): 

4904 i += 1 

4905 return i 

4906#@+node:ekr.20170414034616.1: ** g.Git 

4907#@+node:ekr.20180325025502.1: *3* g.backupGitIssues 

4908def backupGitIssues(c: Cmdr, base_url: str=None) -> None: 

4909 """Get a list of issues from Leo's GitHub site.""" 

4910 if base_url is None: 

4911 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues' 

4912 

4913 root = c.lastTopLevel().insertAfter() 

4914 root.h = f'Backup of issues: {time.strftime("%Y/%m/%d")}' 

4915 label_list: List[str] = [] 

4916 GitIssueController().backup_issues(base_url, c, label_list, root) 

4917 root.expand() 

4918 c.selectPosition(root) 

4919 c.redraw() 

4920 g.trace('done') 

4921#@+node:ekr.20170616102324.1: *3* g.execGitCommand 

4922def execGitCommand(command: str, directory: str) -> List[str]: 

4923 """Execute the given git command in the given directory.""" 

4924 git_dir = g.os_path_finalize_join(directory, '.git') 

4925 if not g.os_path_exists(git_dir): 

4926 g.trace('not found:', git_dir, g.callers()) 

4927 return [] 

4928 if '\n' in command: 

4929 g.trace('removing newline from', command) 

4930 command = command.replace('\n', '') 

4931 # #1777: Save/restore os.curdir 

4932 old_dir = os.getcwd() 

4933 if directory: 

4934 os.chdir(directory) 

4935 try: 

4936 p = subprocess.Popen( 

4937 shlex.split(command), 

4938 stdout=subprocess.PIPE, 

4939 stderr=None, # Shows error traces. 

4940 shell=False, 

4941 ) 

4942 out, err = p.communicate() 

4943 lines = [g.toUnicode(z) for z in g.splitLines(out or [])] 

4944 finally: 

4945 os.chdir(old_dir) 

4946 return lines 

4947#@+node:ekr.20180126043905.1: *3* g.getGitIssues 

4948def getGitIssues(c: Cmdr, 

4949 base_url: str=None, 

4950 label_list: List=None, 

4951 milestone: str=None, 

4952 state: Optional[str]=None, # in (None, 'closed', 'open') 

4953) -> None: 

4954 """Get a list of issues from Leo's GitHub site.""" 

4955 if base_url is None: 

4956 base_url = 'https://api.github.com/repos/leo-editor/leo-editor/issues' 

4957 if isinstance(label_list, (list, tuple)): 

4958 root = c.lastTopLevel().insertAfter() 

4959 root.h = 'Issues for ' + milestone if milestone else 'Backup' 

4960 GitIssueController().backup_issues(base_url, c, label_list, root) 

4961 root.expand() 

4962 c.selectPosition(root) 

4963 c.redraw() 

4964 g.trace('done') 

4965 else: 

4966 g.trace('label_list must be a list or tuple', repr(label_list)) 

4967#@+node:ekr.20180126044602.1: *4* class GitIssueController 

4968class GitIssueController: 

4969 """ 

4970 A class encapsulating the retrieval of GitHub issues. 

4971 

4972 The GitHub api: https://developer.github.com/v3/issues/ 

4973 """ 

4974 #@+others 

4975 #@+node:ekr.20180325023336.1: *5* git.backup_issues 

4976 def backup_issues(self, base_url: str, c: Cmdr, label_list: List, root: Pos, state: Any=None) -> None: 

4977 

4978 self.base_url = base_url 

4979 self.root = root 

4980 self.milestone = None 

4981 if label_list: 

4982 for state in ('closed', 'open'): 

4983 for label in label_list: 

4984 self.get_one_issue(label, state) 

4985 elif state is None: 

4986 for state in ('closed', 'open'): 

4987 organizer = root.insertAsLastChild() 

4988 organizer.h = f"{state} issues..." 

4989 self.get_all_issues(label_list, organizer, state) 

4990 elif state in ('closed', 'open'): 

4991 self.get_all_issues(label_list, root, state) 

4992 else: 

4993 g.es_print('state must be in (None, "open", "closed")') 

4994 #@+node:ekr.20180325024334.1: *5* git.get_all_issues 

4995 def get_all_issues(self, label_list: List, root: Pos, state: Any, limit: int=100) -> None: 

4996 """Get all issues for the base url.""" 

4997 try: 

4998 import requests 

4999 except Exception: 

5000 g.trace('requests not found: `pip install requests`') 

5001 return 

5002 label = None 

5003 assert state in ('open', 'closed') 

5004 page_url = self.base_url + '?&state=%s&page=%s' 

5005 page, total = 1, 0 

5006 while True: 

5007 url = page_url % (state, page) 

5008 r = requests.get(url) 

5009 try: 

5010 done, n = self.get_one_page(label, page, r, root) 

5011 # Do not remove this trace. It's reassuring. 

5012 g.trace(f"done: {done:5} page: {page:3} found: {n} label: {label}") 

5013 except AttributeError: 

5014 g.trace('Possible rate limit') 

5015 self.print_header(r) 

5016 g.es_exception() 

5017 break 

5018 total += n 

5019 if done: 

5020 break 

5021 page += 1 

5022 if page > limit: 

5023 g.trace('too many pages') 

5024 break 

5025 #@+node:ekr.20180126044850.1: *5* git.get_issues 

5026 def get_issues(self, base_url: str, label_list: List, milestone: Any, root: Pos, state: Any) -> None: 

5027 """Create a list of issues for each label in label_list.""" 

5028 self.base_url = base_url 

5029 self.milestone = milestone 

5030 self.root = root 

5031 for label in label_list: 

5032 self.get_one_issue(label, state) 

5033 #@+node:ekr.20180126043719.3: *5* git.get_one_issue 

5034 def get_one_issue(self, label: str, state: Any, limit: int=20) -> None: 

5035 """Create a list of issues with the given label.""" 

5036 try: 

5037 import requests 

5038 except Exception: 

5039 g.trace('requests not found: `pip install requests`') 

5040 return 

5041 root = self.root.insertAsLastChild() 

5042 page, total = 1, 0 

5043 page_url = self.base_url + '?labels=%s&state=%s&page=%s' 

5044 while True: 

5045 url = page_url % (label, state, page) 

5046 r = requests.get(url) 

5047 try: 

5048 done, n = self.get_one_page(label, page, r, root) 

5049 # Do not remove this trace. It's reassuring. 

5050 g.trace(f"done: {done:5} page: {page:3} found: {n:3} label: {label}") 

5051 except AttributeError: 

5052 g.trace('Possible rate limit') 

5053 self.print_header(r) 

5054 g.es_exception() 

5055 break 

5056 total += n 

5057 if done: 

5058 break 

5059 page += 1 

5060 if page > limit: 

5061 g.trace('too many pages') 

5062 break 

5063 state = state.capitalize() 

5064 if self.milestone: 

5065 root.h = f"{total} {state} {label} issues for milestone {self.milestone}" 

5066 else: 

5067 root.h = f"{total} {state} {label} issues" 

5068 #@+node:ekr.20180126043719.4: *5* git.get_one_page 

5069 def get_one_page(self, label: str, page: int, r: Any, root: Pos) -> Tuple[bool, int]: 

5070 

5071 if self.milestone: 

5072 aList = [ 

5073 z for z in r.json() 

5074 if z.get('milestone') is not None and 

5075 self.milestone == z.get('milestone').get('title') 

5076 ] 

5077 else: 

5078 aList = [z for z in r.json()] 

5079 for d in aList: 

5080 n, title = d.get('number'), d.get('title') 

5081 html_url = d.get('html_url') or self.base_url 

5082 p = root.insertAsNthChild(0) 

5083 p.h = f"#{n}: {title}" 

5084 p.b = f"{html_url}\n\n" 

5085 p.b += d.get('body').strip() 

5086 link = r.headers.get('Link') 

5087 done = not link or link.find('rel="next"') == -1 

5088 return done, len(aList) 

5089 #@+node:ekr.20180127092201.1: *5* git.print_header 

5090 def print_header(self, r: Any) -> None: 

5091 

5092 # r.headers is a CaseInsensitiveDict 

5093 # so g.printObj(r.headers) is just repr(r.headers) 

5094 if 0: 

5095 print('Link', r.headers.get('Link')) 

5096 else: 

5097 for key in r.headers: 

5098 print(f"{key:35}: {r.headers.get(key)}") 

5099 #@-others 

5100#@+node:ekr.20190428173354.1: *3* g.getGitVersion 

5101def getGitVersion(directory: str=None) -> Tuple[str, str, str]: 

5102 """Return a tuple (author, build, date) from the git log, or None.""" 

5103 # 

5104 # -n: Get only the last log. 

5105 trace = 'git' in g.app.debug 

5106 try: 

5107 s = subprocess.check_output( 

5108 'git log -n 1 --date=iso', 

5109 cwd=directory or g.app.loadDir, 

5110 stderr=subprocess.DEVNULL, 

5111 shell=True, 

5112 ) 

5113 # #1209. 

5114 except subprocess.CalledProcessError as e: 

5115 s = e.output 

5116 if trace: 

5117 g.trace('return code', e.returncode) 

5118 g.trace('value', repr(s)) 

5119 g.es_print('Exception in g.getGitVersion') 

5120 g.es_exception() 

5121 s = g.toUnicode(s) 

5122 if not isinstance(s, str): 

5123 return '', '', '' 

5124 except Exception: 

5125 if trace: 

5126 g.es_print('Exception in g.getGitVersion') 

5127 g.es_exception() 

5128 return '', '', '' 

5129 

5130 info = [g.toUnicode(z) for z in s.splitlines()] 

5131 

5132 def find(kind: str) -> str: 

5133 """Return the given type of log line.""" 

5134 for z in info: 

5135 if z.startswith(kind): 

5136 return z.lstrip(kind).lstrip(':').strip() 

5137 return '' 

5138 

5139 return find('Author'), find('commit')[:10], find('Date') 

5140#@+node:ekr.20170414034616.2: *3* g.gitBranchName 

5141def gitBranchName(path: str=None) -> str: 

5142 """ 

5143 Return the git branch name 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 branch 

5149#@+node:ekr.20170414034616.4: *3* g.gitCommitNumber 

5150def gitCommitNumber(path: str=None) -> str: 

5151 """ 

5152 Return the git commit number associated with path/.git, or the empty 

5153 string if path/.git does not exist. If path is None, use the leo-editor 

5154 directory. 

5155 """ 

5156 branch, commit = g.gitInfo(path) 

5157 return commit 

5158#@+node:ekr.20200724132432.1: *3* g.gitInfoForFile 

5159def gitInfoForFile(filename: str) -> Tuple[str, str]: 

5160 """ 

5161 Return the git (branch, commit) info associated for the given file. 

5162 """ 

5163 # g.gitInfo and g.gitHeadPath now do all the work. 

5164 return g.gitInfo(filename) 

5165#@+node:ekr.20200724133754.1: *3* g.gitInfoForOutline 

5166def gitInfoForOutline(c: Cmdr) -> Tuple[str, str]: 

5167 """ 

5168 Return the git (branch, commit) info associated for commander c. 

5169 """ 

5170 return g.gitInfoForFile(c.fileName()) 

5171#@+node:maphew.20171112205129.1: *3* g.gitDescribe 

5172def gitDescribe(path: str=None) -> Tuple[str, str, str]: 

5173 """ 

5174 Return the Git tag, distance-from-tag, and commit hash for the 

5175 associated path. If path is None, use the leo-editor directory. 

5176 

5177 Given `git describe` cmd line output: `x-leo-v5.6-55-ge1129da\n` 

5178 This function returns ('x-leo-v5.6', '55', 'e1129da') 

5179 """ 

5180 describe = g.execGitCommand('git describe --tags --long', path) 

5181 # rsplit not split, as '-' might be in tag name. 

5182 tag, distance, commit = describe[0].rsplit('-', 2) 

5183 if 'g' in commit[0:]: 

5184 # leading 'g' isn't part of the commit hash. 

5185 commit = commit[1:] 

5186 commit = commit.rstrip() 

5187 return tag, distance, commit 

5188#@+node:ekr.20170414034616.6: *3* g.gitHeadPath 

5189def gitHeadPath(path_s: str) -> Optional[str]: 

5190 """ 

5191 Compute the path to .git/HEAD given the path. 

5192 """ 

5193 path = Path(path_s) 

5194 # #1780: Look up the directory tree, looking the .git directory. 

5195 while os.path.exists(path): 

5196 head = os.path.join(path, '.git', 'HEAD') 

5197 if os.path.exists(head): 

5198 return head 

5199 if path == path.parent: 

5200 break 

5201 path = path.parent 

5202 return None 

5203#@+node:ekr.20170414034616.3: *3* g.gitInfo 

5204def gitInfo(path: str=None) -> Tuple[str, str]: 

5205 """ 

5206 Path may be a directory or file. 

5207 

5208 Return the branch and commit number or ('', ''). 

5209 """ 

5210 branch, commit = '', '' # Set defaults. 

5211 if path is None: 

5212 # Default to leo/core. 

5213 path = os.path.dirname(__file__) 

5214 if not os.path.isdir(path): 

5215 path = os.path.dirname(path) 

5216 # Does path/../ref exist? 

5217 path = g.gitHeadPath(path) 

5218 if not path: 

5219 return branch, commit 

5220 try: 

5221 with open(path) as f: 

5222 s = f.read() 

5223 if not s.startswith('ref'): 

5224 branch = 'None' 

5225 commit = s[:7] 

5226 return branch, commit 

5227 # On a proper branch 

5228 pointer = s.split()[1] 

5229 dirs = pointer.split('/') 

5230 branch = dirs[-1] 

5231 except IOError: 

5232 g.trace('can not open:', path) 

5233 return branch, commit 

5234 # Try to get a better commit number. 

5235 git_dir = g.os_path_finalize_join(path, '..') 

5236 try: 

5237 path = g.os_path_finalize_join(git_dir, pointer) 

5238 with open(path) as f: # type:ignore 

5239 s = f.read() 

5240 commit = s.strip()[0:12] 

5241 # shorten the hash to a unique shortname 

5242 except IOError: 

5243 try: 

5244 path = g.os_path_finalize_join(git_dir, 'packed-refs') 

5245 with open(path) as f: # type:ignore 

5246 for line in f: 

5247 if line.strip().endswith(' ' + pointer): 

5248 commit = line.split()[0][0:12] 

5249 break 

5250 except IOError: 

5251 pass 

5252 return branch, commit 

5253#@+node:ekr.20031218072017.3139: ** g.Hooks & Plugins 

5254#@+node:ekr.20101028131948.5860: *3* g.act_on_node 

5255def dummy_act_on_node(c: Cmdr, p: Pos, event: Any) -> None: 

5256 pass 

5257 

5258# This dummy definition keeps pylint happy. 

5259# Plugins can change this. 

5260 

5261act_on_node = dummy_act_on_node 

5262#@+node:ville.20120502221057.7500: *3* g.childrenModifiedSet, g.contentModifiedSet 

5263childrenModifiedSet: Set["VNode"] = set() 

5264contentModifiedSet: Set["VNode"] = set() 

5265#@+node:ekr.20031218072017.1596: *3* g.doHook 

5266def doHook(tag: str, *args: Any, **keywords: Any) -> Any: 

5267 """ 

5268 This global function calls a hook routine. Hooks are identified by the 

5269 tag param. 

5270 

5271 Returns the value returned by the hook routine, or None if the there is 

5272 an exception. 

5273 

5274 We look for a hook routine in three places: 

5275 1. c.hookFunction 

5276 2. app.hookFunction 

5277 3. leoPlugins.doPlugins() 

5278 

5279 Set app.hookError on all exceptions. 

5280 Scripts may reset app.hookError to try again. 

5281 """ 

5282 if g.app.killed or g.app.hookError: 

5283 return None 

5284 if args: 

5285 # A minor error in Leo's core. 

5286 g.pr(f"***ignoring args param. tag = {tag}") 

5287 if not g.app.config.use_plugins: 

5288 if tag in ('open0', 'start1'): 

5289 g.warning("Plugins disabled: use_plugins is 0 in a leoSettings.leo file.") 

5290 return None 

5291 # Get the hook handler function. Usually this is doPlugins. 

5292 c = keywords.get("c") 

5293 # pylint: disable=consider-using-ternary 

5294 f = (c and c.hookFunction) or g.app.hookFunction 

5295 if not f: 

5296 g.app.hookFunction = f = g.app.pluginsController.doPlugins 

5297 try: 

5298 # Pass the hook to the hook handler. 

5299 # g.pr('doHook',f.__name__,keywords.get('c')) 

5300 return f(tag, keywords) 

5301 except Exception: 

5302 g.es_exception() 

5303 g.app.hookError = True # Supress this function. 

5304 g.app.idle_time_hooks_enabled = False 

5305 return None 

5306#@+node:ekr.20100910075900.5950: *3* g.Wrappers for g.app.pluginController methods 

5307# Important: we can not define g.pc here! 

5308#@+node:ekr.20100910075900.5951: *4* g.Loading & registration 

5309def loadOnePlugin(pluginName: str, verbose: bool=False) -> Any: 

5310 pc = g.app.pluginsController 

5311 return pc.loadOnePlugin(pluginName, verbose=verbose) 

5312 

5313def registerExclusiveHandler(tags: List[str], fn: str) -> Any: 

5314 pc = g.app.pluginsController 

5315 return pc.registerExclusiveHandler(tags, fn) 

5316 

5317def registerHandler(tags: Any, fn: Any) -> Any: 

5318 pc = g.app.pluginsController 

5319 return pc.registerHandler(tags, fn) 

5320 

5321def plugin_signon(module_name: str, verbose: bool=False) -> Any: 

5322 pc = g.app.pluginsController 

5323 return pc.plugin_signon(module_name, verbose) 

5324 

5325def unloadOnePlugin(moduleOrFileName: str, verbose: bool=False) -> Any: 

5326 pc = g.app.pluginsController 

5327 return pc.unloadOnePlugin(moduleOrFileName, verbose) 

5328 

5329def unregisterHandler(tags: Any, fn: Any) -> Any: 

5330 pc = g.app.pluginsController 

5331 return pc.unregisterHandler(tags, fn) 

5332#@+node:ekr.20100910075900.5952: *4* g.Information 

5333def getHandlersForTag(tags: List[str]) -> List: 

5334 pc = g.app.pluginsController 

5335 return pc.getHandlersForTag(tags) 

5336 

5337def getLoadedPlugins() -> List: 

5338 pc = g.app.pluginsController 

5339 return pc.getLoadedPlugins() 

5340 

5341def getPluginModule(moduleName: str) -> Any: 

5342 pc = g.app.pluginsController 

5343 return pc.getPluginModule(moduleName) 

5344 

5345def pluginIsLoaded(fn: str) -> bool: 

5346 pc = g.app.pluginsController 

5347 return pc.isLoaded(fn) 

5348#@+node:ekr.20031218072017.1315: ** g.Idle time functions 

5349#@+node:EKR.20040602125018.1: *3* g.disableIdleTimeHook 

5350def disableIdleTimeHook() -> None: 

5351 """Disable the global idle-time hook.""" 

5352 g.app.idle_time_hooks_enabled = False 

5353#@+node:EKR.20040602125018: *3* g.enableIdleTimeHook 

5354def enableIdleTimeHook(*args: Any, **keys: Any) -> None: 

5355 """Enable idle-time processing.""" 

5356 g.app.idle_time_hooks_enabled = True 

5357#@+node:ekr.20140825042850.18410: *3* g.IdleTime 

5358def IdleTime(handler: Any, delay: int=500, tag: str=None) -> Any: 

5359 """ 

5360 A thin wrapper for the LeoQtGui.IdleTime class. 

5361 

5362 The IdleTime class executes a handler with a given delay at idle time. 

5363 The handler takes a single argument, the IdleTime instance:: 

5364 

5365 def handler(timer): 

5366 '''IdleTime handler. timer is an IdleTime instance.''' 

5367 delta_t = timer.time-timer.starting_time 

5368 g.trace(timer.count, '%2.4f' % (delta_t)) 

5369 if timer.count >= 5: 

5370 g.trace('done') 

5371 timer.stop() 

5372 

5373 # Execute handler every 500 msec. at idle time. 

5374 timer = g.IdleTime(handler,delay=500) 

5375 if timer: timer.start() 

5376 

5377 Timer instances are completely independent:: 

5378 

5379 def handler1(timer): 

5380 delta_t = timer.time-timer.starting_time 

5381 g.trace('%2s %2.4f' % (timer.count,delta_t)) 

5382 if timer.count >= 5: 

5383 g.trace('done') 

5384 timer.stop() 

5385 

5386 def handler2(timer): 

5387 delta_t = timer.time-timer.starting_time 

5388 g.trace('%2s %2.4f' % (timer.count,delta_t)) 

5389 if timer.count >= 10: 

5390 g.trace('done') 

5391 timer.stop() 

5392 

5393 timer1 = g.IdleTime(handler1, delay=500) 

5394 timer2 = g.IdleTime(handler2, delay=1000) 

5395 if timer1 and timer2: 

5396 timer1.start() 

5397 timer2.start() 

5398 """ 

5399 try: 

5400 return g.app.gui.idleTimeClass(handler, delay, tag) 

5401 except Exception: 

5402 return None 

5403#@+node:ekr.20161027205025.1: *3* g.idleTimeHookHandler (stub) 

5404def idleTimeHookHandler(timer: Any) -> None: 

5405 """This function exists for compatibility.""" 

5406 g.es_print('Replaced by IdleTimeManager.on_idle') 

5407 g.trace(g.callers()) 

5408#@+node:ekr.20041219095213: ** g.Importing 

5409#@+node:ekr.20040917061619: *3* g.cantImport 

5410def cantImport(moduleName: str, pluginName: str=None, verbose: bool=True) -> None: 

5411 """Print a "Can't Import" message and return None.""" 

5412 s = f"Can not import {moduleName}" 

5413 if pluginName: 

5414 s = s + f" from {pluginName}" 

5415 if not g.app or not g.app.gui: 

5416 print(s) 

5417 elif g.unitTesting: 

5418 return 

5419 else: 

5420 g.warning('', s) 

5421#@+node:ekr.20191220044128.1: *3* g.import_module 

5422def import_module(name: str, package: str=None) -> Any: 

5423 """ 

5424 A thin wrapper over importlib.import_module. 

5425 """ 

5426 trace = 'plugins' in g.app.debug and not g.unitTesting 

5427 exceptions = [] 

5428 try: 

5429 m = importlib.import_module(name, package=package) 

5430 except Exception as e: 

5431 m = None 

5432 if trace: 

5433 t, v, tb = sys.exc_info() 

5434 del tb # don't need the traceback 

5435 # In case v is empty, we'll at least have the execption type 

5436 v = v or str(t) # type:ignore 

5437 if v not in exceptions: 

5438 exceptions.append(v) 

5439 g.trace(f"Can not import {name}: {e}") 

5440 return m 

5441#@+node:ekr.20140711071454.17650: ** g.Indices, Strings, Unicode & Whitespace 

5442#@+node:ekr.20140711071454.17647: *3* g.Indices 

5443#@+node:ekr.20050314140957: *4* g.convertPythonIndexToRowCol 

5444def convertPythonIndexToRowCol(s: str, i: int) -> Tuple[int, int]: 

5445 """Convert index i into string s into zero-based row/col indices.""" 

5446 if not s or i <= 0: 

5447 return 0, 0 

5448 i = min(i, len(s)) 

5449 # works regardless of what s[i] is 

5450 row = s.count('\n', 0, i) # Don't include i 

5451 if row == 0: 

5452 return row, i 

5453 prevNL = s.rfind('\n', 0, i) # Don't include i 

5454 return row, i - prevNL - 1 

5455#@+node:ekr.20050315071727: *4* g.convertRowColToPythonIndex 

5456def convertRowColToPythonIndex(s: str, row: int, col: int, lines: List[str]=None) -> int: 

5457 """Convert zero-based row/col indices into a python index into string s.""" 

5458 if row < 0: 

5459 return 0 

5460 if lines is None: 

5461 lines = g.splitLines(s) 

5462 if row >= len(lines): 

5463 return len(s) 

5464 col = min(col, len(lines[row])) 

5465 # A big bottleneck 

5466 prev = 0 

5467 for line in lines[:row]: 

5468 prev += len(line) 

5469 return prev + col 

5470#@+node:ekr.20061031102333.2: *4* g.getWord & getLine 

5471def getWord(s: str, i: int) -> Tuple[int, int]: 

5472 """Return i,j such that s[i:j] is the word surrounding s[i].""" 

5473 if i >= len(s): 

5474 i = len(s) - 1 

5475 if i < 0: 

5476 i = 0 

5477 # Scan backwards. 

5478 while 0 <= i < len(s) and g.isWordChar(s[i]): 

5479 i -= 1 

5480 i += 1 

5481 # Scan forwards. 

5482 j = i 

5483 while 0 <= j < len(s) and g.isWordChar(s[j]): 

5484 j += 1 

5485 return i, j 

5486 

5487def getLine(s: str, i: int) -> Tuple[int, int]: 

5488 """ 

5489 Return i,j such that s[i:j] is the line surrounding s[i]. 

5490 s[i] is a newline only if the line is empty. 

5491 s[j] is a newline unless there is no trailing newline. 

5492 """ 

5493 if i > len(s): 

5494 i = len(s) - 1 

5495 if i < 0: 

5496 i = 0 

5497 # A newline *ends* the line, so look to the left of a newline. 

5498 j = s.rfind('\n', 0, i) 

5499 if j == -1: 

5500 j = 0 

5501 else: 

5502 j += 1 

5503 k = s.find('\n', i) 

5504 if k == -1: 

5505 k = len(s) 

5506 else: 

5507 k = k + 1 

5508 return j, k 

5509#@+node:ekr.20111114151846.9847: *4* g.toPythonIndex 

5510def toPythonIndex(s: str, index: int) -> int: 

5511 """ 

5512 Convert index to a Python int. 

5513 

5514 index may be a Tk index (x.y) or 'end'. 

5515 """ 

5516 if index is None: 

5517 return 0 

5518 if isinstance(index, int): 

5519 return index 

5520 if index == '1.0': 

5521 return 0 

5522 if index == 'end': 

5523 return len(s) 

5524 data = index.split('.') 

5525 if len(data) == 2: 

5526 row, col = data 

5527 row, col = int(row), int(col) 

5528 i = g.convertRowColToPythonIndex(s, row - 1, col) 

5529 return i 

5530 g.trace(f"bad string index: {index}") 

5531 return 0 

5532#@+node:ekr.20140526144610.17601: *3* g.Strings 

5533#@+node:ekr.20190503145501.1: *4* g.isascii 

5534def isascii(s: str) -> bool: 

5535 # s.isascii() is defined in Python 3.7. 

5536 return all(ord(ch) < 128 for ch in s) 

5537#@+node:ekr.20031218072017.3106: *4* g.angleBrackets & virtual_event_name 

5538def angleBrackets(s: str) -> str: 

5539 """Returns < < s > >""" 

5540 lt = "<<" 

5541 rt = ">>" 

5542 return lt + s + rt 

5543 

5544virtual_event_name = angleBrackets 

5545#@+node:ekr.20090516135452.5777: *4* g.ensureLeading/TrailingNewlines 

5546def ensureLeadingNewlines(s: str, n: int) -> str: 

5547 s = g.removeLeading(s, '\t\n\r ') 

5548 return ('\n' * n) + s 

5549 

5550def ensureTrailingNewlines(s: str, n: int) -> str: 

5551 s = g.removeTrailing(s, '\t\n\r ') 

5552 return s + '\n' * n 

5553#@+node:ekr.20050920084036.4: *4* g.longestCommonPrefix & g.itemsMatchingPrefixInList 

5554def longestCommonPrefix(s1: str, s2: str) -> str: 

5555 """Find the longest prefix common to strings s1 and s2.""" 

5556 prefix = '' 

5557 for ch in s1: 

5558 if s2.startswith(prefix + ch): 

5559 prefix = prefix + ch 

5560 else: 

5561 return prefix 

5562 return prefix 

5563 

5564def itemsMatchingPrefixInList(s: str, aList: List[str], matchEmptyPrefix: bool=False) -> Tuple[List, str]: 

5565 """This method returns a sorted list items of aList whose prefix is s. 

5566 

5567 It also returns the longest common prefix of all the matches. 

5568 """ 

5569 if s: 

5570 pmatches = [a for a in aList if a.startswith(s)] 

5571 elif matchEmptyPrefix: 

5572 pmatches = aList[:] 

5573 else: pmatches = [] 

5574 if pmatches: 

5575 pmatches.sort() 

5576 common_prefix = reduce(g.longestCommonPrefix, pmatches) 

5577 else: 

5578 common_prefix = '' 

5579 return pmatches, common_prefix 

5580#@+node:ekr.20090516135452.5776: *4* g.removeLeading/Trailing 

5581# Warning: g.removeTrailingWs already exists. 

5582# Do not change it! 

5583 

5584def removeLeading(s: str, chars: str) -> str: 

5585 """Remove all characters in chars from the front of s.""" 

5586 i = 0 

5587 while i < len(s) and s[i] in chars: 

5588 i += 1 

5589 return s[i:] 

5590 

5591def removeTrailing(s: str, chars: str) -> str: 

5592 """Remove all characters in chars from the end of s.""" 

5593 i = len(s) - 1 

5594 while i >= 0 and s[i] in chars: 

5595 i -= 1 

5596 i += 1 

5597 return s[:i] 

5598#@+node:ekr.20060410112600: *4* g.stripBrackets 

5599def stripBrackets(s: str) -> str: 

5600 """Strip leading and trailing angle brackets.""" 

5601 if s.startswith('<'): 

5602 s = s[1:] 

5603 if s.endswith('>'): 

5604 s = s[:-1] 

5605 return s 

5606#@+node:ekr.20170317101100.1: *4* g.unCamel 

5607def unCamel(s: str) -> List[str]: 

5608 """Return a list of sub-words in camelCased string s.""" 

5609 result: List[str] = [] 

5610 word: List[str] = [] 

5611 for ch in s: 

5612 if ch.isalpha() and ch.isupper(): 

5613 if word: 

5614 result.append(''.join(word)) 

5615 word = [ch] 

5616 elif ch.isalpha(): 

5617 word.append(ch) 

5618 elif word: 

5619 result.append(''.join(word)) 

5620 word = [] 

5621 if word: 

5622 result.append(''.join(word)) 

5623 return result 

5624#@+node:ekr.20031218072017.1498: *3* g.Unicode 

5625#@+node:ekr.20190505052756.1: *4* g.checkUnicode 

5626checkUnicode_dict: Dict[str, bool] = {} 

5627 

5628def checkUnicode(s: str, encoding: str=None) -> str: 

5629 """ 

5630 Warn when converting bytes. Report *all* errors. 

5631 

5632 This method is meant to document defensive programming. We don't expect 

5633 these errors, but they might arise as the result of problems in 

5634 user-defined plugins or scripts. 

5635 """ 

5636 tag = 'g.checkUnicode' 

5637 if s is None and g.unitTesting: 

5638 return '' 

5639 if isinstance(s, str): 

5640 return s 

5641 if not isinstance(s, bytes): 

5642 g.error(f"{tag}: unexpected argument: {s!r}") 

5643 return '' 

5644 # 

5645 # Report the unexpected conversion. 

5646 callers = g.callers(1) 

5647 if callers not in checkUnicode_dict: 

5648 g.trace(g.callers()) 

5649 g.error(f"\n{tag}: expected unicode. got: {s!r}\n") 

5650 checkUnicode_dict[callers] = True 

5651 # 

5652 # Convert to unicode, reporting all errors. 

5653 if not encoding: 

5654 encoding = 'utf-8' 

5655 try: 

5656 s = s.decode(encoding, 'strict') 

5657 except(UnicodeDecodeError, UnicodeError): 

5658 # https://wiki.python.org/moin/UnicodeDecodeError 

5659 s = s.decode(encoding, 'replace') 

5660 g.trace(g.callers()) 

5661 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}") 

5662 except Exception: 

5663 g.trace(g.callers()) 

5664 g.es_excption() 

5665 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}") 

5666 return s 

5667#@+node:ekr.20100125073206.8709: *4* g.getPythonEncodingFromString 

5668def getPythonEncodingFromString(s: str) -> str: 

5669 """Return the encoding given by Python's encoding line. 

5670 s is the entire file. 

5671 """ 

5672 encoding = None 

5673 tag, tag2 = '# -*- coding:', '-*-' 

5674 n1, n2 = len(tag), len(tag2) 

5675 if s: 

5676 # For Python 3.x we must convert to unicode before calling startswith. 

5677 # The encoding doesn't matter: we only look at the first line, and if 

5678 # the first line is an encoding line, it will contain only ascii characters. 

5679 s = g.toUnicode(s, encoding='ascii', reportErrors=False) 

5680 lines = g.splitLines(s) 

5681 line1 = lines[0].strip() 

5682 if line1.startswith(tag) and line1.endswith(tag2): 

5683 e = line1[n1 : -n2].strip() 

5684 if e and g.isValidEncoding(e): 

5685 encoding = e 

5686 elif g.match_word(line1, 0, '@first'): # 2011/10/21. 

5687 line1 = line1[len('@first') :].strip() 

5688 if line1.startswith(tag) and line1.endswith(tag2): 

5689 e = line1[n1 : -n2].strip() 

5690 if e and g.isValidEncoding(e): 

5691 encoding = e 

5692 return encoding 

5693#@+node:ekr.20031218072017.1500: *4* g.isValidEncoding 

5694def isValidEncoding(encoding: str) -> bool: 

5695 """Return True if the encooding is valid.""" 

5696 if not encoding: 

5697 return False 

5698 if sys.platform == 'cli': 

5699 return True 

5700 try: 

5701 codecs.lookup(encoding) 

5702 return True 

5703 except LookupError: # Windows 

5704 return False 

5705 except AttributeError: # Linux 

5706 return False 

5707 except Exception: 

5708 # UnicodeEncodeError 

5709 g.es_print('Please report the following error') 

5710 g.es_exception() 

5711 return False 

5712#@+node:ekr.20061006152327: *4* g.isWordChar & g.isWordChar1 

5713def isWordChar(ch: str) -> bool: 

5714 """Return True if ch should be considered a letter.""" 

5715 return bool(ch and (ch.isalnum() or ch == '_')) 

5716 

5717def isWordChar1(ch: str) -> bool: 

5718 return bool(ch and (ch.isalpha() or ch == '_')) 

5719#@+node:ekr.20130910044521.11304: *4* g.stripBOM 

5720def stripBOM(s: str) -> Tuple[Optional[str], str]: 

5721 """ 

5722 If there is a BOM, return (e,s2) where e is the encoding 

5723 implied by the BOM and s2 is the s stripped of the BOM. 

5724 

5725 If there is no BOM, return (None,s) 

5726 

5727 s must be the contents of a file (a string) read in binary mode. 

5728 """ 

5729 table = ( 

5730 # Important: test longer bom's first. 

5731 (4, 'utf-32', codecs.BOM_UTF32_BE), 

5732 (4, 'utf-32', codecs.BOM_UTF32_LE), 

5733 (3, 'utf-8', codecs.BOM_UTF8), 

5734 (2, 'utf-16', codecs.BOM_UTF16_BE), 

5735 (2, 'utf-16', codecs.BOM_UTF16_LE), 

5736 ) 

5737 if s: 

5738 for n, e, bom in table: 

5739 assert len(bom) == n 

5740 if bom == s[: len(bom)]: 

5741 return e, s[len(bom) :] 

5742 return None, s 

5743#@+node:ekr.20050208093800: *4* g.toEncodedString 

5744def toEncodedString(s: str, encoding: str='utf-8', reportErrors: bool=False) -> bytes: 

5745 """Convert unicode string to an encoded string.""" 

5746 if not isinstance(s, str): 

5747 return s 

5748 if not encoding: 

5749 encoding = 'utf-8' 

5750 # These are the only significant calls to s.encode in Leo. 

5751 try: 

5752 s = s.encode(encoding, "strict") # type:ignore 

5753 except UnicodeError: 

5754 s = s.encode(encoding, "replace") # type:ignore 

5755 if reportErrors: 

5756 g.error(f"Error converting {s} from unicode to {encoding} encoding") 

5757 # Tracing these calls directly yields thousands of calls. 

5758 return s # type:ignore 

5759#@+node:ekr.20050208093800.1: *4* g.toUnicode 

5760unicode_warnings: Dict[str, bool] = {} # Keys are g.callers. 

5761 

5762def toUnicode(s: Any, encoding: str=None, reportErrors: bool=False) -> str: 

5763 """Convert bytes to unicode if necessary.""" 

5764 if isinstance(s, str): 

5765 return s 

5766 tag = 'g.toUnicode' 

5767 if not isinstance(s, bytes): 

5768 if not isinstance(s, (NullObject, TracingNullObject)): 

5769 callers = g.callers() 

5770 if callers not in unicode_warnings: 

5771 unicode_warnings[callers] = True 

5772 g.error(f"{tag}: unexpected argument of type {s.__class__.__name__}") 

5773 g.trace(callers) 

5774 return '' 

5775 if not encoding: 

5776 encoding = 'utf-8' 

5777 try: 

5778 s = s.decode(encoding, 'strict') 

5779 except(UnicodeDecodeError, UnicodeError): 

5780 # https://wiki.python.org/moin/UnicodeDecodeError 

5781 s = s.decode(encoding, 'replace') 

5782 if reportErrors: 

5783 g.error(f"{tag}: unicode error. encoding: {encoding!r}, s:\n{s!r}") 

5784 g.trace(g.callers()) 

5785 except Exception: 

5786 g.es_exception() 

5787 g.error(f"{tag}: unexpected error! encoding: {encoding!r}, s:\n{s!r}") 

5788 g.trace(g.callers()) 

5789 return s 

5790#@+node:ekr.20031218072017.3197: *3* g.Whitespace 

5791#@+node:ekr.20031218072017.3198: *4* g.computeLeadingWhitespace 

5792# Returns optimized whitespace corresponding to width with the indicated tab_width. 

5793 

5794def computeLeadingWhitespace(width: int, tab_width: int) -> str: 

5795 if width <= 0: 

5796 return "" 

5797 if tab_width > 1: 

5798 tabs = int(width / tab_width) 

5799 blanks = int(width % tab_width) 

5800 return ('\t' * tabs) + (' ' * blanks) 

5801 # Negative tab width always gets converted to blanks. 

5802 return ' ' * width 

5803#@+node:ekr.20120605172139.10263: *4* g.computeLeadingWhitespaceWidth 

5804# Returns optimized whitespace corresponding to width with the indicated tab_width. 

5805 

5806def computeLeadingWhitespaceWidth(s: str, tab_width: int) -> int: 

5807 w = 0 

5808 for ch in s: 

5809 if ch == ' ': 

5810 w += 1 

5811 elif ch == '\t': 

5812 w += (abs(tab_width) - (w % abs(tab_width))) 

5813 else: 

5814 break 

5815 return w 

5816#@+node:ekr.20031218072017.3199: *4* g.computeWidth 

5817# Returns the width of s, assuming s starts a line, with indicated tab_width. 

5818 

5819def computeWidth(s: str, tab_width: int) -> int: 

5820 w = 0 

5821 for ch in s: 

5822 if ch == '\t': 

5823 w += (abs(tab_width) - (w % abs(tab_width))) 

5824 elif ch == '\n': # Bug fix: 2012/06/05. 

5825 break 

5826 else: 

5827 w += 1 

5828 return w 

5829#@+node:ekr.20110727091744.15083: *4* g.wrap_lines (newer) 

5830#@@language rest 

5831#@+at 

5832# Important note: this routine need not deal with leading whitespace. 

5833# 

5834# Instead, the caller should simply reduce pageWidth by the width of 

5835# leading whitespace wanted, then add that whitespace to the lines 

5836# returned here. 

5837# 

5838# The key to this code is the invarient that line never ends in whitespace. 

5839#@@c 

5840#@@language python 

5841 

5842def wrap_lines(lines: List[str], pageWidth: int, firstLineWidth: int=None) -> List[str]: 

5843 """Returns a list of lines, consisting of the input lines wrapped to the given pageWidth.""" 

5844 if pageWidth < 10: 

5845 pageWidth = 10 

5846 # First line is special 

5847 if not firstLineWidth: 

5848 firstLineWidth = pageWidth 

5849 if firstLineWidth < 10: 

5850 firstLineWidth = 10 

5851 outputLineWidth = firstLineWidth 

5852 # Sentence spacing 

5853 # This should be determined by some setting, and can only be either 1 or 2 

5854 sentenceSpacingWidth = 1 

5855 assert 0 < sentenceSpacingWidth < 3 

5856 result = [] # The lines of the result. 

5857 line = "" # The line being formed. It never ends in whitespace. 

5858 for s in lines: 

5859 i = 0 

5860 while i < len(s): 

5861 assert len(line) <= outputLineWidth # DTHEIN 18-JAN-2004 

5862 j = g.skip_ws(s, i) 

5863 k = g.skip_non_ws(s, j) 

5864 word = s[j:k] 

5865 assert k > i 

5866 i = k 

5867 # DTHEIN 18-JAN-2004: wrap at exactly the text width, 

5868 # not one character less 

5869 # 

5870 wordLen = len(word) 

5871 if line.endswith('.') or line.endswith('?') or line.endswith('!'): 

5872 space = ' ' * sentenceSpacingWidth 

5873 else: 

5874 space = ' ' 

5875 if line and wordLen > 0: 

5876 wordLen += len(space) 

5877 if wordLen + len(line) <= outputLineWidth: 

5878 if wordLen > 0: 

5879 #@+<< place blank and word on the present line >> 

5880 #@+node:ekr.20110727091744.15084: *5* << place blank and word on the present line >> 

5881 if line: 

5882 # Add the word, preceeded by a blank. 

5883 line = space.join((line, word)) 

5884 else: 

5885 # Just add the word to the start of the line. 

5886 line = word 

5887 #@-<< place blank and word on the present line >> 

5888 else: pass # discard the trailing whitespace. 

5889 else: 

5890 #@+<< place word on a new line >> 

5891 #@+node:ekr.20110727091744.15085: *5* << place word on a new line >> 

5892 # End the previous line. 

5893 if line: 

5894 result.append(line) 

5895 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines 

5896 # Discard the whitespace and put the word on a new line. 

5897 line = word 

5898 # Careful: the word may be longer than pageWidth. 

5899 if len(line) > pageWidth: # DTHEIN 18-JAN-2004: line can equal pagewidth 

5900 result.append(line) 

5901 outputLineWidth = pageWidth # DTHEIN 3-NOV-2002: width for remaining lines 

5902 line = "" 

5903 #@-<< place word on a new line >> 

5904 if line: 

5905 result.append(line) 

5906 return result 

5907#@+node:ekr.20031218072017.3200: *4* g.get_leading_ws 

5908def get_leading_ws(s: str) -> str: 

5909 """Returns the leading whitespace of 's'.""" 

5910 i = 0 

5911 n = len(s) 

5912 while i < n and s[i] in (' ', '\t'): 

5913 i += 1 

5914 return s[0:i] 

5915#@+node:ekr.20031218072017.3201: *4* g.optimizeLeadingWhitespace 

5916# Optimize leading whitespace in s with the given tab_width. 

5917 

5918def optimizeLeadingWhitespace(line: str, tab_width: int) -> str: 

5919 i, width = g.skip_leading_ws_with_indent(line, 0, tab_width) 

5920 s = g.computeLeadingWhitespace(width, tab_width) + line[i:] 

5921 return s 

5922#@+node:ekr.20040723093558: *4* g.regularizeTrailingNewlines 

5923#@+at The caller should call g.stripBlankLines before calling this routine 

5924# if desired. 

5925# 

5926# This routine does _not_ simply call rstrip(): that would delete all 

5927# trailing whitespace-only lines, and in some cases that would change 

5928# the meaning of program or data. 

5929#@@c 

5930 

5931def regularizeTrailingNewlines(s: str, kind: str) -> None: 

5932 """Kind is 'asis', 'zero' or 'one'.""" 

5933 pass 

5934#@+node:ekr.20091229090857.11698: *4* g.removeBlankLines 

5935def removeBlankLines(s: str) -> str: 

5936 lines = g.splitLines(s) 

5937 lines = [z for z in lines if z.strip()] 

5938 return ''.join(lines) 

5939#@+node:ekr.20091229075924.6235: *4* g.removeLeadingBlankLines 

5940def removeLeadingBlankLines(s: str) -> str: 

5941 lines = g.splitLines(s) 

5942 result = [] 

5943 remove = True 

5944 for line in lines: 

5945 if remove and not line.strip(): 

5946 pass 

5947 else: 

5948 remove = False 

5949 result.append(line) 

5950 return ''.join(result) 

5951#@+node:ekr.20031218072017.3202: *4* g.removeLeadingWhitespace 

5952# Remove whitespace up to first_ws wide in s, given tab_width, the width of a tab. 

5953 

5954def removeLeadingWhitespace(s: str, first_ws: int, tab_width: int) -> str: 

5955 j = 0 

5956 ws = 0 

5957 first_ws = abs(first_ws) 

5958 for ch in s: 

5959 if ws >= first_ws: 

5960 break 

5961 elif ch == ' ': 

5962 j += 1 

5963 ws += 1 

5964 elif ch == '\t': 

5965 j += 1 

5966 ws += (abs(tab_width) - (ws % abs(tab_width))) 

5967 else: 

5968 break 

5969 if j > 0: 

5970 s = s[j:] 

5971 return s 

5972#@+node:ekr.20031218072017.3203: *4* g.removeTrailingWs 

5973# Warning: string.rstrip also removes newlines! 

5974 

5975def removeTrailingWs(s: str) -> str: 

5976 j = len(s) - 1 

5977 while j >= 0 and (s[j] == ' ' or s[j] == '\t'): 

5978 j -= 1 

5979 return s[: j + 1] 

5980#@+node:ekr.20031218072017.3204: *4* g.skip_leading_ws 

5981# Skips leading up to width leading whitespace. 

5982 

5983def skip_leading_ws(s: str, i: int, ws: int, tab_width: int) -> int: 

5984 count = 0 

5985 while count < ws and i < len(s): 

5986 ch = s[i] 

5987 if ch == ' ': 

5988 count += 1 

5989 i += 1 

5990 elif ch == '\t': 

5991 count += (abs(tab_width) - (count % abs(tab_width))) 

5992 i += 1 

5993 else: break 

5994 return i 

5995#@+node:ekr.20031218072017.3205: *4* g.skip_leading_ws_with_indent 

5996def skip_leading_ws_with_indent(s: str, i: int, tab_width: int) -> Tuple[int, int]: 

5997 """Skips leading whitespace and returns (i, indent), 

5998 

5999 - i points after the whitespace 

6000 - indent is the width of the whitespace, assuming tab_width wide tabs.""" 

6001 count = 0 

6002 n = len(s) 

6003 while i < n: 

6004 ch = s[i] 

6005 if ch == ' ': 

6006 count += 1 

6007 i += 1 

6008 elif ch == '\t': 

6009 count += (abs(tab_width) - (count % abs(tab_width))) 

6010 i += 1 

6011 else: break 

6012 return i, count 

6013#@+node:ekr.20040723093558.1: *4* g.stripBlankLines 

6014def stripBlankLines(s: str) -> str: 

6015 lines = g.splitLines(s) 

6016 for i, line in enumerate(lines): 

6017 j = g.skip_ws(line, 0) 

6018 if j >= len(line): 

6019 lines[i] = '' 

6020 elif line[j] == '\n': 

6021 lines[i] = '\n' 

6022 return ''.join(lines) 

6023#@+node:ekr.20031218072017.3108: ** g.Logging & Printing 

6024# g.es and related print to the Log window. 

6025# g.pr prints to the console. 

6026# g.es_print and related print to both the Log window and the console. 

6027#@+node:ekr.20080821073134.2: *3* g.doKeywordArgs 

6028def doKeywordArgs(keys: Dict, d: Dict=None) -> Dict: 

6029 """ 

6030 Return a result dict that is a copy of the keys dict 

6031 with missing items replaced by defaults in d dict. 

6032 """ 

6033 if d is None: 

6034 d = {} 

6035 result = {} 

6036 for key, default_val in d.items(): 

6037 isBool = default_val in (True, False) 

6038 val = keys.get(key) 

6039 if isBool and val in (True, 'True', 'true'): 

6040 result[key] = True 

6041 elif isBool and val in (False, 'False', 'false'): 

6042 result[key] = False 

6043 elif val is None: 

6044 result[key] = default_val 

6045 else: 

6046 result[key] = val 

6047 return result 

6048#@+node:ekr.20031218072017.1474: *3* g.enl, ecnl & ecnls 

6049def ecnl(tabName: str='Log') -> None: 

6050 g.ecnls(1, tabName) 

6051 

6052def ecnls(n: int, tabName: str='Log') -> None: 

6053 log = app.log 

6054 if log and not log.isNull: 

6055 while log.newlines < n: 

6056 g.enl(tabName) 

6057 

6058def enl(tabName: str='Log') -> None: 

6059 log = app.log 

6060 if log and not log.isNull: 

6061 log.newlines += 1 

6062 log.putnl(tabName) 

6063#@+node:ekr.20100914094836.5892: *3* g.error, g.note, g.warning, g.red, g.blue 

6064def blue(*args: Any, **keys: Any) -> None: 

6065 g.es_print(color='blue', *args, **keys) 

6066 

6067def error(*args: Any, **keys: Any) -> None: 

6068 g.es_print(color='error', *args, **keys) 

6069 

6070def note(*args: Any, **keys: Any) -> None: 

6071 g.es_print(color='note', *args, **keys) 

6072 

6073def red(*args: Any, **keys: Any) -> None: 

6074 g.es_print(color='red', *args, **keys) 

6075 

6076def warning(*args: Any, **keys: Any) -> None: 

6077 g.es_print(color='warning', *args, **keys) 

6078#@+node:ekr.20070626132332: *3* g.es 

6079def es(*args: Any, **keys: Any) -> None: 

6080 """Put all non-keyword args to the log pane. 

6081 The first, third, fifth, etc. arg translated by g.translateString. 

6082 Supports color, comma, newline, spaces and tabName keyword arguments. 

6083 """ 

6084 if not app or app.killed: 

6085 return 

6086 if app.gui and app.gui.consoleOnly: 

6087 return 

6088 log = app.log 

6089 # Compute the effective args. 

6090 d = { 

6091 'color': None, 

6092 'commas': False, 

6093 'newline': True, 

6094 'spaces': True, 

6095 'tabName': 'Log', 

6096 'nodeLink': None, 

6097 } 

6098 d = g.doKeywordArgs(keys, d) 

6099 color = d.get('color') 

6100 if color == 'suppress': 

6101 return # New in 4.3. 

6102 color = g.actualColor(color) 

6103 tabName = d.get('tabName') or 'Log' 

6104 newline = d.get('newline') 

6105 s = g.translateArgs(args, d) 

6106 # Do not call g.es, g.es_print, g.pr or g.trace here! 

6107 # sys.__stdout__.write('\n===== g.es: %r\n' % s) 

6108 if app.batchMode: 

6109 if app.log: 

6110 app.log.put(s) 

6111 elif g.unitTesting: 

6112 if log and not log.isNull: 

6113 # This makes the output of unit tests match the output of scripts. 

6114 g.pr(s, newline=newline) 

6115 elif log and app.logInited: 

6116 if newline: 

6117 s += '\n' 

6118 log.put(s, color=color, tabName=tabName, nodeLink=d['nodeLink']) 

6119 # Count the number of *trailing* newlines. 

6120 for ch in s: 

6121 if ch == '\n': 

6122 log.newlines += 1 

6123 else: 

6124 log.newlines = 0 

6125 else: 

6126 app.logWaiting.append((s, color, newline, d),) 

6127 

6128log = es 

6129#@+node:ekr.20060917120951: *3* g.es_dump 

6130def es_dump(s: str, n: int=30, title: str=None) -> None: 

6131 if title: 

6132 g.es_print('', title) 

6133 i = 0 

6134 while i < len(s): 

6135 aList = ''.join([f"{ord(ch):2x} " for ch in s[i : i + n]]) 

6136 g.es_print('', aList) 

6137 i += n 

6138#@+node:ekr.20031218072017.3110: *3* g.es_error & es_print_error 

6139def es_error(*args: Any, **keys: Any) -> None: 

6140 color = keys.get('color') 

6141 if color is None and g.app.config: 

6142 keys['color'] = g.app.config.getColor("log-error-color") or 'red' 

6143 g.es(*args, **keys) 

6144 

6145def es_print_error(*args: Any, **keys: Any) -> None: 

6146 color = keys.get('color') 

6147 if color is None and g.app.config: 

6148 keys['color'] = g.app.config.getColor("log-error-color") or 'red' 

6149 g.es_print(*args, **keys) 

6150#@+node:ekr.20031218072017.3111: *3* g.es_event_exception 

6151def es_event_exception(eventName: str, full: bool=False) -> None: 

6152 g.es("exception handling ", eventName, "event") 

6153 typ, val, tb = sys.exc_info() 

6154 if full: 

6155 errList = traceback.format_exception(typ, val, tb) 

6156 else: 

6157 errList = traceback.format_exception_only(typ, val) 

6158 for i in errList: 

6159 g.es('', i) 

6160 if not g.stdErrIsRedirected(): # 2/16/04 

6161 traceback.print_exc() 

6162#@+node:ekr.20031218072017.3112: *3* g.es_exception 

6163def es_exception(full: bool=True, c: Cmdr=None, color: str="red") -> Tuple[str, int]: 

6164 typ, val, tb = sys.exc_info() 

6165 # val is the second argument to the raise statement. 

6166 if full: 

6167 lines = traceback.format_exception(typ, val, tb) 

6168 else: 

6169 lines = traceback.format_exception_only(typ, val) 

6170 for line in lines: 

6171 g.es_print_error(line, color=color) 

6172 fileName, n = g.getLastTracebackFileAndLineNumber() 

6173 return fileName, n 

6174#@+node:ekr.20061015090538: *3* g.es_exception_type 

6175def es_exception_type(c: Cmdr=None, color: str="red") -> None: 

6176 # exctype is a Exception class object; value is the error message. 

6177 exctype, value = sys.exc_info()[:2] 

6178 g.es_print('', f"{exctype.__name__}, {value}", color=color) # type:ignore 

6179#@+node:ekr.20050707064040: *3* g.es_print 

6180# see: http://www.diveintopython.org/xml_processing/unicode.html 

6181 

6182def es_print(*args: Any, **keys: Any) -> None: 

6183 """ 

6184 Print all non-keyword args, and put them to the log pane. 

6185 

6186 The first, third, fifth, etc. arg translated by g.translateString. 

6187 Supports color, comma, newline, spaces and tabName keyword arguments. 

6188 """ 

6189 g.pr(*args, **keys) 

6190 if g.app and not g.unitTesting: 

6191 g.es(*args, **keys) 

6192#@+node:ekr.20111107181638.9741: *3* g.print_exception 

6193def print_exception(full: bool=True, c: Cmdr=None, flush: bool=False, color: str="red") -> Tuple[str, int]: 

6194 """Print exception info about the last exception.""" 

6195 # val is the second argument to the raise statement. 

6196 typ, val, tb = sys.exc_info() 

6197 if full: 

6198 lines = traceback.format_exception(typ, val, tb) 

6199 else: 

6200 lines = traceback.format_exception_only(typ, val) 

6201 print(''.join(lines), flush=flush) 

6202 try: 

6203 fileName, n = g.getLastTracebackFileAndLineNumber() 

6204 return fileName, n 

6205 except Exception: 

6206 return "<no file>", 0 

6207#@+node:ekr.20050707065530: *3* g.es_trace 

6208def es_trace(*args: Any, **keys: Any) -> None: 

6209 if args: 

6210 try: 

6211 s = args[0] 

6212 g.trace(g.toEncodedString(s, 'ascii')) 

6213 except Exception: 

6214 pass 

6215 g.es(*args, **keys) 

6216#@+node:ekr.20040731204831: *3* g.getLastTracebackFileAndLineNumber 

6217def getLastTracebackFileAndLineNumber() -> Tuple[str, int]: 

6218 typ, val, tb = sys.exc_info() 

6219 if typ == SyntaxError: 

6220 # IndentationError is a subclass of SyntaxError. 

6221 return val.filename, val.lineno 

6222 # 

6223 # Data is a list of tuples, one per stack entry. 

6224 # Tupls have the form (filename,lineNumber,functionName,text). 

6225 data = traceback.extract_tb(tb) 

6226 if data: 

6227 item = data[-1] # Get the item at the top of the stack. 

6228 filename, n, functionName, text = item 

6229 return filename, n 

6230 # Should never happen. 

6231 return '<string>', 0 

6232#@+node:ekr.20150621095017.1: *3* g.goto_last_exception 

6233def goto_last_exception(c: Cmdr) -> None: 

6234 """Go to the line given by sys.last_traceback.""" 

6235 typ, val, tb = sys.exc_info() 

6236 if tb: 

6237 file_name, line_number = g.getLastTracebackFileAndLineNumber() 

6238 line_number = max(0, line_number - 1) # Convert to zero-based. 

6239 if file_name.endswith('scriptFile.py'): 

6240 # A script. 

6241 c.goToScriptLineNumber(line_number, c.p) 

6242 else: 

6243 for p in c.all_nodes(): 

6244 if p.isAnyAtFileNode() and p.h.endswith(file_name): 

6245 c.goToLineNumber(line_number) 

6246 return 

6247 else: 

6248 g.trace('No previous exception') 

6249#@+node:ekr.20100126062623.6240: *3* g.internalError 

6250def internalError(*args: Any) -> None: 

6251 """Report a serious interal error in Leo.""" 

6252 callers = g.callers(20).split(',') 

6253 caller = callers[-1] 

6254 g.error('\nInternal Leo error in', caller) 

6255 g.es_print(*args) 

6256 g.es_print('Called from', ', '.join(callers[:-1])) 

6257 g.es_print('Please report this error to Leo\'s developers', color='red') 

6258#@+node:ekr.20150127060254.5: *3* g.log_to_file 

6259def log_to_file(s: str, fn: str=None) -> None: 

6260 """Write a message to ~/test/leo_log.txt.""" 

6261 if fn is None: 

6262 fn = g.os_path_expanduser('~/test/leo_log.txt') 

6263 if not s.endswith('\n'): 

6264 s = s + '\n' 

6265 try: 

6266 with open(fn, 'a') as f: 

6267 f.write(s) 

6268 except Exception: 

6269 g.es_exception() 

6270#@+node:ekr.20080710101653.1: *3* g.pr 

6271# see: http://www.diveintopython.org/xml_processing/unicode.html 

6272 

6273def pr(*args: Any, **keys: Any) -> None: 

6274 """ 

6275 Print all non-keyword args. This is a wrapper for the print statement. 

6276 

6277 The first, third, fifth, etc. arg translated by g.translateString. 

6278 Supports color, comma, newline, spaces and tabName keyword arguments. 

6279 """ 

6280 # Compute the effective args. 

6281 d = {'commas': False, 'newline': True, 'spaces': True} 

6282 d = doKeywordArgs(keys, d) 

6283 newline = d.get('newline') 

6284 # Unit tests require sys.stdout. 

6285 stdout = sys.stdout if sys.stdout and g.unitTesting else sys.__stdout__ 

6286 if not stdout: 

6287 # #541. 

6288 return 

6289 if sys.platform.lower().startswith('win'): 

6290 encoding = 'ascii' # 2011/11/9. 

6291 elif getattr(stdout, 'encoding', None): 

6292 # sys.stdout is a TextIOWrapper with a particular encoding. 

6293 encoding = stdout.encoding 

6294 else: 

6295 encoding = 'utf-8' 

6296 s = translateArgs(args, d) # Translates everything to unicode. 

6297 s = g.toUnicode(s, encoding=encoding, reportErrors=False) 

6298 if newline: 

6299 s += '\n' 

6300 # Python's print statement *can* handle unicode, but 

6301 # sitecustomize.py must have sys.setdefaultencoding('utf-8') 

6302 try: 

6303 # #783: print-* commands fail under pythonw. 

6304 stdout.write(s) 

6305 except Exception: 

6306 pass 

6307#@+node:ekr.20060221083356: *3* g.prettyPrintType 

6308def prettyPrintType(obj: Any) -> str: 

6309 if isinstance(obj, str): # type:ignore 

6310 return 'string' 

6311 t: Any = type(obj) 

6312 if t in (types.BuiltinFunctionType, types.FunctionType): 

6313 return 'function' 

6314 if t == types.ModuleType: 

6315 return 'module' 

6316 if t in [types.MethodType, types.BuiltinMethodType]: 

6317 return 'method' 

6318 # Fall back to a hack. 

6319 t = str(type(obj)) # type:ignore 

6320 if t.startswith("<type '"): 

6321 t = t[7:] 

6322 if t.endswith("'>"): 

6323 t = t[:-2] 

6324 return t 

6325#@+node:ekr.20031218072017.3113: *3* g.printBindings 

6326def print_bindings(name: str, window: Any) -> None: 

6327 bindings = window.bind() 

6328 g.pr("\nBindings for", name) 

6329 for b in bindings: 

6330 g.pr(b) 

6331#@+node:ekr.20070510074941: *3* g.printEntireTree 

6332def printEntireTree(c: Cmdr, tag: str='') -> None: 

6333 g.pr('printEntireTree', '=' * 50) 

6334 g.pr('printEntireTree', tag, 'root', c.rootPosition()) 

6335 for p in c.all_positions(): 

6336 g.pr('..' * p.level(), p.v) 

6337#@+node:ekr.20031218072017.3114: *3* g.printGlobals 

6338def printGlobals(message: str=None) -> None: 

6339 # Get the list of globals. 

6340 globs = list(globals()) 

6341 globs.sort() 

6342 # Print the list. 

6343 if message: 

6344 leader = "-" * 10 

6345 g.pr(leader, ' ', message, ' ', leader) 

6346 for name in globs: 

6347 g.pr(name) 

6348#@+node:ekr.20031218072017.3115: *3* g.printLeoModules 

6349def printLeoModules(message: str=None) -> None: 

6350 # Create the list. 

6351 mods = [] 

6352 for name in sys.modules: 

6353 if name and name[0:3] == "leo": 

6354 mods.append(name) 

6355 # Print the list. 

6356 if message: 

6357 leader = "-" * 10 

6358 g.pr(leader, ' ', message, ' ', leader) 

6359 mods.sort() 

6360 for m in mods: 

6361 g.pr(m, newline=False) 

6362 g.pr('') 

6363#@+node:ekr.20041122153823: *3* g.printStack 

6364def printStack() -> None: 

6365 traceback.print_stack() 

6366#@+node:ekr.20031218072017.2317: *3* g.trace 

6367def trace(*args: Any, **keys: Any) -> None: 

6368 """Print a tracing message.""" 

6369 # Don't use g here: in standalone mode g is a NullObject! 

6370 # Compute the effective args. 

6371 d: Dict[str, Any] = {'align': 0, 'before': '', 'newline': True, 'caller_level': 1, 'noname': False} 

6372 d = doKeywordArgs(keys, d) 

6373 newline = d.get('newline') 

6374 align = d.get('align', 0) 

6375 caller_level = d.get('caller_level', 1) 

6376 noname = d.get('noname') 

6377 # Compute the caller name. 

6378 if noname: 

6379 name = '' 

6380 else: 

6381 try: # get the function name from the call stack. 

6382 f1 = sys._getframe(caller_level) # The stack frame, one level up. 

6383 code1 = f1.f_code # The code object 

6384 name = code1.co_name # The code name 

6385 except Exception: 

6386 name = g.shortFileName(__file__) 

6387 if name == '<module>': 

6388 name = g.shortFileName(__file__) 

6389 if name.endswith('.pyc'): 

6390 name = name[:-1] 

6391 # Pad the caller name. 

6392 if align != 0 and len(name) < abs(align): 

6393 pad = ' ' * (abs(align) - len(name)) 

6394 if align > 0: 

6395 name = name + pad 

6396 else: 

6397 name = pad + name 

6398 # Munge *args into s. 

6399 result = [name] if name else [] 

6400 # 

6401 # Put leading newlines into the prefix. 

6402 if isinstance(args, tuple): 

6403 args = list(args) # type:ignore 

6404 if args and isinstance(args[0], str): 

6405 prefix = '' 

6406 while args[0].startswith('\n'): 

6407 prefix += '\n' 

6408 args[0] = args[0][1:] # type:ignore 

6409 else: 

6410 prefix = '' 

6411 for arg in args: 

6412 if isinstance(arg, str): 

6413 pass 

6414 elif isinstance(arg, bytes): 

6415 arg = toUnicode(arg) 

6416 else: 

6417 arg = repr(arg) 

6418 if result: 

6419 result.append(" " + arg) 

6420 else: 

6421 result.append(arg) 

6422 s = d.get('before') + ''.join(result) 

6423 if prefix: 

6424 prefix = prefix[1:] # One less newline. 

6425 pr(prefix) 

6426 pr(s, newline=newline) 

6427#@+node:ekr.20080220111323: *3* g.translateArgs 

6428console_encoding = None 

6429 

6430def translateArgs(args: Iterable[Any], d: Dict[str, Any]) -> str: 

6431 """ 

6432 Return the concatenation of s and all args, with odd args translated. 

6433 """ 

6434 global console_encoding 

6435 if not console_encoding: 

6436 e = sys.getdefaultencoding() 

6437 console_encoding = e if isValidEncoding(e) else 'utf-8' 

6438 # print 'translateArgs',console_encoding 

6439 result: List[str] = [] 

6440 n, spaces = 0, d.get('spaces') 

6441 for arg in args: 

6442 n += 1 

6443 # First, convert to unicode. 

6444 if isinstance(arg, str): 

6445 arg = toUnicode(arg, console_encoding) 

6446 # Now translate. 

6447 if not isinstance(arg, str): 

6448 arg = repr(arg) 

6449 elif (n % 2) == 1: 

6450 arg = translateString(arg) 

6451 else: 

6452 pass # The arg is an untranslated string. 

6453 if arg: 

6454 if result and spaces: 

6455 result.append(' ') 

6456 result.append(arg) 

6457 return ''.join(result) 

6458#@+node:ekr.20060810095921: *3* g.translateString & tr 

6459def translateString(s: str) -> str: 

6460 """Return the translated text of s.""" 

6461 # pylint: disable=undefined-loop-variable 

6462 # looks like a pylint bug 

6463 upper = app and getattr(app, 'translateToUpperCase', None) 

6464 if not isinstance(s, str): 

6465 s = str(s, 'utf-8') 

6466 if upper: 

6467 s = s.upper() 

6468 else: 

6469 s = gettext.gettext(s) 

6470 return s 

6471 

6472tr = translateString 

6473#@+node:EKR.20040612114220: ** g.Miscellaneous 

6474#@+node:ekr.20120928142052.10116: *3* g.actualColor 

6475def actualColor(color: str) -> str: 

6476 """Return the actual color corresponding to the requested color.""" 

6477 c = g.app.log and g.app.log.c 

6478 # Careful: c.config may not yet exist. 

6479 if not c or not c.config: 

6480 return color 

6481 # Don't change absolute colors. 

6482 if color and color.startswith('#'): 

6483 return color 

6484 # #788: Translate colors to theme-defined colors. 

6485 if color is None: 

6486 # Prefer text_foreground_color' 

6487 color2 = c.config.getColor('log-text-foreground-color') 

6488 if color2: 

6489 return color2 

6490 # Fall back to log_black_color. 

6491 color2 = c.config.getColor('log-black-color') 

6492 return color2 or 'black' 

6493 if color == 'black': 

6494 # Prefer log_black_color. 

6495 color2 = c.config.getColor('log-black-color') 

6496 if color2: 

6497 return color2 

6498 # Fall back to log_text_foreground_color. 

6499 color2 = c.config.getColor('log-text-foreground-color') 

6500 return color2 or 'black' 

6501 color2 = c.config.getColor(f"log_{color}_color") 

6502 return color2 or color 

6503#@+node:ekr.20060921100435: *3* g.CheckVersion & helpers 

6504# Simplified version by EKR: stringCompare not used. 

6505 

6506def CheckVersion( 

6507 s1: str, 

6508 s2: str, 

6509 condition: str=">=", 

6510 stringCompare: bool=None, 

6511 delimiter: str='.', 

6512 trace: bool=False, 

6513) -> bool: 

6514 # CheckVersion is called early in the startup process. 

6515 vals1 = [g.CheckVersionToInt(s) for s in s1.split(delimiter)] 

6516 n1 = len(vals1) 

6517 vals2 = [g.CheckVersionToInt(s) for s in s2.split(delimiter)] 

6518 n2 = len(vals2) 

6519 n = max(n1, n2) 

6520 if n1 < n: 

6521 vals1.extend([0 for i in range(n - n1)]) 

6522 if n2 < n: 

6523 vals2.extend([0 for i in range(n - n2)]) 

6524 for cond, val in ( 

6525 ('==', vals1 == vals2), ('!=', vals1 != vals2), 

6526 ('<', vals1 < vals2), ('<=', vals1 <= vals2), 

6527 ('>', vals1 > vals2), ('>=', vals1 >= vals2), 

6528 ): 

6529 if condition == cond: 

6530 result = val 

6531 break 

6532 else: 

6533 raise EnvironmentError( 

6534 "condition must be one of '>=', '>', '==', '!=', '<', or '<='.") 

6535 return result 

6536#@+node:ekr.20070120123930: *4* g.CheckVersionToInt 

6537def CheckVersionToInt(s: str) -> int: 

6538 try: 

6539 return int(s) 

6540 except ValueError: 

6541 aList = [] 

6542 for ch in s: 

6543 if ch.isdigit(): 

6544 aList.append(ch) 

6545 else: 

6546 break 

6547 if aList: 

6548 s = ''.join(aList) 

6549 return int(s) 

6550 return 0 

6551#@+node:ekr.20111103205308.9657: *3* g.cls 

6552@command('cls') 

6553def cls(event: Any=None) -> None: 

6554 """Clear the screen.""" 

6555 if sys.platform.lower().startswith('win'): 

6556 os.system('cls') 

6557#@+node:ekr.20131114124839.16665: *3* g.createScratchCommander 

6558def createScratchCommander(fileName: str=None) -> None: 

6559 c = g.app.newCommander(fileName) 

6560 frame = c.frame 

6561 frame.createFirstTreeNode() 

6562 assert c.rootPosition() 

6563 frame.setInitialWindowGeometry() 

6564 frame.resizePanesToRatio(frame.ratio, frame.secondary_ratio) 

6565#@+node:ekr.20031218072017.3126: *3* g.funcToMethod (Python Cookbook) 

6566def funcToMethod(f: Any, theClass: Any, name: str=None) -> None: 

6567 """ 

6568 From the Python Cookbook... 

6569 

6570 The following method allows you to add a function as a method of 

6571 any class. That is, it converts the function to a method of the 

6572 class. The method just added is available instantly to all 

6573 existing instances of the class, and to all instances created in 

6574 the future. 

6575 

6576 The function's first argument should be self. 

6577 

6578 The newly created method has the same name as the function unless 

6579 the optional name argument is supplied, in which case that name is 

6580 used as the method name. 

6581 """ 

6582 setattr(theClass, name or f.__name__, f) 

6583#@+node:ekr.20060913090832.1: *3* g.init_zodb 

6584init_zodb_import_failed = False 

6585init_zodb_failed: Dict[str, bool] = {} # Keys are paths, values are True. 

6586init_zodb_db: Dict[str, Any] = {} # Keys are paths, values are ZODB.DB instances. 

6587 

6588def init_zodb(pathToZodbStorage: str, verbose: bool=True) -> Any: 

6589 """ 

6590 Return an ZODB.DB instance from the given path. 

6591 return None on any error. 

6592 """ 

6593 global init_zodb_db, init_zodb_failed, init_zodb_import_failed 

6594 db = init_zodb_db.get(pathToZodbStorage) 

6595 if db: 

6596 return db 

6597 if init_zodb_import_failed: 

6598 return None 

6599 failed = init_zodb_failed.get(pathToZodbStorage) 

6600 if failed: 

6601 return None 

6602 try: 

6603 import ZODB # type:ignore 

6604 except ImportError: 

6605 if verbose: 

6606 g.es('g.init_zodb: can not import ZODB') 

6607 g.es_exception() 

6608 init_zodb_import_failed = True 

6609 return None 

6610 try: 

6611 storage = ZODB.FileStorage.FileStorage(pathToZodbStorage) 

6612 init_zodb_db[pathToZodbStorage] = db = ZODB.DB(storage) 

6613 return db 

6614 except Exception: 

6615 if verbose: 

6616 g.es('g.init_zodb: exception creating ZODB.DB instance') 

6617 g.es_exception() 

6618 init_zodb_failed[pathToZodbStorage] = True 

6619 return None 

6620#@+node:ekr.20170206080908.1: *3* g.input_ 

6621def input_(message: str='', c: Cmdr=None) -> str: 

6622 """ 

6623 Safely execute python's input statement. 

6624 

6625 c.executeScriptHelper binds 'input' to be a wrapper that calls g.input_ 

6626 with c and handler bound properly. 

6627 """ 

6628 if app.gui.isNullGui: 

6629 return '' 

6630 # Prompt for input from the console, assuming there is one. 

6631 # pylint: disable=no-member 

6632 from leo.core.leoQt import QtCore 

6633 QtCore.pyqtRemoveInputHook() 

6634 return input(message) 

6635#@+node:ekr.20110609125359.16493: *3* g.isMacOS 

6636def isMacOS() -> bool: 

6637 return sys.platform == 'darwin' 

6638#@+node:ekr.20181027133311.1: *3* g.issueSecurityWarning 

6639def issueSecurityWarning(setting: str) -> None: 

6640 g.es('Security warning! Ignoring...', color='red') 

6641 g.es(setting, color='red') 

6642 g.es('This setting can be set only in') 

6643 g.es('leoSettings.leo or myLeoSettings.leo') 

6644#@+node:ekr.20031218072017.3144: *3* g.makeDict (Python Cookbook) 

6645# From the Python cookbook. 

6646 

6647def makeDict(**keys: Any) -> Dict: 

6648 """Returns a Python dictionary from using the optional keyword arguments.""" 

6649 return keys 

6650#@+node:ekr.20140528065727.17963: *3* g.pep8_class_name 

6651def pep8_class_name(s: str) -> str: 

6652 """Return the proper class name for s.""" 

6653 # Warning: s.capitalize() does not work. 

6654 # It lower cases all but the first letter! 

6655 return ''.join([z[0].upper() + z[1:] for z in s.split('_') if z]) 

6656 

6657if 0: # Testing: 

6658 cls() 

6659 aList = ( 

6660 '_', 

6661 '__', 

6662 '_abc', 

6663 'abc_', 

6664 'abc', 

6665 'abc_xyz', 

6666 'AbcPdQ', 

6667 ) 

6668 for s in aList: 

6669 print(pep8_class_name(s)) 

6670#@+node:ekr.20160417174224.1: *3* g.plural 

6671def plural(obj: Any) -> str: 

6672 """Return "s" or "" depending on n.""" 

6673 if isinstance(obj, (list, tuple, str)): 

6674 n = len(obj) 

6675 else: 

6676 n = obj 

6677 return '' if n == 1 else 's' 

6678#@+node:ekr.20160331194701.1: *3* g.truncate 

6679def truncate(s: str, n: int) -> str: 

6680 """Return s truncated to n characters.""" 

6681 if len(s) <= n: 

6682 return s 

6683 # Fail: weird ws. 

6684 s2 = s[: n - 3] + f"...({len(s)})" 

6685 if s.endswith('\n'): 

6686 return s2 + '\n' 

6687 return s2 

6688#@+node:ekr.20031218072017.3150: *3* g.windows 

6689def windows() -> Optional[List]: 

6690 return app and app.windowList 

6691#@+node:ekr.20031218072017.2145: ** g.os_path_ Wrappers 

6692#@+at Note: all these methods return Unicode strings. It is up to the user to 

6693# convert to an encoded string as needed, say when opening a file. 

6694#@+node:ekr.20180314120442.1: *3* g.glob_glob 

6695def glob_glob(pattern: str) -> List: 

6696 """Return the regularized glob.glob(pattern)""" 

6697 aList = glob.glob(pattern) 

6698 # os.path.normpath does the *reverse* of what we want. 

6699 if g.isWindows: 

6700 aList = [z.replace('\\', '/') for z in aList] 

6701 return aList 

6702#@+node:ekr.20031218072017.2146: *3* g.os_path_abspath 

6703def os_path_abspath(path: str) -> str: 

6704 """Convert a path to an absolute path.""" 

6705 if not path: 

6706 return '' 

6707 if '\x00' in path: 

6708 g.trace('NULL in', repr(path), g.callers()) 

6709 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6710 path = os.path.abspath(path) 

6711 # os.path.normpath does the *reverse* of what we want. 

6712 if g.isWindows: 

6713 path = path.replace('\\', '/') 

6714 return path 

6715#@+node:ekr.20031218072017.2147: *3* g.os_path_basename 

6716def os_path_basename(path: str) -> str: 

6717 """Return the second half of the pair returned by split(path).""" 

6718 if not path: 

6719 return '' 

6720 path = os.path.basename(path) 

6721 # os.path.normpath does the *reverse* of what we want. 

6722 if g.isWindows: 

6723 path = path.replace('\\', '/') 

6724 return path 

6725#@+node:ekr.20031218072017.2148: *3* g.os_path_dirname 

6726def os_path_dirname(path: str) -> str: 

6727 """Return the first half of the pair returned by split(path).""" 

6728 if not path: 

6729 return '' 

6730 path = os.path.dirname(path) 

6731 # os.path.normpath does the *reverse* of what we want. 

6732 if g.isWindows: 

6733 path = path.replace('\\', '/') 

6734 return path 

6735#@+node:ekr.20031218072017.2149: *3* g.os_path_exists 

6736def os_path_exists(path: str) -> bool: 

6737 """Return True if path exists.""" 

6738 if not path: 

6739 return False 

6740 if '\x00' in path: 

6741 g.trace('NULL in', repr(path), g.callers()) 

6742 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6743 return os.path.exists(path) 

6744#@+node:ekr.20080921060401.13: *3* g.os_path_expanduser 

6745def os_path_expanduser(path: str) -> str: 

6746 """wrap os.path.expanduser""" 

6747 if not path: 

6748 return '' 

6749 result = os.path.normpath(os.path.expanduser(path)) 

6750 # os.path.normpath does the *reverse* of what we want. 

6751 if g.isWindows: 

6752 path = path.replace('\\', '/') 

6753 return result 

6754#@+node:ekr.20080921060401.14: *3* g.os_path_finalize 

6755def os_path_finalize(path: str) -> str: 

6756 """ 

6757 Expand '~', then return os.path.normpath, os.path.abspath of the path. 

6758 There is no corresponding os.path method 

6759 """ 

6760 if '\x00' in path: 

6761 g.trace('NULL in', repr(path), g.callers()) 

6762 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6763 path = os.path.expanduser(path) # #1383. 

6764 path = os.path.abspath(path) 

6765 path = os.path.normpath(path) 

6766 # os.path.normpath does the *reverse* of what we want. 

6767 if g.isWindows: 

6768 path = path.replace('\\', '/') 

6769 # calling os.path.realpath here would cause problems in some situations. 

6770 return path 

6771#@+node:ekr.20140917154740.19483: *3* g.os_path_finalize_join 

6772def os_path_finalize_join(*args: Any, **keys: Any) -> str: 

6773 """ 

6774 Join and finalize. 

6775 

6776 **keys may contain a 'c' kwarg, used by g.os_path_join. 

6777 """ 

6778 path = g.os_path_join(*args, **keys) 

6779 path = g.os_path_finalize(path) 

6780 return path 

6781#@+node:ekr.20031218072017.2150: *3* g.os_path_getmtime 

6782def os_path_getmtime(path: str) -> float: 

6783 """Return the modification time of path.""" 

6784 if not path: 

6785 return 0 

6786 try: 

6787 return os.path.getmtime(path) 

6788 except Exception: 

6789 return 0 

6790#@+node:ekr.20080729142651.2: *3* g.os_path_getsize 

6791def os_path_getsize(path: str) -> int: 

6792 """Return the size of path.""" 

6793 return os.path.getsize(path) if path else 0 

6794#@+node:ekr.20031218072017.2151: *3* g.os_path_isabs 

6795def os_path_isabs(path: str) -> bool: 

6796 """Return True if path is an absolute path.""" 

6797 return os.path.isabs(path) if path else False 

6798#@+node:ekr.20031218072017.2152: *3* g.os_path_isdir 

6799def os_path_isdir(path: str) -> bool: 

6800 """Return True if the path is a directory.""" 

6801 return os.path.isdir(path) if path else False 

6802#@+node:ekr.20031218072017.2153: *3* g.os_path_isfile 

6803def os_path_isfile(path: str) -> bool: 

6804 """Return True if path is a file.""" 

6805 return os.path.isfile(path) if path else False 

6806#@+node:ekr.20031218072017.2154: *3* g.os_path_join 

6807def os_path_join(*args: Any, **keys: Any) -> str: 

6808 """ 

6809 Join paths, like os.path.join, with enhancements: 

6810 

6811 A '!!' arg prepends g.app.loadDir to the list of paths. 

6812 A '.' arg prepends c.openDirectory to the list of paths, 

6813 provided there is a 'c' kwarg. 

6814 """ 

6815 c = keys.get('c') 

6816 uargs = [z for z in args if z] 

6817 if not uargs: 

6818 return '' 

6819 # Note: This is exactly the same convention as used by getBaseDirectory. 

6820 if uargs[0] == '!!': 

6821 uargs[0] = g.app.loadDir 

6822 elif uargs[0] == '.': 

6823 c = keys.get('c') 

6824 if c and c.openDirectory: 

6825 uargs[0] = c.openDirectory 

6826 try: 

6827 path = os.path.join(*uargs) 

6828 except TypeError: 

6829 g.trace(uargs, args, keys, g.callers()) 

6830 raise 

6831 # May not be needed on some Pythons. 

6832 if '\x00' in path: 

6833 g.trace('NULL in', repr(path), g.callers()) 

6834 path = path.replace('\x00', '') # Fix Python 3 bug on Windows 10. 

6835 # os.path.normpath does the *reverse* of what we want. 

6836 if g.isWindows: 

6837 path = path.replace('\\', '/') 

6838 return path 

6839#@+node:ekr.20031218072017.2156: *3* g.os_path_normcase 

6840def os_path_normcase(path: str) -> str: 

6841 """Normalize the path's case.""" 

6842 if not path: 

6843 return '' 

6844 path = os.path.normcase(path) 

6845 if g.isWindows: 

6846 path = path.replace('\\', '/') 

6847 return path 

6848#@+node:ekr.20031218072017.2157: *3* g.os_path_normpath 

6849def os_path_normpath(path: str) -> str: 

6850 """Normalize the path.""" 

6851 if not path: 

6852 return '' 

6853 path = os.path.normpath(path) 

6854 # os.path.normpath does the *reverse* of what we want. 

6855 if g.isWindows: 

6856 path = path.replace('\\', '/').lower() # #2049: ignore case! 

6857 return path 

6858#@+node:ekr.20180314081254.1: *3* g.os_path_normslashes 

6859def os_path_normslashes(path: str) -> str: 

6860 

6861 # os.path.normpath does the *reverse* of what we want. 

6862 if g.isWindows and path: 

6863 path = path.replace('\\', '/') 

6864 return path 

6865#@+node:ekr.20080605064555.2: *3* g.os_path_realpath 

6866def os_path_realpath(path: str) -> str: 

6867 """Return the canonical path of the specified filename, eliminating any 

6868 symbolic links encountered in the path (if they are supported by the 

6869 operating system). 

6870 """ 

6871 if not path: 

6872 return '' 

6873 path = os.path.realpath(path) 

6874 # os.path.normpath does the *reverse* of what we want. 

6875 if g.isWindows: 

6876 path = path.replace('\\', '/') 

6877 return path 

6878#@+node:ekr.20031218072017.2158: *3* g.os_path_split 

6879def os_path_split(path: str) -> Tuple[str, str]: 

6880 if not path: 

6881 return '', '' 

6882 head, tail = os.path.split(path) 

6883 return head, tail 

6884#@+node:ekr.20031218072017.2159: *3* g.os_path_splitext 

6885def os_path_splitext(path: str) -> Tuple[str, str]: 

6886 

6887 if not path: 

6888 return '', '' 

6889 head, tail = os.path.splitext(path) 

6890 return head, tail 

6891#@+node:ekr.20090829140232.6036: *3* g.os_startfile 

6892def os_startfile(fname: str) -> None: 

6893 #@+others 

6894 #@+node:bob.20170516112250.1: *4* stderr2log() 

6895 def stderr2log(g: Any, ree: Any, fname: str) -> None: 

6896 """ Display stderr output in the Leo-Editor log pane 

6897 

6898 Arguments: 

6899 g: Leo-Editor globals 

6900 ree: Read file descriptor for stderr 

6901 fname: file pathname 

6902 

6903 Returns: 

6904 None 

6905 """ 

6906 

6907 while True: 

6908 emsg = ree.read().decode('utf-8') 

6909 if emsg: 

6910 g.es_print_error(f"xdg-open {fname} caused output to stderr:\n{emsg}") 

6911 else: 

6912 break 

6913 #@+node:bob.20170516112304.1: *4* itPoll() 

6914 def itPoll(fname: str, ree: Any, subPopen: Any, g: Any, ito: Any) -> None: 

6915 """ Poll for subprocess done 

6916 

6917 Arguments: 

6918 fname: File name 

6919 ree: stderr read file descriptor 

6920 subPopen: URL open subprocess object 

6921 g: Leo-Editor globals 

6922 ito: Idle time object for itPoll() 

6923 

6924 Returns: 

6925 None 

6926 """ 

6927 

6928 stderr2log(g, ree, fname) 

6929 rc = subPopen.poll() 

6930 if not rc is None: 

6931 ito.stop() 

6932 ito.destroy_self() 

6933 if rc != 0: 

6934 g.es_print(f"xdg-open {fname} failed with exit code {rc}") 

6935 stderr2log(g, ree, fname) 

6936 ree.close() 

6937 #@-others 

6938 # pylint: disable=used-before-assignment 

6939 if fname.find('"') > -1: 

6940 quoted_fname = f"'{fname}'" 

6941 else: 

6942 quoted_fname = f'"{fname}"' 

6943 if sys.platform.startswith('win'): 

6944 # pylint: disable=no-member 

6945 os.startfile(quoted_fname) 

6946 # Exists only on Windows. 

6947 elif sys.platform == 'darwin': 

6948 # From Marc-Antoine Parent. 

6949 try: 

6950 # Fix bug 1226358: File URL's are broken on MacOS: 

6951 # use fname, not quoted_fname, as the argument to subprocess.call. 

6952 subprocess.call(['open', fname]) 

6953 except OSError: 

6954 pass # There may be a spurious "Interrupted system call" 

6955 except ImportError: 

6956 os.system(f"open {quoted_fname}") 

6957 else: 

6958 try: 

6959 ree = None 

6960 wre = tempfile.NamedTemporaryFile() 

6961 ree = io.open(wre.name, 'rb', buffering=0) 

6962 except IOError: 

6963 g.trace(f"error opening temp file for {fname!r}") 

6964 if ree: 

6965 ree.close() 

6966 return 

6967 try: 

6968 subPopen = subprocess.Popen(['xdg-open', fname], stderr=wre, shell=False) 

6969 except Exception: 

6970 g.es_print(f"error opening {fname!r}") 

6971 g.es_exception() 

6972 try: 

6973 itoPoll = g.IdleTime( 

6974 (lambda ito: itPoll(fname, ree, subPopen, g, ito)), 

6975 delay=1000, 

6976 ) 

6977 itoPoll.start() 

6978 # Let the Leo-Editor process run 

6979 # so that Leo-Editor is usable while the file is open. 

6980 except Exception: 

6981 g.es_exception(f"exception executing g.startfile for {fname!r}") 

6982#@+node:ekr.20111115155710.9859: ** g.Parsing & Tokenizing 

6983#@+node:ekr.20031218072017.822: *3* g.createTopologyList 

6984def createTopologyList(c: Cmdr, root: Pos=None, useHeadlines: bool=False) -> List: 

6985 """Creates a list describing a node and all its descendents""" 

6986 if not root: 

6987 root = c.rootPosition() 

6988 v = root 

6989 if useHeadlines: 

6990 aList = [(v.numberOfChildren(), v.headString()),] # type: ignore 

6991 else: 

6992 aList = [v.numberOfChildren()] # type: ignore 

6993 child = v.firstChild() 

6994 while child: 

6995 aList.append(g.createTopologyList(c, child, useHeadlines)) # type: ignore 

6996 child = child.next() 

6997 return aList 

6998#@+node:ekr.20111017204736.15898: *3* g.getDocString 

6999def getDocString(s: str) -> str: 

7000 """Return the text of the first docstring found in s.""" 

7001 tags = ('"""', "'''") 

7002 tag1, tag2 = tags 

7003 i1, i2 = s.find(tag1), s.find(tag2) 

7004 if i1 == -1 and i2 == -1: 

7005 return '' 

7006 if i1 > -1 and i2 > -1: 

7007 i = min(i1, i2) 

7008 else: 

7009 i = max(i1, i2) 

7010 tag = s[i : i + 3] 

7011 assert tag in tags 

7012 j = s.find(tag, i + 3) 

7013 if j > -1: 

7014 return s[i + 3 : j] 

7015 return '' 

7016#@+node:ekr.20111017211256.15905: *3* g.getDocStringForFunction 

7017def getDocStringForFunction(func: Any) -> str: 

7018 """Return the docstring for a function that creates a Leo command.""" 

7019 

7020 def name(func: Any) -> str: 

7021 return func.__name__ if hasattr(func, '__name__') else '<no __name__>' 

7022 

7023 def get_defaults(func: str, i: int) -> Any: 

7024 defaults = inspect.getfullargspec(func)[3] 

7025 return defaults[i] 

7026 

7027 # Fix bug 1251252: https://bugs.launchpad.net/leo-editor/+bug/1251252 

7028 # Minibuffer commands created by mod_scripting.py have no docstrings. 

7029 # Do special cases first. 

7030 

7031 s = '' 

7032 if name(func) == 'minibufferCallback': 

7033 func = get_defaults(func, 0) 

7034 if hasattr(func, 'func.__doc__') and func.__doc__.strip(): 

7035 s = func.__doc__ 

7036 if not s and name(func) == 'commonCommandCallback': 

7037 script = get_defaults(func, 1) 

7038 s = g.getDocString(script) # Do a text scan for the function. 

7039 # Now the general cases. Prefer __doc__ to docstring() 

7040 if not s and hasattr(func, '__doc__'): 

7041 s = func.__doc__ 

7042 if not s and hasattr(func, 'docstring'): 

7043 s = func.docstring 

7044 return s 

7045#@+node:ekr.20111115155710.9814: *3* g.python_tokenize (not used) 

7046def python_tokenize(s: str) -> List: 

7047 """ 

7048 Tokenize string s and return a list of tokens (kind, value, line_number) 

7049 

7050 where kind is in ('comment,'id','nl','other','string','ws'). 

7051 """ 

7052 result: List[Tuple[str, str, int]] = [] 

7053 i, line_number = 0, 0 

7054 while i < len(s): 

7055 progress = j = i 

7056 ch = s[i] 

7057 if ch == '\n': 

7058 kind, i = 'nl', i + 1 

7059 elif ch in ' \t': 

7060 kind = 'ws' 

7061 while i < len(s) and s[i] in ' \t': 

7062 i += 1 

7063 elif ch == '#': 

7064 kind, i = 'comment', g.skip_to_end_of_line(s, i) 

7065 elif ch in '"\'': 

7066 kind, i = 'string', g.skip_python_string(s, i) 

7067 elif ch == '_' or ch.isalpha(): 

7068 kind, i = 'id', g.skip_id(s, i) 

7069 else: 

7070 kind, i = 'other', i + 1 

7071 assert progress < i and j == progress 

7072 val = s[j:i] 

7073 assert val 

7074 line_number += val.count('\n') # A comment. 

7075 result.append((kind, val, line_number),) 

7076 return result 

7077#@+node:ekr.20040327103735.2: ** g.Scripting 

7078#@+node:ekr.20161223090721.1: *3* g.exec_file 

7079def exec_file(path: str, d: Dict[str, str], script: str=None) -> None: 

7080 """Simulate python's execfile statement for python 3.""" 

7081 if script is None: 

7082 with open(path) as f: 

7083 script = f.read() 

7084 exec(compile(script, path, 'exec'), d) 

7085#@+node:ekr.20131016032805.16721: *3* g.execute_shell_commands 

7086def execute_shell_commands(commands: Any, trace: bool=False) -> None: 

7087 """ 

7088 Execute each shell command in a separate process. 

7089 Wait for each command to complete, except those starting with '&' 

7090 """ 

7091 if isinstance(commands, str): 

7092 commands = [commands] 

7093 for command in commands: 

7094 wait = not command.startswith('&') 

7095 if trace: 

7096 g.trace(command) 

7097 if command.startswith('&'): 

7098 command = command[1:].strip() 

7099 proc = subprocess.Popen(command, shell=True) 

7100 if wait: 

7101 proc.communicate() 

7102 else: 

7103 if trace: 

7104 print('Start:', proc) 

7105 # #1489: call proc.poll at idle time. 

7106 

7107 def proc_poller(timer: Any, proc: Any=proc) -> None: 

7108 val = proc.poll() 

7109 if val is not None: 

7110 # This trace can be disruptive. 

7111 if trace: 

7112 print(' End:', proc, val) 

7113 timer.stop() 

7114 

7115 g.IdleTime(proc_poller, delay=0).start() 

7116#@+node:ekr.20180217113719.1: *3* g.execute_shell_commands_with_options & helpers 

7117def execute_shell_commands_with_options( 

7118 base_dir: str=None, 

7119 c: Cmdr=None, 

7120 command_setting: str=None, 

7121 commands: List=None, 

7122 path_setting: str=None, 

7123 trace: bool=False, 

7124 warning: str=None, 

7125) -> None: 

7126 """ 

7127 A helper for prototype commands or any other code that 

7128 runs programs in a separate process. 

7129 

7130 base_dir: Base directory to use if no config path given. 

7131 commands: A list of commands, for g.execute_shell_commands. 

7132 commands_setting: Name of @data setting for commands. 

7133 path_setting: Name of @string setting for the base directory. 

7134 warning: A warning to be printed before executing the commands. 

7135 """ 

7136 base_dir = g.computeBaseDir(c, base_dir, path_setting, trace) 

7137 if not base_dir: 

7138 return 

7139 commands = g.computeCommands(c, commands, command_setting, trace) 

7140 if not commands: 

7141 return 

7142 if warning: 

7143 g.es_print(warning) 

7144 os.chdir(base_dir) # Can't do this in the commands list. 

7145 g.execute_shell_commands(commands) 

7146#@+node:ekr.20180217152624.1: *4* g.computeBaseDir 

7147def computeBaseDir(c: Cmdr, base_dir: str, path_setting: str, trace: bool=False) -> Optional[str]: 

7148 """ 

7149 Compute a base_directory. 

7150 If given, @string path_setting takes precedence. 

7151 """ 

7152 # Prefer the path setting to the base_dir argument. 

7153 if path_setting: 

7154 if not c: 

7155 g.es_print('@string path_setting requires valid c arg') 

7156 return None 

7157 # It's not an error for the setting to be empty. 

7158 base_dir2 = c.config.getString(path_setting) 

7159 if base_dir2: 

7160 base_dir2 = base_dir2.replace('\\', '/') 

7161 if g.os_path_exists(base_dir2): 

7162 return base_dir2 

7163 g.es_print(f"@string {path_setting} not found: {base_dir2!r}") 

7164 return None 

7165 # Fall back to given base_dir. 

7166 if base_dir: 

7167 base_dir = base_dir.replace('\\', '/') 

7168 if g.os_path_exists(base_dir): 

7169 return base_dir 

7170 g.es_print(f"base_dir not found: {base_dir!r}") 

7171 return None 

7172 g.es_print(f"Please use @string {path_setting}") 

7173 return None 

7174#@+node:ekr.20180217153459.1: *4* g.computeCommands 

7175def computeCommands(c: Cmdr, commands: List[str], command_setting: str, trace: bool=False) -> List[str]: 

7176 """ 

7177 Get the list of commands. 

7178 If given, @data command_setting takes precedence. 

7179 """ 

7180 if not commands and not command_setting: 

7181 g.es_print('Please use commands, command_setting or both') 

7182 return [] 

7183 # Prefer the setting to the static commands. 

7184 if command_setting: 

7185 if c: 

7186 aList = c.config.getData(command_setting) 

7187 # It's not an error for the setting to be empty. 

7188 # Fall back to the commands. 

7189 return aList or commands 

7190 g.es_print('@data command_setting requires valid c arg') 

7191 return [] 

7192 return commands 

7193#@+node:ekr.20050503112513.7: *3* g.executeFile 

7194def executeFile(filename: str, options: str='') -> None: 

7195 if not os.access(filename, os.R_OK): 

7196 return 

7197 fdir, fname = g.os_path_split(filename) 

7198 # New in Leo 4.10: alway use subprocess. 

7199 

7200 def subprocess_wrapper(cmdlst: str) -> Tuple: 

7201 

7202 p = subprocess.Popen(cmdlst, cwd=fdir, 

7203 universal_newlines=True, 

7204 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 

7205 stdo, stde = p.communicate() 

7206 return p.wait(), stdo, stde 

7207 

7208 rc, so, se = subprocess_wrapper(f"{sys.executable} {fname} {options}") 

7209 if rc: 

7210 g.pr('return code', rc) 

7211 g.pr(so, se) 

7212#@+node:ekr.20040321065415: *3* g.find*Node* 

7213#@+others 

7214#@+node:ekr.20210303123423.3: *4* findNodeAnywhere 

7215def findNodeAnywhere(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]: 

7216 h = headline.strip() 

7217 for p in c.all_unique_positions(copy=False): 

7218 if p.h.strip() == h: 

7219 return p.copy() 

7220 if not exact: 

7221 for p in c.all_unique_positions(copy=False): 

7222 if p.h.strip().startswith(h): 

7223 return p.copy() 

7224 return None 

7225#@+node:ekr.20210303123525.1: *4* findNodeByPath 

7226def findNodeByPath(c: Cmdr, path: str) -> Optional[Pos]: 

7227 """Return the first @<file> node in Cmdr c whose path is given.""" 

7228 if not os.path.isabs(path): # #2049. Only absolute paths could possibly work. 

7229 g.trace(f"path not absolute: {path}") 

7230 return None 

7231 path = g.os_path_normpath(path) # #2049. Do *not* use os.path.normpath. 

7232 for p in c.all_positions(): 

7233 if p.isAnyAtFileNode(): 

7234 if path == g.os_path_normpath(g.fullPath(c, p)): # #2049. Do *not* use os.path.normpath. 

7235 return p 

7236 return None 

7237#@+node:ekr.20210303123423.1: *4* findNodeInChildren 

7238def findNodeInChildren(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]: 

7239 """Search for a node in v's tree matching the given headline.""" 

7240 p1 = p.copy() 

7241 h = headline.strip() 

7242 for p in p1.children(): 

7243 if p.h.strip() == h: 

7244 return p.copy() 

7245 if not exact: 

7246 for p in p1.children(): 

7247 if p.h.strip().startswith(h): 

7248 return p.copy() 

7249 return None 

7250#@+node:ekr.20210303123423.2: *4* findNodeInTree 

7251def findNodeInTree(c: Cmdr, p: Pos, headline: str, exact: bool=True) -> Optional[Pos]: 

7252 """Search for a node in v's tree matching the given headline.""" 

7253 h = headline.strip() 

7254 p1 = p.copy() 

7255 for p in p1.subtree(): 

7256 if p.h.strip() == h: 

7257 return p.copy() 

7258 if not exact: 

7259 for p in p1.subtree(): 

7260 if p.h.strip().startswith(h): 

7261 return p.copy() 

7262 return None 

7263#@+node:ekr.20210303123423.4: *4* findTopLevelNode 

7264def findTopLevelNode(c: Cmdr, headline: str, exact: bool=True) -> Optional[Pos]: 

7265 h = headline.strip() 

7266 for p in c.rootPosition().self_and_siblings(copy=False): 

7267 if p.h.strip() == h: 

7268 return p.copy() 

7269 if not exact: 

7270 for p in c.rootPosition().self_and_siblings(copy=False): 

7271 if p.h.strip().startswith(h): 

7272 return p.copy() 

7273 return None 

7274#@-others 

7275#@+node:EKR.20040614071102.1: *3* g.getScript & helpers 

7276def getScript( 

7277 c: Cmdr, 

7278 p: Pos, 

7279 useSelectedText: bool=True, 

7280 forcePythonSentinels: bool=True, 

7281 useSentinels: bool=True, 

7282) -> str: 

7283 """ 

7284 Return the expansion of the selected text of node p. 

7285 Return the expansion of all of node p's body text if 

7286 p is not the current node or if there is no text selection. 

7287 """ 

7288 w = c.frame.body.wrapper 

7289 if not p: 

7290 p = c.p 

7291 try: 

7292 if g.app.inBridge: 

7293 s = p.b 

7294 elif w and p == c.p and useSelectedText and w.hasSelection(): 

7295 s = w.getSelectedText() 

7296 else: 

7297 s = p.b 

7298 # Remove extra leading whitespace so the user may execute indented code. 

7299 s = textwrap.dedent(s) 

7300 s = g.extractExecutableString(c, p, s) 

7301 script = g.composeScript(c, p, s, 

7302 forcePythonSentinels=forcePythonSentinels, 

7303 useSentinels=useSentinels) 

7304 except Exception: 

7305 g.es_print("unexpected exception in g.getScript") 

7306 g.es_exception() 

7307 script = '' 

7308 return script 

7309#@+node:ekr.20170228082641.1: *4* g.composeScript 

7310def composeScript( 

7311 c: Cmdr, 

7312 p: Pos, 

7313 s: str, 

7314 forcePythonSentinels: bool=True, 

7315 useSentinels: bool=True, 

7316) -> str: 

7317 """Compose a script from p.b.""" 

7318 # This causes too many special cases. 

7319 # if not g.unitTesting and forceEncoding: 

7320 # aList = g.get_directives_dict_list(p) 

7321 # encoding = scanAtEncodingDirectives(aList) or 'utf-8' 

7322 # s = g.insertCodingLine(encoding,s) 

7323 if not s.strip(): 

7324 return '' 

7325 at = c.atFileCommands # type:ignore 

7326 old_in_script = g.app.inScript 

7327 try: 

7328 # #1297: set inScript flags. 

7329 g.app.inScript = g.inScript = True 

7330 g.app.scriptDict["script1"] = s 

7331 # Important: converts unicode to utf-8 encoded strings. 

7332 script = at.stringToString(p.copy(), s, 

7333 forcePythonSentinels=forcePythonSentinels, 

7334 sentinels=useSentinels) 

7335 # Important, the script is an **encoded string**, not a unicode string. 

7336 script = script.replace("\r\n", "\n") # Use brute force. 

7337 g.app.scriptDict["script2"] = script 

7338 finally: 

7339 g.app.inScript = g.inScript = old_in_script 

7340 return script 

7341#@+node:ekr.20170123074946.1: *4* g.extractExecutableString 

7342def extractExecutableString(c: Cmdr, p: Pos, s: str) -> str: 

7343 """ 

7344 Return all lines for the given @language directive. 

7345 

7346 Ignore all lines under control of any other @language directive. 

7347 """ 

7348 # 

7349 # Rewritten to fix #1071. 

7350 if g.unitTesting: 

7351 return s # Regretable, but necessary. 

7352 # 

7353 # Return s if no @language in effect. Should never happen. 

7354 language = g.scanForAtLanguage(c, p) 

7355 if not language: 

7356 return s 

7357 # 

7358 # Return s if @language is unambiguous. 

7359 pattern = r'^@language\s+(\w+)' 

7360 matches = list(re.finditer(pattern, s, re.MULTILINE)) 

7361 if len(matches) < 2: 

7362 return s 

7363 # 

7364 # Scan the lines, extracting only the valid lines. 

7365 extracting, result = False, [] 

7366 for i, line in enumerate(g.splitLines(s)): 

7367 m = re.match(pattern, line) 

7368 if m: 

7369 # g.trace(language, m.group(1)) 

7370 extracting = m.group(1) == language 

7371 elif extracting: 

7372 result.append(line) 

7373 return ''.join(result) 

7374#@+node:ekr.20060624085200: *3* g.handleScriptException 

7375def handleScriptException(c: Cmdr, p: Pos, script: str, script1: str) -> None: 

7376 g.warning("exception executing script") 

7377 full = c.config.getBool('show-full-tracebacks-in-scripts') 

7378 fileName, n = g.es_exception(full=full) 

7379 # Careful: this test is no longer guaranteed. 

7380 if p.v.context == c: 

7381 try: 

7382 c.goToScriptLineNumber(n, p) 

7383 #@+<< dump the lines near the error >> 

7384 #@+node:EKR.20040612215018: *4* << dump the lines near the error >> 

7385 if g.os_path_exists(fileName): 

7386 with open(fileName) as f: 

7387 lines = f.readlines() 

7388 else: 

7389 lines = g.splitLines(script) 

7390 s = '-' * 20 

7391 g.es_print('', s) 

7392 # Print surrounding lines. 

7393 i = max(0, n - 2) 

7394 j = min(n + 2, len(lines)) 

7395 while i < j: 

7396 ch = '*' if i == n - 1 else ' ' 

7397 s = f"{ch} line {i+1:d}: {lines[i]}" 

7398 g.es('', s, newline=False) 

7399 i += 1 

7400 #@-<< dump the lines near the error >> 

7401 except Exception: 

7402 g.es_print('Unexpected exception in g.handleScriptException') 

7403 g.es_exception() 

7404#@+node:ekr.20140209065845.16767: *3* g.insertCodingLine 

7405def insertCodingLine(encoding: str, script: str) -> str: 

7406 """ 

7407 Insert a coding line at the start of script s if no such line exists. 

7408 The coding line must start with @first because it will be passed to 

7409 at.stringToString. 

7410 """ 

7411 if script: 

7412 tag = '@first # -*- coding:' 

7413 lines = g.splitLines(script) 

7414 for s in lines: 

7415 if s.startswith(tag): 

7416 break 

7417 else: 

7418 lines.insert(0, f"{tag} {encoding} -*-\n") 

7419 script = ''.join(lines) 

7420 return script 

7421#@+node:ekr.20070524083513: ** g.Unit Tests 

7422#@+node:ekr.20210901071523.1: *3* g.run_coverage_tests 

7423def run_coverage_tests(module: str='', filename: str='') -> None: 

7424 """ 

7425 Run the coverage tests given by the module and filename strings. 

7426 """ 

7427 unittests_dir = g.os_path_finalize_join(g.app.loadDir, '..', 'unittests') 

7428 assert os.path.exists(unittests_dir) 

7429 os.chdir(unittests_dir) 

7430 prefix = r"python -m pytest --cov-report html --cov-report term-missing --cov " 

7431 command = f"{prefix} {module} {filename}" 

7432 g.execute_shell_commands(command, trace=False) 

7433#@+node:ekr.20200221050038.1: *3* g.run_unit_test_in_separate_process 

7434def run_unit_test_in_separate_process(command: str) -> None: 

7435 """ 

7436 A script to be run from unitTest.leo. 

7437 

7438 Run the unit testing command (say `python -m leo.core.leoAst`) in a separate process. 

7439 """ 

7440 leo_editor_dir = os.path.join(g.app.loadDir, '..', '..') 

7441 os.chdir(leo_editor_dir) 

7442 p = subprocess.Popen( 

7443 shlex.split(command), 

7444 stdout=subprocess.PIPE, 

7445 stderr=subprocess.PIPE, 

7446 shell=sys.platform.startswith('win'), 

7447 ) 

7448 out, err = p.communicate() 

7449 err = g.toUnicode(err) 

7450 out = g.toUnicode(out) 

7451 print('') 

7452 print(command) 

7453 if out.strip(): 

7454 # print('traces...') 

7455 print(out.rstrip()) 

7456 print(err.rstrip()) 

7457 # There may be skipped tests... 

7458 err_lines = g.splitLines(err.rstrip()) 

7459 if not err_lines[-1].startswith('OK'): 

7460 g.trace('Test failed') 

7461 g.printObj(err_lines, tag='err_lines') 

7462 assert False 

7463#@+node:ekr.20210901065224.1: *3* g.run_unit_tests 

7464def run_unit_tests(tests: str=None, verbose: bool=False) -> None: 

7465 """ 

7466 Run the unit tests given by the "tests" string. 

7467 

7468 Run *all* unit tests if "tests" is not given. 

7469 """ 

7470 leo_editor_dir = g.os_path_finalize_join(g.app.loadDir, '..', '..') 

7471 os.chdir(leo_editor_dir) 

7472 verbosity = '-v' if verbose else '' 

7473 command = f"python -m unittest {verbosity} {tests or ''} " 

7474 # pytest reports too many errors. 

7475 # command = f"python -m pytest --pdb {tests or ''}" 

7476 g.execute_shell_commands(command, trace=False) 

7477#@+node:ekr.20120311151914.9916: ** g.Urls & UNLs 

7478unl_regex = re.compile(r'\bunl:.*$') 

7479 

7480kinds = '(file|ftp|gopher|http|https|mailto|news|nntp|prospero|telnet|wais)' 

7481url_regex = re.compile(fr"""{kinds}://[^\s'"]+[\w=/]""") 

7482#@+node:ekr.20120320053907.9776: *3* g.computeFileUrl 

7483def computeFileUrl(fn: str, c: Cmdr=None, p: Pos=None) -> str: 

7484 """ 

7485 Compute finalized url for filename fn. 

7486 """ 

7487 # First, replace special characters (especially %20, by their equivalent). 

7488 url = urllib.parse.unquote(fn) 

7489 # Finalize the path *before* parsing the url. 

7490 i = url.find('~') 

7491 if i > -1: 

7492 # Expand '~'. 

7493 path = url[i:] 

7494 path = g.os_path_expanduser(path) 

7495 # #1338: This is way too dangerous, and a serious security violation. 

7496 # path = c.os_path_expandExpression(path) 

7497 path = g.os_path_finalize(path) 

7498 url = url[:i] + path 

7499 else: 

7500 tag = 'file://' 

7501 tag2 = 'file:///' 

7502 if sys.platform.startswith('win') and url.startswith(tag2): 

7503 path = url[len(tag2) :].lstrip() 

7504 elif url.startswith(tag): 

7505 path = url[len(tag) :].lstrip() 

7506 else: 

7507 path = url 

7508 # #1338: This is way too dangerous, and a serious security violation. 

7509 # path = c.os_path_expandExpression(path) 

7510 # Handle ancestor @path directives. 

7511 if c and c.openDirectory: 

7512 base = c.getNodePath(p) 

7513 path = g.os_path_finalize_join(c.openDirectory, base, path) 

7514 else: 

7515 path = g.os_path_finalize(path) 

7516 url = f"{tag}{path}" 

7517 return url 

7518#@+node:ekr.20190608090856.1: *3* g.es_clickable_link 

7519def es_clickable_link(c: Cmdr, p: Pos, line_number: int, message: str) -> None: 

7520 """ 

7521 Write a clickable message to the given line number of p.b. 

7522 

7523 Negative line numbers indicate global lines. 

7524 

7525 """ 

7526 unl = p.get_UNL() 

7527 c.frame.log.put(message.strip() + '\n', nodeLink=f"{unl}::{line_number}") 

7528#@+node:tbrown.20140311095634.15188: *3* g.findUNL & helpers 

7529def findUNL(unlList1: List[str], c: Cmdr) -> Optional[Pos]: 

7530 """ 

7531 Find and move to the unl given by the unlList in the commander c. 

7532 Return the found position, or None. 

7533 """ 

7534 # Define the unl patterns. 

7535 old_pat = re.compile(r'^(.*):(\d+),?(\d+)?,?([-\d]+)?,?(\d+)?$') # ':' is the separator. 

7536 new_pat = re.compile(r'^(.*?)(::)([-\d]+)?$') # '::' is the separator. 

7537 

7538 #@+others # Define helper functions 

7539 #@+node:ekr.20220213142925.1: *4* function: convert_unl_list 

7540 def convert_unl_list(aList: List[str]) -> List[str]: 

7541 """ 

7542 Convert old-style UNLs to new UNLs, retaining line numbers if possible. 

7543 """ 

7544 result = [] 

7545 for s in aList: 

7546 # Try to get the line number. 

7547 for m, line_group in ( 

7548 (old_pat.match(s), 4), 

7549 (new_pat.match(s), 3), 

7550 ): 

7551 if m: 

7552 try: 

7553 n = int(m.group(line_group)) 

7554 result.append(f"{m.group(1)}::{n}") 

7555 continue 

7556 except Exception: 

7557 pass 

7558 # Finally, just add the whole UNL. 

7559 result.append(s) 

7560 return result 

7561 #@+node:ekr.20220213142735.1: *4* function: full_match 

7562 def full_match(p: Pos) -> bool: 

7563 """Return True if the headlines of p and all p's parents match unlList.""" 

7564 # Careful: make copies. 

7565 aList, p1 = unlList[:], p.copy() 

7566 while aList and p1: 

7567 m = new_pat.match(aList[-1]) 

7568 if m and m.group(1).strip() != p1.h.strip(): 

7569 return False 

7570 if not m and aList[-1].strip() != p1.h.strip(): 

7571 return False 

7572 aList.pop() 

7573 p1.moveToParent() 

7574 return not aList 

7575 #@-others 

7576 

7577 unlList = convert_unl_list(unlList1) 

7578 if not unlList: 

7579 return None 

7580 # Find all target headlines. 

7581 targets = [] 

7582 m = new_pat.match(unlList[-1]) 

7583 target = m and m.group(1) or unlList[-1] 

7584 targets.append(target) 

7585 targets.extend(unlList[:-1]) 

7586 # Find all target positions. Prefer later positions. 

7587 positions = list(reversed(list(z for z in c.all_positions() if z.h.strip() in targets))) 

7588 while unlList: 

7589 for p in positions: 

7590 p1 = p.copy() 

7591 if full_match(p): 

7592 assert p == p1, (p, p1) 

7593 n = 0 # The default line number. 

7594 # Parse the last target. 

7595 m = new_pat.match(unlList[-1]) 

7596 if m: 

7597 line = m.group(3) 

7598 try: 

7599 n = int(line) 

7600 except(TypeError, ValueError): 

7601 g.trace('bad line number', line) 

7602 if n == 0: 

7603 c.redraw(p) 

7604 elif n < 0: 

7605 p, offset, ok = c.gotoCommands.find_file_line(-n, p) # Calls c.redraw(). 

7606 return p if ok else None 

7607 elif n > 0: 

7608 insert_point = sum(len(i) + 1 for i in p.b.split('\n')[: n - 1]) 

7609 c.redraw(p) 

7610 c.frame.body.wrapper.setInsertPoint(insert_point) 

7611 c.frame.bringToFront() 

7612 c.bodyWantsFocusNow() 

7613 return p 

7614 # Not found. Pop the first parent from unlList. 

7615 unlList.pop(0) 

7616 return None 

7617#@+node:ekr.20120311151914.9917: *3* g.getUrlFromNode 

7618def getUrlFromNode(p: Pos) -> Optional[str]: 

7619 """ 

7620 Get an url from node p: 

7621 1. Use the headline if it contains a valid url. 

7622 2. Otherwise, look *only* at the first line of the body. 

7623 """ 

7624 if not p: 

7625 return None 

7626 c = p.v.context 

7627 assert c 

7628 table = [p.h, g.splitLines(p.b)[0] if p.b else ''] 

7629 table = [s[4:] if g.match_word(s, 0, '@url') else s for s in table] 

7630 table = [s.strip() for s in table if s.strip()] 

7631 # First, check for url's with an explicit scheme. 

7632 for s in table: 

7633 if g.isValidUrl(s): 

7634 return s 

7635 # Next check for existing file and add a file:// scheme. 

7636 for s in table: 

7637 tag = 'file://' 

7638 url = computeFileUrl(s, c=c, p=p) 

7639 if url.startswith(tag): 

7640 fn = url[len(tag) :].lstrip() 

7641 fn = fn.split('#', 1)[0] 

7642 if g.os_path_isfile(fn): 

7643 # Return the *original* url, with a file:// scheme. 

7644 # g.handleUrl will call computeFileUrl again. 

7645 return 'file://' + s 

7646 # Finally, check for local url's. 

7647 for s in table: 

7648 if s.startswith("#"): 

7649 return s 

7650 return None 

7651#@+node:ekr.20170221063527.1: *3* g.handleUnl 

7652def handleUnl(unl: str, c: Cmdr) -> Any: 

7653 """ 

7654 Handle a Leo UNL. This must *never* open a browser. 

7655 

7656 Return the commander for the found UNL, or None. 

7657  

7658 Redraw the commander if the UNL is found. 

7659 """ 

7660 if not unl: 

7661 return None 

7662 unll = unl.lower() 

7663 if unll.startswith('unl://'): 

7664 unl = unl[6:] 

7665 elif unll.startswith('file://'): 

7666 unl = unl[7:] 

7667 unl = unl.strip() 

7668 if not unl: 

7669 return None 

7670 unl = g.unquoteUrl(unl) 

7671 # Compute path and unl. 

7672 if '#' not in unl and '-->' not in unl: 

7673 # The path is the entire unl. 

7674 path, unl = unl, None 

7675 elif '#' not in unl: 

7676 # The path is empty. 

7677 # Move to the unl in *this* commander. 

7678 p = g.findUNL(unl.split("-->"), c) 

7679 if p: 

7680 c.redraw(p) 

7681 return c 

7682 else: 

7683 path, unl = unl.split('#', 1) 

7684 if unl and not path: # #2407 

7685 # Move to the unl in *this* commander. 

7686 p = g.findUNL(unl.split("-->"), c) 

7687 if p: 

7688 c.redraw(p) 

7689 return c 

7690 if c: 

7691 base = g.os_path_dirname(c.fileName()) 

7692 c_path = g.os_path_finalize_join(base, path) 

7693 else: 

7694 c_path = None 

7695 # Look for the file in various places. 

7696 table = ( 

7697 c_path, 

7698 g.os_path_finalize_join(g.app.loadDir, '..', path), 

7699 g.os_path_finalize_join(g.app.loadDir, '..', '..', path), 

7700 g.os_path_finalize_join(g.app.loadDir, '..', 'core', path), 

7701 g.os_path_finalize_join(g.app.loadDir, '..', 'config', path), 

7702 g.os_path_finalize_join(g.app.loadDir, '..', 'dist', path), 

7703 g.os_path_finalize_join(g.app.loadDir, '..', 'doc', path), 

7704 g.os_path_finalize_join(g.app.loadDir, '..', 'test', path), 

7705 g.app.loadDir, 

7706 g.app.homeDir, 

7707 ) 

7708 for path2 in table: 

7709 if path2 and path2.lower().endswith('.leo') and os.path.exists(path2): 

7710 path = path2 

7711 break 

7712 else: 

7713 g.es_print('path not found', repr(path)) 

7714 return None 

7715 # End editing in *this* outline, so typing in the new outline works. 

7716 c.endEditing() 

7717 c.redraw() 

7718 # Open the path. 

7719 c2 = g.openWithFileName(path, old_c=c) 

7720 if not c2: 

7721 return None 

7722 # Find and redraw. 

7723 # #2445: Default to c2.rootPosition(). 

7724 p = g.findUNL(unl.split("-->"), c2) or c2.rootPosition() 

7725 c2.redraw(p) 

7726 c2.bringToFront() 

7727 c2.bodyWantsFocusNow() 

7728 return c2 

7729#@+node:tbrown.20090219095555.63: *3* g.handleUrl & helpers 

7730def handleUrl(url: str, c: Cmdr=None, p: Pos=None) -> Any: 

7731 """Open a url or a unl.""" 

7732 if c and not p: 

7733 p = c.p 

7734 urll = url.lower() 

7735 if urll.startswith('@url'): 

7736 url = url[4:].lstrip() 

7737 if ( 

7738 urll.startswith('unl://') or 

7739 urll.startswith('file://') and url.find('-->') > -1 or 

7740 urll.startswith('#') 

7741 ): 

7742 return g.handleUnl(url, c) 

7743 try: 

7744 return g.handleUrlHelper(url, c, p) 

7745 except Exception: 

7746 g.es_print("g.handleUrl: exception opening", repr(url)) 

7747 g.es_exception() 

7748 return None 

7749#@+node:ekr.20170226054459.1: *4* g.handleUrlHelper 

7750def handleUrlHelper(url: str, c: Cmdr, p: Pos) -> None: 

7751 """Open a url. Most browsers should handle: 

7752 ftp://ftp.uu.net/public/whatever 

7753 http://localhost/MySiteUnderDevelopment/index.html 

7754 file:///home/me/todolist.html 

7755 """ 

7756 tag = 'file://' 

7757 original_url = url 

7758 if url.startswith(tag) and not url.startswith(tag + '#'): 

7759 # Finalize the path *before* parsing the url. 

7760 url = g.computeFileUrl(url, c=c, p=p) 

7761 parsed = urlparse.urlparse(url) 

7762 if parsed.netloc: 

7763 leo_path = os.path.join(parsed.netloc, parsed.path) 

7764 # "readme.txt" gets parsed into .netloc... 

7765 else: 

7766 leo_path = parsed.path 

7767 if leo_path.endswith('\\'): 

7768 leo_path = leo_path[:-1] 

7769 if leo_path.endswith('/'): 

7770 leo_path = leo_path[:-1] 

7771 if parsed.scheme == 'file' and leo_path.endswith('.leo'): 

7772 g.handleUnl(original_url, c) 

7773 elif parsed.scheme in ('', 'file'): 

7774 unquote_path = g.unquoteUrl(leo_path) 

7775 if g.unitTesting: 

7776 pass 

7777 elif g.os_path_exists(leo_path): 

7778 g.os_startfile(unquote_path) 

7779 else: 

7780 g.es(f"File '{leo_path}' does not exist") 

7781 else: 

7782 if g.unitTesting: 

7783 pass 

7784 else: 

7785 # Mozilla throws a weird exception, then opens the file! 

7786 try: 

7787 webbrowser.open(url) 

7788 except Exception: 

7789 pass 

7790#@+node:ekr.20170226060816.1: *4* g.traceUrl 

7791def traceUrl(c: Cmdr, path: str, parsed: Any, url: str) -> None: 

7792 

7793 print() 

7794 g.trace('url ', url) 

7795 g.trace('c.frame.title', c.frame.title) 

7796 g.trace('path ', path) 

7797 g.trace('parsed.fragment', parsed.fragment) 

7798 g.trace('parsed.netloc', parsed.netloc) 

7799 g.trace('parsed.path ', parsed.path) 

7800 g.trace('parsed.scheme', repr(parsed.scheme)) 

7801#@+node:ekr.20120311151914.9918: *3* g.isValidUrl 

7802def isValidUrl(url: str) -> bool: 

7803 """Return true if url *looks* like a valid url.""" 

7804 table = ( 

7805 'file', 'ftp', 'gopher', 'hdl', 'http', 'https', 'imap', 

7806 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu', 

7807 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', 'wais', 

7808 ) 

7809 if url.lower().startswith('unl://') or url.startswith('#'): 

7810 # All Leo UNL's. 

7811 return True 

7812 if url.startswith('@'): 

7813 return False 

7814 parsed = urlparse.urlparse(url) 

7815 scheme = parsed.scheme 

7816 for s in table: 

7817 if scheme.startswith(s): 

7818 return True 

7819 return False 

7820#@+node:ekr.20120315062642.9744: *3* g.openUrl 

7821def openUrl(p: Pos) -> None: 

7822 """ 

7823 Open the url of node p. 

7824 Use the headline if it contains a valid url. 

7825 Otherwise, look *only* at the first line of the body. 

7826 """ 

7827 if p: 

7828 url = g.getUrlFromNode(p) 

7829 if url: 

7830 c = p.v.context 

7831 if not g.doHook("@url1", c=c, p=p, url=url): 

7832 g.handleUrl(url, c=c, p=p) 

7833 g.doHook("@url2", c=c, p=p, url=url) 

7834#@+node:ekr.20110605121601.18135: *3* g.openUrlOnClick (open-url-under-cursor) 

7835def openUrlOnClick(event: Any, url: str=None) -> Optional[str]: 

7836 """Open the URL under the cursor. Return it for unit testing.""" 

7837 # This can be called outside Leo's command logic, so catch all exceptions. 

7838 try: 

7839 return openUrlHelper(event, url) 

7840 except Exception: 

7841 g.es_exception() 

7842 return None 

7843#@+node:ekr.20170216091704.1: *4* g.openUrlHelper 

7844def openUrlHelper(event: Any, url: str=None) -> Optional[str]: 

7845 """Open the UNL or URL under the cursor. Return it for unit testing.""" 

7846 c = getattr(event, 'c', None) 

7847 if not c: 

7848 return None 

7849 w = getattr(event, 'w', c.frame.body.wrapper) 

7850 if not g.app.gui.isTextWrapper(w): 

7851 g.internalError('must be a text wrapper', w) 

7852 return None 

7853 setattr(event, 'widget', w) 

7854 # Part 1: get the url. 

7855 if url is None: 

7856 s = w.getAllText() 

7857 ins = w.getInsertPoint() 

7858 i, j = w.getSelectionRange() 

7859 if i != j: 

7860 return None # So find doesn't open the url. 

7861 row, col = g.convertPythonIndexToRowCol(s, ins) 

7862 i, j = g.getLine(s, ins) 

7863 line = s[i:j] 

7864 

7865 # Navigation target types: 

7866 #@+<< gnx >> 

7867 #@+node:tom.20220328142302.1: *5* << gnx >> 

7868 match = target = None 

7869 for match in GNXre.finditer(line): 

7870 # Don't open if we click after the gnx. 

7871 if match.start() <= col < match.end(): 

7872 target = match.group() 

7873 break 

7874 

7875 if target: 

7876 # pylint: disable=undefined-loop-variable 

7877 found_gnx = target_is_self = False 

7878 if c.p.gnx == target: 

7879 found_gnx = target_is_self = True 

7880 else: 

7881 for p in c.all_unique_positions(): 

7882 if p.v.gnx == target: 

7883 found_gnx = True 

7884 break 

7885 if found_gnx: 

7886 if not target_is_self: 

7887 c.selectPosition(p) 

7888 c.redraw() 

7889 return target 

7890 #@-<< gnx >> 

7891 #@+<< section ref >> 

7892 #@+node:tom.20220328141455.1: *5* << section ref >> 

7893 # Navigate to section reference if one was clicked. 

7894 l_ = line.strip() 

7895 if l_.startswith('<<') and l_.endswith('>>'): 

7896 p = c.p 

7897 px = None 

7898 for p1 in p.subtree(): 

7899 if p1.h.strip() == l_: 

7900 px = p1 

7901 break 

7902 if px: 

7903 c.selectPosition(px) 

7904 c.redraw() 

7905 #@-<< section ref >> 

7906 #@+<< url or unl >> 

7907 #@+node:tom.20220328141544.1: *5* << url or unl >> 

7908 # Find the url on the line. 

7909 for match in g.url_regex.finditer(line): 

7910 # Don't open if we click after the url. 

7911 if match.start() <= col < match.end(): 

7912 url = match.group() 

7913 if g.isValidUrl(url): 

7914 break 

7915 else: 

7916 # Look for the unl: 

7917 for match in g.unl_regex.finditer(line): 

7918 # Don't open if we click after the unl. 

7919 if match.start() <= col < match.end(): 

7920 unl = match.group() 

7921 g.handleUnl(unl, c) 

7922 return None 

7923 #@-<< url or unl >> 

7924 

7925 elif not isinstance(url, str): 

7926 url = url.toString() 

7927 url = g.toUnicode(url) # #571 

7928 if url and g.isValidUrl(url): 

7929 # Part 2: handle the url 

7930 p = c.p 

7931 if not g.doHook("@url1", c=c, p=p, url=url): 

7932 g.handleUrl(url, c=c, p=p) 

7933 g.doHook("@url2", c=c, p=p) 

7934 return url 

7935 # Part 3: call find-def. 

7936 if not w.hasSelection(): 

7937 c.editCommands.extendToWord(event, select=True) 

7938 word = w.getSelectedText().strip() 

7939 if not word: 

7940 return None 

7941 p, pos, newpos = c.findCommands.find_def_strict(event) 

7942 if p: 

7943 return None 

7944 # Part 4: #2546: look for a file name. 

7945 s = w.getAllText() 

7946 i, j = w.getSelectionRange() 

7947 m = re.match(r'(\w+)\.(\w){1,4}\b', s[i:]) 

7948 if not m: 

7949 return None 

7950 # Find the first node whose headline ends with the filename. 

7951 filename = m.group(0) 

7952 for p in c.all_unique_positions(): 

7953 if p.h.strip().endswith(filename): 

7954 # Set the find text. 

7955 c.findCommands.ftm.set_find_text(filename) 

7956 # Select. 

7957 c.redraw(p) 

7958 break 

7959 return None 

7960#@+node:ekr.20170226093349.1: *3* g.unquoteUrl 

7961def unquoteUrl(url: str) -> str: 

7962 """Replace special characters (especially %20, by their equivalent).""" 

7963 return urllib.parse.unquote(url) 

7964#@-others 

7965# set g when the import is about to complete. 

7966g: Any = sys.modules.get('leo.core.leoGlobals') 

7967assert g, sorted(sys.modules.keys()) 

7968if __name__ == '__main__': 

7969 unittest.main() 

7970 

7971#@@language python 

7972#@@tabwidth -4 

7973#@@pagewidth 70 

7974#@-leo