Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#@+leo-ver=5-thin 

2#@+node:felix.20210621233316.1: * @file leoserver.py 

3#@@language python 

4#@@tabwidth -4 

5""" 

6Leo's internet server. 

7 

8Written by Félix Malboeuf and Edward K. Ream. 

9""" 

10# pylint: disable=import-self,raise-missing-from,wrong-import-position 

11#@+<< imports >> 

12#@+node:felix.20210621233316.2: ** << imports >> 

13import argparse 

14import asyncio 

15import inspect 

16import json 

17import os 

18import sys 

19import socket 

20import textwrap 

21import time 

22import tkinter as Tk 

23from typing import List, Union 

24# Third-party. 

25# #2300 

26try: 

27 import websockets 

28except Exception: 

29 websockets = None 

30# Make sure leo-editor folder is on sys.path. 

31core_dir = os.path.dirname(__file__) 

32leo_path = os.path.normpath(os.path.join(core_dir, '..', '..')) 

33assert os.path.exists(leo_path), repr(leo_path) 

34if leo_path not in sys.path: 

35 sys.path.append(leo_path) 

36# Leo 

37from leo.core.leoNodes import Position 

38from leo.core.leoGui import StringFindTabManager 

39from leo.core.leoExternalFiles import ExternalFilesController 

40#@-<< imports >> 

41version_tuple = (1, 0, 1) 

42v1, v2, v3 = version_tuple 

43__version__ = f"leoserver.py version {v1}.{v2}.{v3}" 

44g = None # The bridge's leoGlobals module. 

45 

46# Server defaults 

47SERVER_STARTED_TOKEN = "LeoBridge started" # Output when started successfully 

48# Websocket connections (to be sent 'notify' messages) 

49connectionsPool = set() # type:ignore 

50connectionsTotal = 0 # Current connected client total 

51# Customizable server options 

52argFile = "" 

53traces: List = [] 

54wsLimit = 1 

55wsPersist = False 

56wsSkipDirty = False 

57wsHost = "localhost" 

58wsPort = 32125 

59 

60#@+others 

61#@+node:felix.20210712224107.1: ** setup JSON encoder 

62class SetEncoder(json.JSONEncoder): 

63 def default(self, obj): 

64 if isinstance(obj, set): 

65 return list(obj) 

66 return json.JSONEncoder.default(self, obj) 

67#@+node:felix.20210621233316.3: ** Exception classes 

68class InternalServerError(Exception): # pragma: no cover 

69 """The server violated its own coding conventions.""" 

70 pass 

71 

72class ServerError(Exception): # pragma: no cover 

73 """The server received an erroneous package.""" 

74 pass 

75 

76class TerminateServer(Exception): # pragma: no cover 

77 """Ask the server to terminate.""" 

78 pass 

79#@+node:felix.20210626222905.1: ** class ServerExternalFilesController 

80class ServerExternalFilesController(ExternalFilesController): 

81 """EFC Modified from Leo's sources""" 

82 # pylint: disable=no-else-return 

83 

84 #@+others 

85 #@+node:felix.20210626222905.2: *3* sefc.ctor 

86 def __init__(self): 

87 """Ctor for ExternalFiles class.""" 

88 super().__init__() 

89 

90 self.on_idle_count = 0 

91 # Keys are full paths, values are modification times. 

92 # DO NOT alter directly, use set_time(path) and 

93 # get_time(path), see set_time() for notes. 

94 self.yesno_all_time: Union[None, bool, float] = None # previous yes/no to all answer, time of answer 

95 self.yesno_all_answer = None # answer, 'yes-all', or 'no-all' 

96 

97 # if yesAll/noAll forced, then just show info message after idle_check_commander 

98 self.infoMessage = None 

99 # False or "detected", "refreshed" or "ignored" 

100 

101 g.app.idleTimeManager.add_callback(self.on_idle) 

102 

103 self.waitingForAnswer = False 

104 self.lastPNode = None # last p node that was asked for if not set to "AllYes\AllNo" 

105 self.lastCommander = None 

106 #@+node:felix.20210626222905.6: *3* sefc.clientResult 

107 def clientResult(self, p_result): 

108 """Received result from connected client that was 'asked' yes/no/... """ 

109 # Got the result to an asked question/warning from the client 

110 if not self.waitingForAnswer: 

111 print("ERROR: Received Result but no Asked Dialog", flush=True) 

112 return 

113 

114 # check if p_result was from a warn (ok) or an ask ('yes','yes-all','no','no-all') 

115 # act accordingly 

116 

117 # 1- if ok, unblock 'warn' 

118 # 2- if no, unblock 'ask' 

119 # ------------------------------------------ Nothing special to do 

120 

121 # 3- if noAll: set noAll, and unblock 'ask' 

122 if p_result and "-all" in p_result.lower(): 

123 self.yesno_all_time = time.time() 

124 self.yesno_all_answer = p_result.lower() 

125 # ------------------------------------------ Also covers setting yesAll in #5 

126 

127 path = "" 

128 if self.lastPNode: 

129 path = g.fullPath(self.lastCommander, self.lastPNode) 

130 # 4- if yes: REFRESH self.lastPNode, and unblock 'ask' 

131 # 5- if yesAll: REFRESH self.lastPNode, set yesAll, and unblock 'ask' 

132 if bool(p_result and 'yes' in p_result.lower()): 

133 self.lastCommander.selectPosition(self.lastPNode) 

134 self.lastCommander.refreshFromDisk() 

135 elif self.lastCommander: 

136 path = self.lastCommander.fileName() 

137 # 6- Same but for Leo file commander (close and reopen .leo file) 

138 if bool(p_result and 'yes' in p_result.lower()): 

139 # self.lastCommander.close() Stops too much if last file closed 

140 g.app.closeLeoWindow(self.lastCommander.frame, finish_quit=False) 

141 g.leoServer.open_file({"filename":path }) # ignore returned value 

142 

143 # Always update the path & time to prevent future warnings for this path. 

144 if path: 

145 self.set_time(path) 

146 self.checksum_d[path] = self.checksum(path) 

147 

148 self.waitingForAnswer = False # unblock 

149 # unblock: run the loop as if timer had hit 

150 if self.lastCommander: 

151 self.idle_check_commander(self.lastCommander) 

152 #@+node:felix.20210714205425.1: *3* sefc.entries 

153 #@+node:felix.20210626222905.19: *4* sefc.check_overwrite 

154 def check_overwrite(self, c, path): 

155 if self.has_changed(path): 

156 package = {"async": "info", "message": "Overwritten "+ path} 

157 g.leoServer._send_async_output(package, True) 

158 return True 

159 

160 #@+node:felix.20210714205604.1: *4* sefc.on_idle & helpers 

161 def on_idle(self): 

162 """ 

163 Check for changed open-with files and all external files in commanders 

164 for which @bool check_for_changed_external_file is True. 

165 """ 

166 # Fix for flushing the terminal console to pass through 

167 sys.stdout.flush() 

168 

169 if not g.app or g.app.killed: 

170 return 

171 if self.waitingForAnswer: 

172 return 

173 

174 self.on_idle_count += 1 

175 

176 if self.unchecked_commanders: 

177 # Check the next commander for which 

178 # @bool check_for_changed_external_file is True. 

179 c = self.unchecked_commanders.pop() 

180 self.lastCommander = c 

181 self.lastPNode = None # when none, a client result means its for the leo file. 

182 self.idle_check_commander(c) 

183 else: 

184 # Add all commanders for which 

185 # @bool check_for_changed_external_file is True. 

186 self.unchecked_commanders = [ 

187 z for z in g.app.commanders() if self.is_enabled(z) 

188 ] 

189 #@+node:felix.20210626222905.4: *5* sefc.idle_check_commander 

190 def idle_check_commander(self, c): 

191 """ 

192 Check all external files corresponding to @<file> nodes in c for 

193 changes. 

194 """ 

195 self.infoMessage = None # reset infoMessage 

196 # False or "detected", "refreshed" or "ignored" 

197 

198 # #1240: Check the .leo file itself. 

199 self.idle_check_leo_file(c) 

200 # 

201 # #1100: always scan the entire file for @<file> nodes. 

202 # #1134: Nested @<file> nodes are no longer valid, but this will do no harm. 

203 for p in c.all_unique_positions(): 

204 if self.waitingForAnswer: 

205 break 

206 if p.isAnyAtFileNode(): 

207 self.idle_check_at_file_node(c, p) 

208 

209 # if yesAll/noAll forced, then just show info message 

210 if self.infoMessage: 

211 package = {"async": "info", "message": self.infoMessage} 

212 g.leoServer._send_async_output(package, True) 

213 #@+node:felix.20210627013530.1: *5* sefc.idle_check_leo_file 

214 def idle_check_leo_file(self, c): 

215 """Check c's .leo file for external changes.""" 

216 path = c.fileName() 

217 if not self.has_changed(path): 

218 return 

219 # Always update the path & time to prevent future warnings. 

220 self.set_time(path) 

221 self.checksum_d[path] = self.checksum(path) 

222 # For now, ignore the #1888 fix method 

223 if self.ask(c, path): 

224 #reload Commander 

225 # self.lastCommander.close() Stops too much if last file closed 

226 g.app.closeLeoWindow(self.lastCommander.frame, finish_quit=False) 

227 g.leoServer.open_file({"filename":path }) # ignore returned value 

228 #@+node:felix.20210626222905.5: *5* sefc.idle_check_at_file_node 

229 def idle_check_at_file_node(self, c, p): 

230 """Check the @<file> node at p for external changes.""" 

231 trace = False 

232 path = g.fullPath(c, p) 

233 has_changed = self.has_changed(path) 

234 if trace: 

235 g.trace('changed', has_changed, p.h) 

236 if has_changed: 

237 self.lastPNode = p # can be set here because its the same process for ask/warn 

238 if p.isAtAsisFileNode() or p.isAtNoSentFileNode(): 

239 # Fix #1081: issue a warning. 

240 self.warn(c, path, p=p) 

241 elif self.ask(c, path, p=p): 

242 old_p = c.p # To restore selection if refresh option set to yes-all & is descendant of at-file 

243 c.selectPosition(self.lastPNode) 

244 c.refreshFromDisk() # Ends with selection on new c.p which is the at-file node 

245 # check with leoServer's config first, and if new c.p is ancestor of old_p 

246 if g.leoServer.leoServerConfig: 

247 if g.leoServer.leoServerConfig["defaultReloadIgnore"].lower()=='yes-all': 

248 if c.positionExists(old_p) and c.p.isAncestorOf(old_p): 

249 c.selectPosition(old_p) 

250 

251 # Always update the path & time to prevent future warnings. 

252 self.set_time(path) 

253 self.checksum_d[path] = self.checksum(path) 

254 #@+node:felix.20210626222905.18: *4* sefc.open_with 

255 def open_with(self, c, d): 

256 """open-with is bypassed in leoserver (for now)""" 

257 return 

258 

259 #@+node:felix.20210626222905.7: *3* sefc.utilities 

260 #@+node:felix.20210626222905.8: *4* sefc.ask 

261 def ask(self, c, path, p=None): 

262 """ 

263 Ask user whether to overwrite an @<file> tree. 

264 Return True if the user agrees by default, or skips and asks 

265 client, blocking further checks until result received. 

266 """ 

267 # check with leoServer's config first 

268 if g.leoServer.leoServerConfig: 

269 check_config = g.leoServer.leoServerConfig["defaultReloadIgnore"].lower() 

270 if not bool('none' in check_config): 

271 if bool('yes' in check_config): 

272 self.infoMessage = "refreshed" 

273 return True 

274 else: 

275 self.infoMessage = "ignored" 

276 return False 

277 # let original function resolve 

278 

279 if self.yesno_all_time + 3 >= time.time() and self.yesno_all_answer: 

280 self.yesno_all_time = time.time() # Still reloading? Extend time 

281 # if yesAll/noAll forced, then just show info message 

282 yesno_all_bool = bool('yes' in self.yesno_all_answer.lower()) 

283 return yesno_all_bool # We already have our answer here, so return it 

284 if not p: 

285 where = 'the outline node' 

286 else: 

287 where = p.h 

288 

289 _is_leo = path.endswith(('.leo', '.db')) 

290 

291 if _is_leo: 

292 s = '\n'.join([ 

293 f'{g.splitLongFileName(path)} has changed outside Leo.', 

294 'Reload it?' 

295 ]) 

296 else: 

297 s = '\n'.join([ 

298 f'{g.splitLongFileName(path)} has changed outside Leo.', 

299 f"Reload {where} in Leo?", 

300 ]) 

301 

302 package = {"async": "ask", "ask": 'Overwrite the version in Leo?', 

303 "message": s, "yes_all": not _is_leo, "no_all": not _is_leo} 

304 

305 g.leoServer._send_async_output(package) # Ask the connected client 

306 self.waitingForAnswer = True # Block the loop and further checks until 'clientResult' 

307 return False # return false so as not to refresh until 'clientResult' says so 

308 #@+node:felix.20210626222905.13: *4* sefc.is_enabled 

309 def is_enabled(self, c): 

310 """Return the cached @bool check_for_changed_external_file setting.""" 

311 # check with the leoServer config first 

312 if g.leoServer.leoServerConfig: 

313 check_config = g.leoServer.leoServerConfig["checkForChangeExternalFiles"].lower() 

314 if bool('check' in check_config): 

315 return True 

316 if bool('ignore' in check_config): 

317 return False 

318 # let original function resolve 

319 return super().is_enabled(c) 

320 #@+node:felix.20210626222905.16: *4* sefc.warn 

321 def warn(self, c, path, p): 

322 """ 

323 Warn that an @asis or @nosent node has been changed externally. 

324 

325 There is *no way* to update the tree automatically. 

326 """ 

327 # check with leoServer's config first 

328 if g.leoServer.leoServerConfig: 

329 check_config = g.leoServer.leoServerConfig["defaultReloadIgnore"].lower() 

330 

331 if check_config != "none": 

332 # if not 'none' then do not warn, just infoMessage 'warn' at most 

333 if not self.infoMessage: 

334 self.infoMessage = "warn" 

335 return 

336 

337 if g.unitTesting or c not in g.app.commanders(): 

338 return 

339 if not p: 

340 g.trace('NO P') 

341 return 

342 

343 s = '\n'.join([ 

344 '%s has changed outside Leo.\n' % g.splitLongFileName( 

345 path), 

346 'Leo can not update this file automatically.\n', 

347 'This file was created from %s.\n' % p.h, 

348 'Warning: refresh-from-disk will destroy all children.' 

349 ]) 

350 

351 package = {"async": "warn", 

352 "warn": 'External file changed', "message": s} 

353 

354 g.leoServer._send_async_output(package, True) 

355 self.waitingForAnswer = True 

356 #@-others 

357#@+node:felix.20210621233316.4: ** class LeoServer 

358class LeoServer: 

359 """Leo Server Controller""" 

360 #@+others 

361 #@+node:felix.20210621233316.5: *3* server.__init__ 

362 def __init__(self, testing=False): 

363 

364 import leo.core.leoApp as leoApp 

365 import leo.core.leoBridge as leoBridge 

366 

367 global g 

368 t1 = time.process_time() 

369 # 

370 # Init ivars first. 

371 self.c = None # Currently Selected Commander. 

372 self.dummy_c = None # Set below, after we set g. 

373 self.action = None 

374 self.bad_commands_list = [] # Set below. 

375 # 

376 # Debug utilities 

377 self.current_id = 0 # Id of action being processed. 

378 self.log_flag = False # set by "log" key 

379 # 

380 # Start the bridge. 

381 self.bridge = leoBridge.controller( 

382 gui='nullGui', 

383 loadPlugins=True, # True: attempt to load plugins. 

384 readSettings=True, # True: read standard settings files. 

385 silent=True, # True: don't print signon messages. 

386 verbose=False, # True: prints messages that would be sent to the log pane. 

387 ) 

388 self.g = g = self.bridge.globals() # Also sets global 'g' object 

389 g.in_leo_server = True # #2098. 

390 g.leoServer = self # Set server singleton global reference 

391 self.leoServerConfig = None 

392 # * Intercept Log Pane output: Sends to client's log pane 

393 g.es = self._es # pointer - not a function call 

394 # 

395 # Set in _init_connection 

396 self.web_socket = None # Main Control Client 

397 self.loop = None 

398 # 

399 # To inspect commands 

400 self.dummy_c = g.app.newCommander(fileName=None) 

401 self.bad_commands_list = self._bad_commands(self.dummy_c) 

402 # 

403 # * Replacement instances to Leo's codebase : getScript, IdleTime and externalFilesController 

404 g.getScript = self._getScript 

405 g.IdleTime = self._idleTime 

406 # 

407 # override for "revert to file" operation 

408 g.app.gui.runAskOkDialog = self._runAskOkDialog 

409 g.app.gui.runAskYesNoDialog = self._runAskYesNoDialog 

410 g.app.gui.runAskYesNoCancelDialog = self._runAskYesNoCancelDialog 

411 g.app.gui.show_find_success = self._show_find_success 

412 self.headlineWidget = g.bunch(_name='tree') 

413 # 

414 # Complete the initialization, as in LeoApp.initApp. 

415 g.app.idleTimeManager = leoApp.IdleTimeManager() 

416 g.app.externalFilesController = ServerExternalFilesController() # Replace 

417 g.app.idleTimeManager.start() 

418 t2 = time.process_time() 

419 if not testing: 

420 print(f"LeoServer: init leoBridge in {t2-t1:4.2} sec.", flush=True) 

421 #@+node:felix.20210622235127.1: *3* server:leo overridden methods 

422 #@+node:felix.20210711194729.1: *4* LeoServer._runAskOkDialog 

423 def _runAskOkDialog(self, c, title, message=None, text="Ok"): 

424 """Create and run an askOK dialog .""" 

425 # Called by many commands in Leo 

426 if message: 

427 s = title + " " + message 

428 else: 

429 s = title 

430 package = {"async": "info", "message": s} 

431 g.leoServer._send_async_output(package) 

432 #@+node:felix.20210711194736.1: *4* LeoServer._runAskYesNoDialog 

433 def _runAskYesNoDialog(self, c, title, message=None, yes_all=False, no_all=False): 

434 """Create and run an askYesNo dialog.""" 

435 # used in ask with title: 'Overwrite the version in Leo?' 

436 # used in revert with title: 'Revert' 

437 # used in create ly leo settings with title: 'Create myLeoSettings.leo?' 

438 # used in move nodes with title: 'Move Marked Nodes?' 

439 s = "runAskYesNoDialog called" 

440 if title.startswith('Overwrite'): 

441 s = "@<file> tree was overwritten" 

442 elif title.startswith('Revert'): 

443 s= "Leo outline reverted to last saved contents" 

444 elif title.startswith('Create'): 

445 s= "myLeoSettings.leo created" 

446 elif title.startswith('Move'): 

447 s= "Marked nodes were moved" 

448 package = {"async": "info", "message": s} 

449 g.leoServer._send_async_output(package) 

450 return "yes" 

451 #@+node:felix.20210711194745.1: *4* LeoServer._runAskYesNoCancelDialog 

452 def _runAskYesNoCancelDialog(self, c, title, 

453 message=None, yesMessage="Yes", noMessage="No", 

454 yesToAllMessage=None, defaultButton="Yes", cancelMessage=None, 

455 ): 

456 """Create and run an askYesNoCancel dialog .""" 

457 # used in dangerous write with title: 'Overwrite existing file?' 

458 # used in prompt for save with title: 'Confirm' 

459 s = "runAskYesNoCancelDialog called" 

460 if title.startswith('Overwrite'): 

461 s= "File Overwritten" 

462 elif title.startswith('Confirm'): 

463 s= "File Saved" 

464 package = {"async": "info", "message": s} 

465 g.leoServer._send_async_output(package) 

466 return "yes" 

467 #@+node:felix.20210622235209.1: *4* LeoServer._es 

468 def _es(self, * args, **keys): # pragma: no cover (tested in client). 

469 """Output to the Log Pane""" 

470 d = { 

471 'color': None, 

472 'commas': False, 

473 'newline': True, 

474 'spaces': True, 

475 'tabName': 'Log', 

476 'nodeLink': None, 

477 } 

478 d = g.doKeywordArgs(keys, d) 

479 color = d.get('color') 

480 color = g.actualColor(color) 

481 s = g.translateArgs(args, d) 

482 package = {"async": "log", "log": s} 

483 if color: 

484 package["color"] = color 

485 self._send_async_output(package, True) 

486 #@+node:felix.20210626002856.1: *4* LeoServer._getScript 

487 def _getScript(self, c, p, 

488 useSelectedText=True, 

489 forcePythonSentinels=True, 

490 useSentinels=True, 

491 ): 

492 """ 

493 Return the expansion of the selected text of node p. 

494 Return the expansion of all of node p's body text if 

495 p is not the current node or if there is no text selection. 

496 """ 

497 w = c.frame.body.wrapper 

498 if not p: 

499 p = c.p 

500 try: 

501 if w and p == c.p and useSelectedText and w.hasSelection(): 

502 s = w.getSelectedText() 

503 else: 

504 s = p.b 

505 # Remove extra leading whitespace so the user may execute indented code. 

506 s = textwrap.dedent(s) 

507 s = g.extractExecutableString(c, p, s) 

508 script = g.composeScript(c, p, s, 

509 forcePythonSentinels=forcePythonSentinels, 

510 useSentinels=useSentinels) 

511 except Exception: 

512 g.es_print("unexpected exception in g.getScript", flush=True) 

513 g.es_exception() 

514 script = '' 

515 return script 

516 #@+node:felix.20210627004238.1: *4* LeoServer._asyncIdleLoop 

517 async def _asyncIdleLoop(self, seconds, func): 

518 while True: 

519 await asyncio.sleep(seconds) 

520 func(self) 

521 #@+node:felix.20210627004039.1: *4* LeoServer._idleTime 

522 def _idleTime(self, fn, delay, tag): 

523 asyncio.get_event_loop().create_task(self._asyncIdleLoop(delay/1000, fn)) 

524 #@+node:felix.20210626003327.1: *4* LeoServer._show_find_success 

525 def _show_find_success(self, c, in_headline, insert, p): 

526 """Handle a successful find match.""" 

527 if in_headline: 

528 g.app.gui.set_focus(c, self.headlineWidget) 

529 # no return 

530 #@+node:felix.20210621233316.6: *3* server:public commands 

531 #@+node:felix.20210621233316.7: *4* server:button commands 

532 # These will fail unless the open_file inits c.theScriptingController. 

533 #@+node:felix.20210621233316.8: *5* _check_button_command 

534 def _check_button_command(self, tag): # pragma: no cover (no scripting controller) 

535 """ 

536 Check that a button command is possible. 

537 Raise ServerError if not. Otherwise, return sc.buttonsDict. 

538 """ 

539 c = self._check_c() 

540 sc = getattr(c, "theScriptingController", None) 

541 if not sc: 

542 # This will happen unless mod_scripting is loaded! 

543 raise ServerError(f"{tag}: no scripting controller") 

544 return sc.buttonsDict 

545 #@+node:felix.20220220203658.1: *5* _get_rclickTree 

546 def _get_rclickTree(self, rclicks): 

547 rclickList = [] 

548 

549 for rc in rclicks: 

550 children = [] 

551 if rc.children: 

552 children = self._get_rclickTree(rc.children) 

553 rclickList.append({"name": rc.position.h, "children":children }) 

554 

555 return rclickList 

556 

557 

558 #@+node:felix.20210621233316.9: *5* server.click_button 

559 def click_button(self, param): # pragma: no cover (no scripting controller) 

560 """Handles buttons clicked in client from the '@button' panel""" 

561 tag = 'click_button' 

562 index = param.get("index") 

563 if not index: 

564 raise ServerError(f"{tag}: no button index given") 

565 d = self._check_button_command(tag) 

566 button = None 

567 for key in d: 

568 # Some button keys are objects so we have to convert first 

569 if str(key) == index: 

570 button = key 

571 

572 if not button: 

573 raise ServerError(f"{tag}: button {index!r} does not exist") 

574 

575 try: 

576 w_rclick = param.get("rclick", False) 

577 if w_rclick and hasattr(button, 'rclicks'): 

578 # not zero 

579 toChooseFrom = button.rclicks 

580 for i_rc in w_rclick: 

581 w_rclickChosen = toChooseFrom[i_rc] 

582 toChooseFrom = w_rclickChosen.children 

583 if w_rclickChosen: 

584 c = self._check_c() 

585 sc = getattr(c, "theScriptingController", None) 

586 sc.executeScriptFromButton(button, "", w_rclickChosen.position, "") 

587 

588 else: 

589 button.command() 

590 except Exception as e: 

591 raise ServerError(f"{tag}: exception clicking button {index!r}: {e}") 

592 # Tag along a possible return value with info sent back by _make_response 

593 return self._make_response() 

594 #@+node:felix.20210621233316.10: *5* server.get_buttons 

595 def get_buttons(self, param): # pragma: no cover (no scripting controller) 

596 """ 

597 Gets the currently opened file's @buttons list 

598 as an array of dict. 

599 

600 Typescript RClick recursive interface: 

601 RClick: {name: string, children: RClick[]} 

602 

603 Typescript return interface: 

604 { 

605 name: string; 

606 index: string; 

607 rclicks: RClick[]; 

608 }[] 

609 """ 

610 d = self._check_button_command('get_buttons') 

611 

612 buttons = [] 

613 # Some button keys are objects so we have to convert first 

614 for key in d: 

615 rclickList = [] 

616 if hasattr(key, 'rclicks'): 

617 rclickList = self._get_rclickTree(key.rclicks) 

618 # buttonRClicks = key.rclicks 

619 # for rc in buttonRClicks: 

620 # rclickList.append(rc.position.h) 

621 

622 entry = {"name": d[key], "index": str(key), "rclicks": rclickList} 

623 buttons.append(entry) 

624 

625 return self._make_minimal_response({ 

626 "buttons": buttons 

627 }) 

628 #@+node:felix.20210621233316.11: *5* server.remove_button 

629 def remove_button(self, param): # pragma: no cover (no scripting controller) 

630 """Remove button by index 'key string'.""" 

631 tag = 'remove_button' 

632 index = param.get("index") 

633 if not index: 

634 raise ServerError(f"{tag}: no button index given") 

635 d = self._check_button_command(tag) 

636 # Some button keys are objects so we have to convert first 

637 key = None 

638 for i_key in d: 

639 if str(i_key) == index: 

640 key = i_key 

641 if key: 

642 try: 

643 del d [key] 

644 except Exception as e: 

645 raise ServerError(f"{tag}: exception removing button {index!r}: {e}") 

646 else: 

647 raise ServerError(f"{tag}: button {index!r} does not exist") 

648 

649 return self._make_response() 

650 #@+node:felix.20211016235830.1: *5* server.goto_script 

651 def goto_script(self, param): # pragma: no cover (no scripting controller) 

652 """Goto the script this button originates.""" 

653 tag = 'goto_script' 

654 index = param.get("index") 

655 if not index: 

656 raise ServerError(f"{tag}: no button index given") 

657 d = self._check_button_command(tag) 

658 # Some button keys are objects so we have to convert first 

659 key = None 

660 for i_key in d: 

661 if str(i_key) == index: 

662 key = i_key 

663 if key: 

664 try: 

665 gnx = key.command.gnx 

666 c = self._check_c() 

667 # pylint: disable=undefined-loop-variable 

668 for p in c.all_positions(): 

669 if p.gnx == gnx: 

670 break 

671 if p: 

672 assert c.positionExists(p) 

673 c.selectPosition(p) 

674 else: 

675 raise ServerError(f"{tag}: not found {gnx}") 

676 except Exception as e: 

677 raise ServerError(f"{tag}: exception going to script of button {index!r}: {e}") 

678 else: 

679 raise ServerError(f"{tag}: button {index!r} does not exist") 

680 

681 return self._make_response() 

682 #@+node:felix.20210621233316.12: *4* server:file commands 

683 #@+node:felix.20210621233316.13: *5* server.open_file 

684 def open_file(self, param): 

685 """ 

686 Open a leo file with the given filename. 

687 Create a new document if no name. 

688 """ 

689 found, tag = False, 'open_file' 

690 filename = param.get('filename') # Optional. 

691 if filename: 

692 for c in g.app.commanders(): 

693 if c.fileName() == filename: 

694 found = True 

695 if not found: 

696 c = self.bridge.openLeoFile(filename) 

697 # Add ftm. This won't happen if opened outside leoserver 

698 c.findCommands.ftm = StringFindTabManager(c) 

699 if not c: # pragma: no cover 

700 raise ServerError(f"{tag}: bridge did not open {filename!r}") 

701 if not c.frame.body.wrapper: # pragma: no cover 

702 raise ServerError(f"{tag}: no wrapper") 

703 # Assign self.c 

704 self.c = c 

705 c.selectPosition(c.rootPosition()) # Required. 

706 # Check the outline! 

707 c.recreateGnxDict() # refresh c.fileCommands.gnxDict used in ap_to_p 

708 self._check_outline(c) 

709 if self.log_flag: # pragma: no cover 

710 self._dump_outline(c) 

711 

712 result = {"total": len(g.app.commanders()), "filename": self.c.fileName()} 

713 

714 return self._make_response(result) 

715 #@+node:felix.20210621233316.14: *5* server.open_files 

716 def open_files(self, param): 

717 """ 

718 Opens an array of leo files. 

719 Returns an object with total opened files 

720 and name of currently last opened & selected document. 

721 """ 

722 files = param.get('files') # Optional. 

723 if files: 

724 for i_file in files: 

725 if os.path.isfile(i_file): 

726 self.open_file({"filename": i_file}) 

727 total = len(g.app.commanders()) 

728 filename = self.c.fileName() if total else "" 

729 result = {"total": total, "filename": filename} 

730 return self._make_response(result) 

731 #@+node:felix.20210621233316.15: *5* server.set_opened_file 

732 def set_opened_file(self, param): 

733 """ 

734 Choose the new active commander from array of opened files. 

735 Returns an object with total opened files 

736 and name of currently last opened & selected document. 

737 """ 

738 tag = 'set_opened_file' 

739 index = param.get('index') 

740 total = len(g.app.commanders()) 

741 if total and index < total: 

742 self.c = g.app.commanders()[index] 

743 # maybe needed for frame wrapper 

744 self.c.selectPosition(self.c.p) 

745 self._check_outline(self.c) 

746 result = {"total": total, "filename": self.c.fileName()} 

747 return self._make_response(result) 

748 raise ServerError(f"{tag}: commander at index {index} does not exist") 

749 #@+node:felix.20210621233316.16: *5* server.close_file 

750 def close_file(self, param): 

751 """ 

752 Closes an outline opened with open_file. 

753 Use a 'forced' flag to force close. 

754 Returns a 'total' member in the package if close is successful. 

755 """ 

756 c = self._check_c() 

757 forced = param.get("forced") 

758 if c: 

759 # First, revert to prevent asking user. 

760 if forced and c.changed: 

761 if c.fileName(): 

762 c.revert() 

763 else: 

764 c.changed = False # Needed in g.app.closeLeoWindow 

765 # Then, if still possible, close it. 

766 if forced or not c.changed: 

767 # c.close() # Stops too much if last file closed 

768 g.app.closeLeoWindow(c.frame, finish_quit=False) 

769 else: 

770 # Cannot close, return empty response without 'total' (ask to save, ignore or cancel) 

771 return self._make_response() 

772 # New 'c': Select the first open outline, if any. 

773 commanders = g.app.commanders() 

774 self.c = commanders and commanders[0] or None 

775 if self.c: 

776 result = {"total": len(g.app.commanders()), "filename": self.c.fileName()} 

777 else: 

778 result = {"total": 0} 

779 return self._make_response(result) 

780 #@+node:felix.20210621233316.17: *5* server.save_file 

781 def save_file(self, param): # pragma: no cover (too dangerous). 

782 """Save the leo outline.""" 

783 tag = 'save_file' 

784 c = self._check_c() 

785 if c: 

786 try: 

787 if "name" in param: 

788 c.save(fileName=param['name']) 

789 else: 

790 c.save() 

791 except Exception as e: 

792 print(f"{tag} Error while saving {param['name']}", flush=True) 

793 print(e, flush=True) 

794 

795 return self._make_response() # Just send empty as 'ok' 

796 #@+node:felix.20210621233316.18: *5* server.import_any_file 

797 def import_any_file(self, param): 

798 """ 

799 Import file(s) from array of file names 

800 """ 

801 tag = 'import_any_file' 

802 c = self._check_c() 

803 ic = c.importCommands 

804 names = param.get('filenames') 

805 if names: 

806 g.chdir(names[0]) 

807 if not names: 

808 raise ServerError(f"{tag}: No file names provided") 

809 # New in Leo 4.9: choose the type of import based on the extension. 

810 derived = [z for z in names if c.looksLikeDerivedFile(z)] 

811 others = [z for z in names if z not in derived] 

812 if derived: 

813 ic.importDerivedFiles(parent=c.p, paths=derived) 

814 for fn in others: 

815 junk, ext = g.os_path_splitext(fn) 

816 ext = ext.lower() # #1522 

817 if ext.startswith('.'): 

818 ext = ext[1:] 

819 if ext == 'csv': 

820 ic.importMindMap([fn]) 

821 elif ext in ('cw', 'cweb'): 

822 ic.importWebCommand([fn], "cweb") 

823 # Not useful. Use @auto x.json instead. 

824 # elif ext == 'json': 

825 # ic.importJSON([fn]) 

826 elif fn.endswith('mm.html'): 

827 ic.importFreeMind([fn]) 

828 elif ext in ('nw', 'noweb'): 

829 ic.importWebCommand([fn], "noweb") 

830 elif ext == 'more': 

831 # (Félix) leoImport Should be on c? 

832 c.leoImport.MORE_Importer(c).import_file(fn) # #1522. 

833 elif ext == 'txt': 

834 # (Félix) import_txt_file Should be on c? 

835 # #1522: Create an @edit node. 

836 c.import_txt_file(c, fn) 

837 else: 

838 # Make *sure* that parent.b is empty. 

839 last = c.lastTopLevel() 

840 parent = last.insertAfter() 

841 parent.v.h = 'Imported Files' 

842 ic.importFilesCommand( 

843 files=[fn], 

844 parent=parent, 

845 treeType='@auto', # was '@clean' 

846 # Experimental: attempt to use permissive section ref logic. 

847 ) 

848 return self._make_response() # Just send empty as 'ok' 

849 #@+node:felix.20210621233316.19: *4* server.search commands 

850 #@+node:felix.20210621233316.20: *5* server.get_search_settings 

851 def get_search_settings(self, param): 

852 """ 

853 Gets search options 

854 """ 

855 tag = 'get_search_settings' 

856 c = self._check_c() 

857 try: 

858 settings = c.findCommands.ftm.get_settings() 

859 # Use the "__dict__" of the settings, to be serializable as a json string. 

860 result = {"searchSettings": settings.__dict__} 

861 except Exception as e: 

862 raise ServerError(f"{tag}: exception getting search settings: {e}") 

863 return self._make_response(result) 

864 #@+node:felix.20210621233316.21: *5* server.set_search_settings 

865 def set_search_settings(self, param): 

866 """ 

867 Sets search options. Init widgets and ivars from param.searchSettings 

868 """ 

869 tag = 'set_search_settings' 

870 c = self._check_c() 

871 find = c.findCommands 

872 ftm = c.findCommands.ftm 

873 searchSettings = param.get('searchSettings') 

874 if not searchSettings: 

875 raise ServerError(f"{tag}: searchSettings object is missing") 

876 # Try to set the search settings 

877 try: 

878 # Find/change text boxes. 

879 table = ( 

880 ('find_findbox', 'find_text', ''), 

881 ('find_replacebox', 'change_text', ''), 

882 ) 

883 for widget_ivar, setting_name, default in table: 

884 w = getattr(ftm, widget_ivar) 

885 s = searchSettings.get(setting_name) or default 

886 w.clear() 

887 w.insert(s) 

888 # Check boxes. 

889 table2 = ( 

890 ('ignore_case', 'check_box_ignore_case'), 

891 ('mark_changes', 'check_box_mark_changes'), 

892 ('mark_finds', 'check_box_mark_finds'), 

893 ('pattern_match', 'check_box_regexp'), 

894 ('search_body', 'check_box_search_body'), 

895 ('search_headline', 'check_box_search_headline'), 

896 ('whole_word', 'check_box_whole_word'), 

897 ) 

898 for setting_name, widget_ivar in table2: 

899 w = getattr(ftm, widget_ivar) 

900 val = searchSettings.get(setting_name) 

901 setattr(find, setting_name, val) 

902 if val != w.isChecked(): 

903 w.toggle() 

904 # Radio buttons 

905 table3 = ( 

906 ('node_only', 'node_only', 'radio_button_node_only'), 

907 ('entire_outline', None, 'radio_button_entire_outline'), 

908 ('suboutline_only', 'suboutline_only', 'radio_button_suboutline_only'), 

909 ) 

910 for setting_name, ivar, widget_ivar in table3: 

911 w = getattr(ftm, widget_ivar) 

912 val = searchSettings.get(setting_name, False) 

913 if ivar is not None: 

914 assert hasattr(find, setting_name), setting_name 

915 setattr(find, setting_name, val) 

916 if val != w.isChecked(): 

917 w.toggle() 

918 # Ensure one radio button is set. 

919 w = ftm.radio_button_entire_outline 

920 if not searchSettings.get('node_only', False) and not searchSettings.get('suboutline_only', False): 

921 setattr(find, 'entire_outline', True) 

922 if not w.isChecked(): 

923 w.toggle() 

924 else: 

925 setattr(find, 'entire_outline', False) 

926 if w.isChecked(): 

927 w.toggle() 

928 except Exception as e: 

929 raise ServerError(f"{tag}: exception setting search settings: {e}") 

930 # Confirm by sending back the settings to the client 

931 try: 

932 settings = ftm.get_settings() 

933 # Use the "__dict__" of the settings, to be serializable as a json string. 

934 result = {"searchSettings": settings.__dict__} 

935 except Exception as e: 

936 raise ServerError(f"{tag}: exception getting search settings: {e}") 

937 return self._make_response(result) 

938 #@+node:felix.20210621233316.22: *5* server.find_all 

939 def find_all(self, param): 

940 """Run Leo's find all command and return results.""" 

941 tag = 'find_all' 

942 c = self._check_c() 

943 fc = c.findCommands 

944 try: 

945 settings = fc.ftm.get_settings() 

946 result = fc.do_find_all(settings) 

947 except Exception as e: 

948 raise ServerError(f"{tag}: exception running 'find all': {e}") 

949 focus = self._get_focus() 

950 return self._make_response({"found": result, "focus": focus}) 

951 #@+node:felix.20210621233316.23: *5* server.find_next 

952 def find_next(self, param): 

953 """Run Leo's find-next command and return results.""" 

954 tag = 'find_next' 

955 c = self._check_c() 

956 p = c.p 

957 fc = c.findCommands 

958 fromOutline = param.get("fromOutline") 

959 fromBody = not fromOutline 

960 # 

961 focus = self._get_focus() 

962 inOutline = ("tree" in focus) or ("head" in focus) 

963 inBody = not inOutline 

964 # 

965 if fromOutline and inBody: 

966 fc.in_headline = True 

967 elif fromBody and inOutline: 

968 fc.in_headline = False 

969 # w = c.frame.body.wrapper 

970 c.bodyWantsFocus() 

971 c.bodyWantsFocusNow() 

972 # 

973 if fc.in_headline: 

974 ins = len(p.h) 

975 gui_w = c.edit_widget(p) 

976 gui_w.setSelectionRange(ins, ins, insert=ins) 

977 # 

978 try: 

979 # Let cursor as-is 

980 settings = fc.ftm.get_settings() 

981 p, pos, newpos = fc.do_find_next(settings) 

982 except Exception as e: 

983 raise ServerError(f"{tag}: Running find operation gave exception: {e}") 

984 # 

985 # get focus again after the operation 

986 focus = self._get_focus() 

987 result = {"found": bool(p), "pos": pos, 

988 "newpos": newpos, "focus": focus} 

989 return self._make_response(result) 

990 #@+node:felix.20210621233316.24: *5* server.find_previous 

991 def find_previous(self, param): 

992 """Run Leo's find-previous command and return results.""" 

993 tag = 'find_previous' 

994 c = self._check_c() 

995 p = c.p 

996 fc = c.findCommands 

997 fromOutline = param.get("fromOutline") 

998 fromBody = not fromOutline 

999 # 

1000 focus = self._get_focus() 

1001 inOutline = ("tree" in focus) or ("head" in focus) 

1002 inBody = not inOutline 

1003 # 

1004 if fromOutline and inBody: 

1005 fc.in_headline = True 

1006 elif fromBody and inOutline: 

1007 fc.in_headline = False 

1008 # w = c.frame.body.wrapper 

1009 c.bodyWantsFocus() 

1010 c.bodyWantsFocusNow() 

1011 # 

1012 if fc.in_headline: 

1013 gui_w = c.edit_widget(p) 

1014 gui_w.setSelectionRange(0, 0, insert=0) 

1015 # 

1016 try: 

1017 # set widget cursor pos to 0 if in headline 

1018 settings = fc.ftm.get_settings() 

1019 p, pos, newpos = fc.do_find_prev(settings) 

1020 except Exception as e: 

1021 raise ServerError(f"{tag}: Running find operation gave exception: {e}") 

1022 # 

1023 # get focus again after the operation 

1024 focus = self._get_focus() 

1025 result = {"found": bool(p), "pos": pos, 

1026 "newpos": newpos, "focus": focus} 

1027 return self._make_response(result) 

1028 #@+node:felix.20210621233316.25: *5* server.replace 

1029 def replace(self, param): 

1030 """Run Leo's replace command and return results.""" 

1031 tag = 'replace' 

1032 c = self._check_c() 

1033 fc = c.findCommands 

1034 try: 

1035 settings = fc.ftm.get_settings() 

1036 fc.change(settings) 

1037 except Exception as e: 

1038 raise ServerError(f"{tag}: Running change operation gave exception: {e}") 

1039 focus = self._get_focus() 

1040 result = {"found": True, "focus": focus} 

1041 return self._make_response(result) 

1042 #@+node:felix.20210621233316.26: *5* server.replace_then_find 

1043 def replace_then_find(self, param): 

1044 """Run Leo's replace then find next command and return results.""" 

1045 tag = 'replace_then_find' 

1046 c = self._check_c() 

1047 fc = c.findCommands 

1048 try: 

1049 settings = fc.ftm.get_settings() 

1050 result = fc.do_change_then_find(settings) 

1051 except Exception as e: 

1052 raise ServerError(f"{tag}: Running change operation gave exception: {e}") 

1053 focus = self._get_focus() 

1054 return self._make_response({"found": result, "focus": focus}) 

1055 #@+node:felix.20210621233316.27: *5* server.replace_all 

1056 def replace_all(self, param): 

1057 """Run Leo's replace all command and return results.""" 

1058 tag = 'replace_all' 

1059 c = self._check_c() 

1060 fc = c.findCommands 

1061 try: 

1062 settings = fc.ftm.get_settings() 

1063 result = fc.do_change_all(settings) 

1064 except Exception as e: 

1065 raise ServerError(f"{tag}: Running change operation gave exception: {e}") 

1066 focus = self._get_focus() 

1067 return self._make_response({"found": result, "focus": focus}) 

1068 #@+node:felix.20210621233316.28: *5* server.clone_find_all 

1069 def clone_find_all(self, param): 

1070 """Run Leo's clone-find-all command and return results.""" 

1071 tag = 'clone_find_all' 

1072 c = self._check_c() 

1073 fc = c.findCommands 

1074 try: 

1075 settings = fc.ftm.get_settings() 

1076 result = fc.do_clone_find_all(settings) 

1077 except Exception as e: 

1078 raise ServerError(f"{tag}: Running clone find operation gave exception: {e}") 

1079 focus = self._get_focus() 

1080 return self._make_response({"found": result, "focus": focus}) 

1081 #@+node:felix.20210621233316.29: *5* server.clone_find_all_flattened 

1082 def clone_find_all_flattened(self, param): 

1083 """Run Leo's clone-find-all-flattened command and return results.""" 

1084 tag = 'clone_find_all_flattened' 

1085 c = self._check_c() 

1086 fc = c.findCommands 

1087 try: 

1088 settings = fc.ftm.get_settings() 

1089 result = fc.do_clone_find_all_flattened(settings) 

1090 except Exception as e: 

1091 raise ServerError(f"{tag}: Running clone find operation gave exception: {e}") 

1092 focus = self._get_focus() 

1093 return self._make_response({"found": result, "focus": focus}) 

1094 #@+node:felix.20210621233316.30: *5* server.find_var 

1095 def find_var(self, param): 

1096 """Run Leo's find-var command and return results.""" 

1097 tag = 'find_var' 

1098 c = self._check_c() 

1099 fc = c.findCommands 

1100 try: 

1101 fc.find_var() 

1102 except Exception as e: 

1103 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}") 

1104 focus = self._get_focus() 

1105 return self._make_response({"found": True, "focus": focus}) 

1106 #@+node:felix.20210722010004.1: *5* server.clone_find_all_flattened_marked 

1107 def clone_find_all_flattened_marked(self, param): 

1108 """Run Leo's clone-find-all-flattened-marked command.""" 

1109 tag = 'clone_find_all_flattened_marked' 

1110 c = self._check_c() 

1111 fc = c.findCommands 

1112 try: 

1113 fc.do_find_marked(flatten=True) 

1114 except Exception as e: 

1115 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}") 

1116 focus = self._get_focus() 

1117 return self._make_response({"found": True, "focus": focus}) 

1118 #@+node:felix.20210722010005.1: *5* server.clone_find_all_marked 

1119 def clone_find_all_marked(self, param): 

1120 """Run Leo's clone-find-all-marked command """ 

1121 tag = 'clone_find_all_marked' 

1122 c = self._check_c() 

1123 fc = c.findCommands 

1124 try: 

1125 fc.do_find_marked(flatten=False) 

1126 except Exception as e: 

1127 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}") 

1128 focus = self._get_focus() 

1129 return self._make_response({"found": True, "focus": focus}) 

1130 #@+node:felix.20210621233316.31: *5* server.find_def 

1131 def find_def(self, param): 

1132 """Run Leo's find-def command and return results.""" 

1133 tag = 'find_def' 

1134 c = self._check_c() 

1135 fc = c.findCommands 

1136 try: 

1137 fc.find_def() 

1138 except Exception as e: 

1139 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}") 

1140 focus = self._get_focus() 

1141 return self._make_response({"found": True, "focus": focus}) 

1142 #@+node:felix.20210621233316.32: *5* server.goto_global_line 

1143 def goto_global_line(self, param): 

1144 """Run Leo's goto-global-line command and return results.""" 

1145 tag = 'goto_global_line' 

1146 c = self._check_c() 

1147 gc = c.gotoCommands 

1148 line = param.get('line', 1) 

1149 try: 

1150 junk_p, junk_offset, found = gc.find_file_line(n=int(line)) 

1151 except Exception as e: 

1152 raise ServerError(f"{tag}: Running clone find operation gave exception: {e}") 

1153 focus = self._get_focus() 

1154 return self._make_response({"found": found, "focus": focus}) 

1155 #@+node:felix.20210621233316.33: *5* server.clone_find_tag 

1156 def clone_find_tag(self, param): 

1157 """Run Leo's clone-find-tag command and return results.""" 

1158 tag = 'clone_find_tag' 

1159 c = self._check_c() 

1160 fc = c.findCommands 

1161 tag_param = param.get("tag") 

1162 if not tag_param: # pragma: no cover 

1163 raise ServerError(f"{tag}: no tag") 

1164 settings = fc.ftm.get_settings() 

1165 if self.log_flag: # pragma: no cover 

1166 g.printObj(settings, tag=f"{tag}: settings for {c.shortFileName()}") 

1167 n, p = fc.do_clone_find_tag(tag_param) 

1168 if self.log_flag: # pragma: no cover 

1169 g.trace("tag: {tag_param} n: {n} p: {p and p.h!r}") 

1170 print('', flush=True) 

1171 return self._make_response({"n": n}) 

1172 #@+node:felix.20210621233316.34: *5* server.tag_children 

1173 def tag_children(self, param): 

1174 """Run Leo's tag-children command""" 

1175 # This is not a find command! 

1176 tag = 'tag_children' 

1177 c = self._check_c() 

1178 fc = c.findCommands 

1179 tag_param = param.get("tag") 

1180 if tag_param is None: # pragma: no cover 

1181 raise ServerError(f"{tag}: no tag") 

1182 # Unlike find commands, do_tag_children does not use a settings dict. 

1183 fc.do_tag_children(c.p, tag_param) 

1184 return self._make_response() 

1185 #@+node:felix.20210621233316.35: *4* server:getter commands 

1186 #@+node:felix.20210621233316.36: *5* server.get_all_open_commanders 

1187 def get_all_open_commanders(self, param): 

1188 """Return array describing each commander in g.app.commanders().""" 

1189 files = [ 

1190 { 

1191 "changed": c.isChanged(), 

1192 "name": c.fileName(), 

1193 "selected": c == self.c, 

1194 } for c in g.app.commanders() 

1195 ] 

1196 return self._make_minimal_response({"files": files}) 

1197 #@+node:felix.20210621233316.37: *5* server.get_all_positions 

1198 def get_all_positions(self, param): 

1199 """ 

1200 Return a list of position data for all positions. 

1201 

1202 Useful as a sanity check for debugging. 

1203 """ 

1204 c = self._check_c() 

1205 result = [ 

1206 self._get_position_d(p) for p in c.all_positions(copy=False) 

1207 ] 

1208 return self._make_minimal_response({"position-data-list": result}) 

1209 #@+node:felix.20210621233316.38: *5* server.get_all_gnx 

1210 def get_all_gnx(self, param): 

1211 """Get gnx array from all unique nodes""" 

1212 if self.log_flag: # pragma: no cover 

1213 print('\nget_all_gnx\n', flush=True) 

1214 c = self._check_c() 

1215 all_gnx = [p.v.gnx for p in c.all_unique_positions(copy=False)] 

1216 return self._make_minimal_response({"gnx": all_gnx}) 

1217 #@+node:felix.20210621233316.39: *5* server.get_body 

1218 def get_body(self, param): 

1219 """ 

1220 Return the body content body specified via GNX. 

1221 """ 

1222 c = self._check_c() 

1223 gnx = param.get("gnx") 

1224 v = c.fileCommands.gnxDict.get(gnx) # vitalije 

1225 body = "" 

1226 if v: 

1227 body = v.b or "" 

1228 # Support asking for unknown gnx when client switches rapidly 

1229 return self._make_minimal_response({"body": body}) 

1230 #@+node:felix.20210621233316.40: *5* server.get_body_length 

1231 def get_body_length(self, param): 

1232 """ 

1233 Return p.b's length in bytes, where p is c.p if param["ap"] is missing. 

1234 """ 

1235 c = self._check_c() 

1236 gnx = param.get("gnx") 

1237 w_v = c.fileCommands.gnxDict.get(gnx) # vitalije 

1238 if w_v: 

1239 # Length in bytes, not just by character count. 

1240 return self._make_minimal_response({"len": len(w_v.b.encode('utf-8'))}) 

1241 return self._make_minimal_response({"len": 0}) # empty as default 

1242 #@+node:felix.20210621233316.41: *5* server.get_body_states 

1243 def get_body_states(self, param): 

1244 """ 

1245 Return body data for p, where p is c.p if param["ap"] is missing. 

1246 The cursor positions are given as {"line": line, "col": col, "index": i} 

1247 with line and col along with a redundant index for convenience and flexibility. 

1248 """ 

1249 c = self._check_c() 

1250 p = self._get_p(param) 

1251 wrapper = c.frame.body.wrapper 

1252 

1253 def row_col_wrapper_dict(i): 

1254 if not i: 

1255 i = 0 # prevent none type 

1256 # BUG: this uses current selection wrapper only, use 

1257 # g.convertPythonIndexToRowCol instead ! 

1258 junk, line, col = wrapper.toPythonIndexRowCol(i) 

1259 return {"line": line, "col": col, "index": i} 

1260 

1261 def row_col_pv_dict(i, s): 

1262 if not i: 

1263 i = 0 # prevent none type 

1264 # BUG: this uses current selection wrapper only, use 

1265 # g.convertPythonIndexToRowCol instead ! 

1266 line, col = g.convertPythonIndexToRowCol(s, i) 

1267 return {"line": line, "col": col, "index": i} 

1268 

1269 # Get the language. 

1270 aList = g.get_directives_dict_list(p) 

1271 d = g.scanAtCommentAndAtLanguageDirectives(aList) 

1272 language = ( 

1273 d and d.get('language') 

1274 or g.getLanguageFromAncestorAtFileNode(p) 

1275 or c.config.getLanguage('target-language') 

1276 or 'plain' 

1277 ) 

1278 # Get the body wrap state 

1279 wrap = g.scanAllAtWrapDirectives(c, p) 

1280 tabWidth = g.scanAllAtTabWidthDirectives(c, p) 

1281 if not isinstance(tabWidth, int): 

1282 tabWidth = False 

1283 # get values from wrapper if it's the selected node. 

1284 if c.p.v.gnx == p.v.gnx: 

1285 insert = wrapper.getInsertPoint() 

1286 start, end = wrapper.getSelectionRange(True) 

1287 scroll = wrapper.getYScrollPosition() 

1288 states = { 

1289 'language': language.lower(), 

1290 'wrap': wrap, 

1291 'tabWidth': tabWidth, 

1292 'selection': { 

1293 "gnx": p.v.gnx, 

1294 "scroll": scroll, 

1295 "insert": row_col_wrapper_dict(insert), 

1296 "start": row_col_wrapper_dict(start), 

1297 "end": row_col_wrapper_dict(end) 

1298 } 

1299 } 

1300 else: # pragma: no cover 

1301 insert = p.v.insertSpot 

1302 start = p.v.selectionStart 

1303 end = p.v.selectionStart + p.v.selectionLength 

1304 scroll = p.v.scrollBarSpot 

1305 states = { 

1306 'language': language.lower(), 

1307 'wrap': wrap, 

1308 'tabWidth': tabWidth, 

1309 'selection': { 

1310 "gnx": p.v.gnx, 

1311 "scroll": scroll, 

1312 "insert": row_col_pv_dict(insert, p.v.b), 

1313 "start": row_col_pv_dict(start, p.v.b), 

1314 "end": row_col_pv_dict(end, p.v.b) 

1315 } 

1316 } 

1317 return self._make_minimal_response(states) 

1318 #@+node:felix.20210621233316.42: *5* server.get_children 

1319 def get_children(self, param): 

1320 """ 

1321 Return the node data for children of p, 

1322 where p is root if param.ap is missing 

1323 """ 

1324 c = self._check_c() 

1325 children = [] # default empty array 

1326 if param.get("ap"): 

1327 # Maybe empty param, for tree-root children(s). 

1328 # _get_p called with the strict=True parameter because 

1329 # we don't want c.p. after switch to another document while refreshing. 

1330 p = self._get_p(param, True) 

1331 if p and p.hasChildren(): 

1332 children = [self._get_position_d(child) for child in p.children()] 

1333 else: 

1334 if c.hoistStack: 

1335 # Always start hoisted tree with single hoisted root node 

1336 children = [self._get_position_d(c.hoistStack[-1].p)] 

1337 else: 

1338 # this outputs all Root Children 

1339 children = [self._get_position_d(child) for child in self._yieldAllRootChildren()] 

1340 return self._make_minimal_response({"children": children}) 

1341 #@+node:felix.20210621233316.43: *5* server.get_focus 

1342 def get_focus(self, param): 

1343 """ 

1344 Return a representation of the focused widget, 

1345 one of ("body", "tree", "headline", repr(the_widget)). 

1346 """ 

1347 return self._make_minimal_response({"focus": self._get_focus()}) 

1348 #@+node:felix.20210621233316.44: *5* server.get_parent 

1349 def get_parent(self, param): 

1350 """Return the node data for the parent of position p, where p is c.p if param["ap"] is missing.""" 

1351 self._check_c() 

1352 p = self._get_p(param) 

1353 parent = p.parent() 

1354 data = self._get_position_d(parent) if parent else None 

1355 return self._make_minimal_response({"node": data}) 

1356 #@+node:felix.20210621233316.45: *5* server.get_position_data 

1357 def get_position_data(self, param): 

1358 """ 

1359 Return a dict of position data for all positions. 

1360 

1361 Useful as a sanity check for debugging. 

1362 """ 

1363 c = self._check_c() 

1364 result = { 

1365 p.v.gnx: self._get_position_d(p) 

1366 for p in c.all_unique_positions(copy=False) 

1367 } 

1368 return self._make_minimal_response({"position-data-dict": result}) 

1369 #@+node:felix.20210621233316.46: *5* server.get_ua 

1370 def get_ua(self, param): 

1371 """Return p.v.u, making sure it can be serialized.""" 

1372 self._check_c() 

1373 p = self._get_p(param) 

1374 try: 

1375 ua = {"ua": p.v.u} 

1376 json.dumps(ua, separators=(',', ':'), cls=SetEncoder) 

1377 response = {"ua": p.v.u} 

1378 except Exception: # pragma: no cover 

1379 response = {"ua": repr(p.v.u)} 

1380 # _make_response adds all the cheap redraw data. 

1381 return self._make_response(response) 

1382 #@+node:felix.20210621233316.48: *5* server.get_ui_states 

1383 def get_ui_states(self, param): 

1384 """ 

1385 Return the enabled/disabled UI states for the open commander, or defaults if None. 

1386 """ 

1387 c = self._check_c() 

1388 tag = 'get_ui_states' 

1389 try: 

1390 states = { 

1391 "changed": c and c.changed, 

1392 "canUndo": c and c.canUndo(), 

1393 "canRedo": c and c.canRedo(), 

1394 "canDemote": c and c.canDemote(), 

1395 "canPromote": c and c.canPromote(), 

1396 "canDehoist": c and c.canDehoist(), 

1397 } 

1398 except Exception as e: # pragma: no cover 

1399 raise ServerError(f"{tag}: Exception setting state: {e}") 

1400 return self._make_minimal_response({"states": states}) 

1401 #@+node:felix.20211210213603.1: *5* server.get_undos 

1402 def get_undos(self, param): 

1403 """Return list of undo operations""" 

1404 c = self._check_c() 

1405 undoer = c.undoer 

1406 undos = [] 

1407 try: 

1408 for bead in undoer.beads: 

1409 undos.append(bead.undoType) 

1410 response = {"bead": undoer.bead, "undos": undos} 

1411 except Exception: # pragma: no cover 

1412 response = {"bead": 0, "undos": []} 

1413 # _make_response adds all the cheap redraw data. 

1414 return self._make_minimal_response(response) 

1415 #@+node:felix.20210621233316.49: *4* server:node commands 

1416 #@+node:felix.20210621233316.50: *5* server.clone_node 

1417 def clone_node(self, param): 

1418 """ 

1419 Clone a node. 

1420 Try to keep selection, then return the selected node that remains. 

1421 """ 

1422 c = self._check_c() 

1423 p = self._get_p(param) 

1424 if p == c.p: 

1425 c.clone() 

1426 else: 

1427 oldPosition = c.p 

1428 c.selectPosition(p) 

1429 c.clone() 

1430 if c.positionExists(oldPosition): 

1431 c.selectPosition(oldPosition) 

1432 # return selected node either ways 

1433 return self._make_response() 

1434 

1435 #@+node:felix.20210621233316.51: *5* server.contract_node 

1436 def contract_node(self, param): 

1437 """ 

1438 Contract (Collapse) the node at position p, where p is c.p if p is missing. 

1439 """ 

1440 p = self._get_p(param) 

1441 p.contract() 

1442 return self._make_response() 

1443 #@+node:felix.20210621233316.52: *5* server.copy_node 

1444 def copy_node(self, param): # pragma: no cover (too dangerous, for now) 

1445 """ 

1446 Copy a node, don't select it. 

1447 Try to keep selection, then return the selected node. 

1448 """ 

1449 c = self._check_c() 

1450 p = self._get_p(param) 

1451 if p == c.p: 

1452 s = c.fileCommands.outline_to_clipboard_string() 

1453 else: 

1454 oldPosition = c.p # not same node, save position to possibly return to 

1455 c.selectPosition(p) 

1456 s = c.fileCommands.outline_to_clipboard_string() 

1457 if c.positionExists(oldPosition): 

1458 # select if old position still valid 

1459 c.selectPosition(oldPosition) 

1460 return self._make_response({"string": s}) 

1461 

1462 #@+node:felix.20220222172507.1: *5* server.cut_node 

1463 def cut_node(self, param): # pragma: no cover (too dangerous, for now) 

1464 """ 

1465 Cut a node, don't select it. 

1466 Try to keep selection, then return the selected node that remains. 

1467 """ 

1468 c = self._check_c() 

1469 p = self._get_p(param) 

1470 if p == c.p: 

1471 s = c.fileCommands.outline_to_clipboard_string() 

1472 c.cutOutline() # already on this node, so cut it 

1473 else: 

1474 oldPosition = c.p # not same node, save position to possibly return to 

1475 c.selectPosition(p) 

1476 s = c.fileCommands.outline_to_clipboard_string() 

1477 c.cutOutline() 

1478 if c.positionExists(oldPosition): 

1479 # select if old position still valid 

1480 c.selectPosition(oldPosition) 

1481 else: 

1482 oldPosition._childIndex = oldPosition._childIndex-1 

1483 # Try again with childIndex decremented 

1484 if c.positionExists(oldPosition): 

1485 # additional try with lowered childIndex 

1486 c.selectPosition(oldPosition) 

1487 return self._make_response({"string": s}) 

1488 #@+node:felix.20210621233316.53: *5* server.delete_node 

1489 def delete_node(self, param): # pragma: no cover (too dangerous, for now) 

1490 """ 

1491 Delete a node, don't select it. 

1492 Try to keep selection, then return the selected node that remains. 

1493 """ 

1494 c = self._check_c() 

1495 p = self._get_p(param) 

1496 if p == c.p: 

1497 c.deleteOutline() # already on this node, so cut it 

1498 else: 

1499 oldPosition = c.p # not same node, save position to possibly return to 

1500 c.selectPosition(p) 

1501 c.deleteOutline() 

1502 if c.positionExists(oldPosition): 

1503 # select if old position still valid 

1504 c.selectPosition(oldPosition) 

1505 else: 

1506 oldPosition._childIndex = oldPosition._childIndex-1 

1507 # Try again with childIndex decremented 

1508 if c.positionExists(oldPosition): 

1509 # additional try with lowered childIndex 

1510 c.selectPosition(oldPosition) 

1511 return self._make_response() 

1512 #@+node:felix.20210621233316.54: *5* server.expand_node 

1513 def expand_node(self, param): 

1514 """ 

1515 Expand the node at position p, where p is c.p if p is missing. 

1516 """ 

1517 p = self._get_p(param) 

1518 p.expand() 

1519 return self._make_response() 

1520 #@+node:felix.20210621233316.55: *5* server.insert_node 

1521 def insert_node(self, param): 

1522 """ 

1523 Insert a node at given node, then select it once created, and finally return it 

1524 """ 

1525 c = self._check_c() 

1526 p = self._get_p(param) 

1527 c.selectPosition(p) 

1528 c.insertHeadline() # Handles undo, sets c.p 

1529 return self._make_response() 

1530 #@+node:felix.20210703021435.1: *5* server.insert_child_node 

1531 def insert_child_node(self, param): 

1532 """ 

1533 Insert a child node at given node, then select it once created, and finally return it 

1534 """ 

1535 c = self._check_c() 

1536 p = self._get_p(param) 

1537 c.selectPosition(p) 

1538 c.insertHeadline(op_name='Insert Child', as_child=True) 

1539 return self._make_response() 

1540 #@+node:felix.20210621233316.56: *5* server.insert_named_node 

1541 def insert_named_node(self, param): 

1542 """ 

1543 Insert a node at given node, set its headline, select it and finally return it 

1544 """ 

1545 c = self._check_c() 

1546 p = self._get_p(param) 

1547 newHeadline = param.get('name') 

1548 bunch = c.undoer.beforeInsertNode(p) 

1549 newNode = p.insertAfter() 

1550 # set this node's new headline 

1551 newNode.h = newHeadline 

1552 newNode.setDirty() 

1553 c.undoer.afterInsertNode( 

1554 newNode, 'Insert Node', bunch) 

1555 c.selectPosition(newNode) 

1556 c.setChanged() 

1557 return self._make_response() 

1558 #@+node:felix.20210703021441.1: *5* server.insert_child_named_node 

1559 def insert_child_named_node(self, param): 

1560 """ 

1561 Insert a child node at given node, set its headline, select it and finally return it 

1562 """ 

1563 c = self._check_c() 

1564 p = self._get_p(param) 

1565 newHeadline = param.get('name') 

1566 bunch = c.undoer.beforeInsertNode(p) 

1567 if c.config.getBool('insert-new-nodes-at-end'): 

1568 newNode = p.insertAsLastChild() 

1569 else: 

1570 newNode = p.insertAsNthChild(0) 

1571 # set this node's new headline 

1572 newNode.h = newHeadline 

1573 newNode.setDirty() 

1574 c.undoer.afterInsertNode( 

1575 newNode, 'Insert Node', bunch) 

1576 c.selectPosition(newNode) 

1577 return self._make_response() 

1578 #@+node:felix.20210621233316.57: *5* server.page_down 

1579 def page_down(self, param): 

1580 """ 

1581 Selects a node "n" steps down in the tree to simulate page down. 

1582 """ 

1583 c = self._check_c() 

1584 n = param.get("n", 3) 

1585 for z in range(n): 

1586 c.selectVisNext() 

1587 return self._make_response() 

1588 #@+node:felix.20210621233316.58: *5* server.page_up 

1589 def page_up(self, param): 

1590 """ 

1591 Selects a node "N" steps up in the tree to simulate page up. 

1592 """ 

1593 c = self._check_c() 

1594 n = param.get("n", 3) 

1595 for z in range(n): 

1596 c.selectVisBack() 

1597 return self._make_response() 

1598 #@+node:felix.20220222173659.1: *5* server.paste_node 

1599 def paste_node(self, param): 

1600 """ 

1601 Pastes a node, 

1602 Try to keep selection, then return the selected node. 

1603 """ 

1604 tag = 'paste_node' 

1605 c = self._check_c() 

1606 p = self._get_p(param) 

1607 s = param.get('name') 

1608 if s is None: # pragma: no cover 

1609 raise ServerError(f"{tag}: no string given") 

1610 if p == c.p: 

1611 c.pasteOutline(s=s) 

1612 else: 

1613 oldPosition = c.p # not same node, save position to possibly return to 

1614 c.selectPosition(p) 

1615 c.pasteOutline(s=s) 

1616 if c.positionExists(oldPosition): 

1617 # select if old position still valid 

1618 c.selectPosition(oldPosition) 

1619 else: 

1620 oldPosition._childIndex = oldPosition._childIndex+1 

1621 # Try again with childIndex incremented 

1622 if c.positionExists(oldPosition): 

1623 # additional try with higher childIndex 

1624 c.selectPosition(oldPosition) 

1625 return self._make_response() 

1626 #@+node:felix.20220222173707.1: *5* paste_as_clone_node 

1627 def paste_as_clone_node(self, param): 

1628 """ 

1629 Pastes a node as a clone, 

1630 Try to keep selection, then return the selected node. 

1631 """ 

1632 tag = 'paste_as_clone_node' 

1633 c = self._check_c() 

1634 p = self._get_p(param) 

1635 s = param.get('name') 

1636 if s is None: # pragma: no cover 

1637 raise ServerError(f"{tag}: no string given") 

1638 if p == c.p: 

1639 c.pasteOutlineRetainingClones(s=s) 

1640 else: 

1641 oldPosition = c.p # not same node, save position to possibly return to 

1642 c.selectPosition(p) 

1643 c.pasteOutlineRetainingClones(s=s) 

1644 if c.positionExists(oldPosition): 

1645 # select if old position still valid 

1646 c.selectPosition(oldPosition) 

1647 else: 

1648 oldPosition._childIndex = oldPosition._childIndex+1 

1649 # Try again with childIndex incremented 

1650 if c.positionExists(oldPosition): 

1651 # additional try with higher childIndex 

1652 c.selectPosition(oldPosition) 

1653 return self._make_response() 

1654 #@+node:felix.20210621233316.59: *5* server.redo 

1655 def redo(self, param): 

1656 """Undo last un-doable operation""" 

1657 c = self._check_c() 

1658 u = c.undoer 

1659 if u.canRedo(): 

1660 u.redo() 

1661 return self._make_response() 

1662 #@+node:felix.20210621233316.60: *5* server.set_body 

1663 def set_body(self, param): 

1664 """ 

1665 Undoably set body text of a v node. 

1666 (Only if new string is different from actual existing body string) 

1667 """ 

1668 tag = 'set_body' 

1669 c = self._check_c() 

1670 gnx = param.get('gnx') 

1671 body = param.get('body') 

1672 u, wrapper = c.undoer, c.frame.body.wrapper 

1673 if body is None: # pragma: no cover 

1674 raise ServerError(f"{tag}: no body given") 

1675 for p in c.all_positions(): 

1676 if p.v.gnx == gnx: 

1677 if body==p.v.b: 

1678 return self._make_response() 

1679 # Just exit if there is no need to change at all. 

1680 bunch = u.beforeChangeNodeContents(p) 

1681 p.v.setBodyString(body) 

1682 u.afterChangeNodeContents(p, "Body Text", bunch) 

1683 if c.p == p: 

1684 wrapper.setAllText(body) 

1685 if not self.c.isChanged(): # pragma: no cover 

1686 c.setChanged() 

1687 if not p.v.isDirty(): # pragma: no cover 

1688 p.setDirty() 

1689 break 

1690 # additional forced string setting 

1691 if gnx: 

1692 v = c.fileCommands.gnxDict.get(gnx) # vitalije 

1693 if v: 

1694 v.b = body 

1695 return self._make_response() 

1696 #@+node:felix.20210621233316.61: *5* server.set_current_position 

1697 def set_current_position(self, param): 

1698 """Select position p. Or try to get p with gnx if not found.""" 

1699 tag = "set_current_position" 

1700 c = self._check_c() 

1701 p = self._get_p(param) 

1702 if p: 

1703 if c.positionExists(p): 

1704 # set this node as selection 

1705 c.selectPosition(p) 

1706 else: 

1707 ap = param.get('ap') 

1708 foundPNode = self._positionFromGnx(ap.get('gnx', "")) 

1709 if foundPNode: 

1710 c.selectPosition(foundPNode) 

1711 else: 

1712 print( 

1713 f"{tag}: node does not exist! " 

1714 f"ap was: {json.dumps(ap, cls=SetEncoder)}", flush=True) 

1715 

1716 return self._make_response() 

1717 #@+node:felix.20210621233316.62: *5* server.set_headline 

1718 def set_headline(self, param): 

1719 """ 

1720 Undoably set p.h, where p is c.p if package["ap"] is missing. 

1721 """ 

1722 c = self._check_c() 

1723 p = self._get_p(param) 

1724 u = c.undoer 

1725 h = param.get('name', '') 

1726 bunch = u.beforeChangeNodeContents(p) 

1727 p.h = h 

1728 u.afterChangeNodeContents(p, 'Change Headline', bunch) 

1729 return self._make_response() 

1730 #@+node:felix.20210621233316.63: *5* server.set_selection 

1731 def set_selection(self, param): 

1732 """ 

1733 Set the selection range for p.b, where p is c.p if package["ap"] is missing. 

1734 

1735 Set the selection in the wrapper if p == c.p 

1736 

1737 Package has these keys: 

1738 

1739 - "ap": An archived position for position p. 

1740 - "start": The start of the selection. 

1741 - "end": The end of the selection. 

1742 - "active": The insert point. Must be either start or end. 

1743 - "scroll": An optional scroll position. 

1744 

1745 Selection points can be sent as {"col":int, "line" int} dict 

1746 or as numbers directly for convenience. 

1747 """ 

1748 c = self._check_c() 

1749 p = self._get_p(param) # Will raise ServerError if p does not exist. 

1750 v = p.v 

1751 wrapper = c.frame.body.wrapper 

1752 convert = g.convertRowColToPythonIndex 

1753 start = param.get('start', 0) 

1754 end = param.get('end', 0) 

1755 active = param.get('insert', 0) # temp var to check if int. 

1756 scroll = param.get('scroll', 0) 

1757 # If sent as number, use 'as is' 

1758 if isinstance(active, int): 

1759 insert = active 

1760 startSel = start 

1761 endSel = end 

1762 else: 

1763 # otherwise convert from line+col data. 

1764 insert = convert( 

1765 v.b, active['line'], active['col']) 

1766 startSel = convert( 

1767 v.b, start['line'], start['col']) 

1768 endSel = convert( 

1769 v.b, end['line'], end['col']) 

1770 # If it's the currently selected node set the wrapper's states too 

1771 if p == c.p: 

1772 wrapper.setSelectionRange(startSel, endSel, insert) 

1773 wrapper.setYScrollPosition(scroll) 

1774 # Always set vnode attrs. 

1775 v.scrollBarSpot = scroll 

1776 v.insertSpot = insert 

1777 v.selectionStart = startSel 

1778 v.selectionLength = abs(startSel - endSel) 

1779 return self._make_response() 

1780 #@+node:felix.20211114202046.1: *5* server.set_ua_member 

1781 def set_ua_member(self, param): 

1782 """ 

1783 Set a single member of a node's ua. 

1784 """ 

1785 self._check_c() 

1786 p = self._get_p(param) 

1787 name = param.get('name') 

1788 value = param.get('value', '') 

1789 if not p.v.u: 

1790 p.v.u = {} # assert at least an empty dict if null or non existent 

1791 if name and isinstance(name, str): 

1792 p.v.u[name] = value 

1793 return self._make_response() 

1794 #@+node:felix.20211114202058.1: *5* server.set_ua 

1795 def set_ua(self, param): 

1796 """ 

1797 Replace / set the whole user attribute dict of a node. 

1798 """ 

1799 self._check_c() 

1800 p = self._get_p(param) 

1801 ua = param.get('ua', {}) 

1802 p.v.u = ua 

1803 return self._make_response() 

1804 #@+node:felix.20210621233316.64: *5* server.toggle_mark 

1805 def toggle_mark(self, param): 

1806 """ 

1807 Toggle the mark at position p. 

1808 Try to keep selection, then return the selected node that remains. 

1809 """ 

1810 c = self._check_c() 

1811 p = self._get_p(param) 

1812 if p == c.p: 

1813 c.markHeadline() 

1814 else: 

1815 oldPosition = c.p 

1816 c.selectPosition(p) 

1817 c.markHeadline() 

1818 if c.positionExists(oldPosition): 

1819 c.selectPosition(oldPosition) 

1820 # return selected node either ways 

1821 return self._make_response() 

1822 #@+node:felix.20210621233316.65: *5* server.mark_node 

1823 def mark_node(self, param): 

1824 """ 

1825 Mark a node. 

1826 Try to keep selection, then return the selected node that remains. 

1827 """ 

1828 # pylint: disable=no-else-return 

1829 self._check_c() 

1830 p = self._get_p(param) 

1831 if p.isMarked(): 

1832 return self._make_response() 

1833 else: 

1834 return self.toggle_mark(param) 

1835 

1836 #@+node:felix.20210621233316.66: *5* server.unmark_node 

1837 def unmark_node(self, param): 

1838 """ 

1839 Unmark a node. 

1840 Try to keep selection, then return the selected node that remains. 

1841 """ 

1842 # pylint: disable=no-else-return 

1843 self._check_c() 

1844 p = self._get_p(param) 

1845 if not p.isMarked(): 

1846 return self._make_response() 

1847 else: 

1848 return self.toggle_mark(param) 

1849 #@+node:felix.20210621233316.67: *5* server.undo 

1850 def undo(self, param): 

1851 """Undo last un-doable operation""" 

1852 c = self._check_c() 

1853 u = c.undoer 

1854 if u.canUndo(): 

1855 u.undo() 

1856 # Félix: Caller can get focus using other calls. 

1857 return self._make_response() 

1858 #@+node:felix.20210621233316.68: *4* server:server commands 

1859 #@+node:felix.20210914230846.1: *5* server.get_version 

1860 def get_version(self, param): 

1861 """ 

1862 Return this server program name and version as a string representation 

1863 along with the three version members as numbers 'major', 'minor' and 'patch'. 

1864 """ 

1865 # uses the __version__ global constant and the v1, v2, v3 global version numbers 

1866 result = {"version": __version__ , "major": v1, "minor": v2, "patch": v3} 

1867 return self._make_minimal_response(result) 

1868 #@+node:felix.20210818012827.1: *5* server.do_nothing 

1869 def do_nothing(self, param): 

1870 """Simply return states from _make_response""" 

1871 return self._make_response() 

1872 #@+node:felix.20210621233316.69: *5* server.set_ask_result 

1873 def set_ask_result(self, param): 

1874 """Got the result to an asked question/warning from client""" 

1875 tag = "set_ask_result" 

1876 result = param.get("result") 

1877 if not result: 

1878 raise ServerError(f"{tag}: no param result") 

1879 g.app.externalFilesController.clientResult(result) 

1880 return self._make_response() 

1881 #@+node:felix.20210621233316.70: *5* server.set_config 

1882 def set_config(self, param): 

1883 """Got auto-reload's config from client""" 

1884 self.leoServerConfig = param # PARAM IS THE CONFIG-DICT 

1885 return self._make_response() 

1886 #@+node:felix.20210621233316.71: *5* server.error 

1887 def error(self, param): 

1888 """For unit testing. Raise ServerError""" 

1889 raise ServerError("error called") 

1890 #@+node:felix.20210621233316.72: *5* server.get_all_leo_commands & helper 

1891 def get_all_leo_commands(self, param): 

1892 """Return a list of all commands that make sense for connected clients.""" 

1893 tag = 'get_all_leo_commands' 

1894 # #173: Use the present commander to get commands created by @button and @command. 

1895 c = self.c 

1896 d = c.commandsDict if c else {} # keys are command names, values are functions. 

1897 bad_names = self._bad_commands(c) # #92. 

1898 good_names = self._good_commands() 

1899 duplicates = set(bad_names).intersection(set(good_names)) 

1900 if duplicates: # pragma: no cover 

1901 print(f"{tag}: duplicate command names...", flush=True) 

1902 for z in sorted(duplicates): 

1903 print(z, flush=True) 

1904 result = [] 

1905 for command_name in sorted(d): 

1906 func = d.get(command_name) 

1907 if not func: # pragma: no cover 

1908 print(f"{tag}: no func: {command_name!r}", flush=True) 

1909 continue 

1910 if command_name in bad_names: # #92. 

1911 continue 

1912 doc = func.__doc__ or '' 

1913 result.append({ 

1914 "label": command_name, # Kebab-cased Command name to be called 

1915 "detail": doc, 

1916 }) 

1917 if self.log_flag: # pragma: no cover 

1918 print(f"\n{tag}: {len(result)} leo commands\n", flush=True) 

1919 g.printObj([z.get("label") for z in result], tag=tag) 

1920 print('', flush=True) 

1921 return self._make_minimal_response({"commands": result}) 

1922 #@+node:felix.20210621233316.73: *6* server._bad_commands 

1923 def _bad_commands(self, c): 

1924 """Return the list of command names that connected clients should ignore.""" 

1925 d = c.commandsDict if c else {} # keys are command names, values are functions. 

1926 bad = [] 

1927 # 

1928 # leoInteg #173: Remove only vim commands. 

1929 for command_name in sorted(d): 

1930 if command_name.startswith(':'): 

1931 bad.append(command_name) 

1932 # 

1933 # Remove other commands. 

1934 # This is a hand-curated list. 

1935 bad_list = [ 

1936 'demangle-recent-files', 

1937 'clean-main-spell-dict', 

1938 'clean-persistence', 

1939 'clean-recent-files', 

1940 'clean-spellpyx', 

1941 'clean-user-spell-dict', 

1942 'clear-recent-files', 

1943 'delete-first-icon', 

1944 'delete-last-icon', 

1945 'delete-node-icons', 

1946 'insert-icon', 

1947 'set-ua', # TODO : Should be easy to implement 

1948 'export-headlines', # export TODO 

1949 'export-jupyter-notebook', # export TODO 

1950 'outline-to-cweb', # export TODO 

1951 'outline-to-noweb', # export TODO 

1952 'remove-sentinels', # import TODO 

1953 

1954 'save-all', 

1955 'save-file-as-zipped', 

1956 'write-file-from-node', 

1957 'edit-setting', 

1958 'edit-shortcut', 

1959 'goto-line', 

1960 'pdb', 

1961 'xdb', 

1962 'compare-two-leo-files', 

1963 'file-compare-two-leo-files', 

1964 'edit-recent-files', 

1965 'exit-leo', 

1966 'help', # To do. 

1967 'help-for-abbreviations', 

1968 'help-for-autocompletion', 

1969 'help-for-bindings', 

1970 'help-for-command', 

1971 'help-for-creating-external-files', 

1972 'help-for-debugging-commands', 

1973 'help-for-drag-and-drop', 

1974 'help-for-dynamic-abbreviations', 

1975 'help-for-find-commands', 

1976 'help-for-keystroke', 

1977 'help-for-minibuffer', 

1978 'help-for-python', 

1979 'help-for-regular-expressions', 

1980 'help-for-scripting', 

1981 'help-for-settings', 

1982 'join-leo-irc', # Some online irc - parameters not working anymore 

1983 

1984 'print-body', 

1985 'print-cmd-docstrings', 

1986 'print-expanded-body', 

1987 'print-expanded-html', 

1988 'print-html', 

1989 'print-marked-bodies', 

1990 'print-marked-html', 

1991 'print-marked-nodes', 

1992 'print-node', 

1993 'print-sep', 

1994 'print-tree-bodies', 

1995 'print-tree-html', 

1996 'print-tree-nodes', 

1997 'print-window-state', 

1998 'quit-leo', 

1999 'reload-style-sheets', 

2000 'save-buffers-kill-leo', 

2001 'screen-capture-5sec', 

2002 'screen-capture-now', 

2003 'set-reference-file', # TODO : maybe offer this 

2004 'show-style-sheet', 

2005 'sort-recent-files', 

2006 'view-lossage', 

2007 

2008 # Buffers commands (Usage?) 

2009 'buffer-append-to', 

2010 'buffer-copy', 

2011 'buffer-insert', 

2012 'buffer-kill', 

2013 'buffer-prepend-to', 

2014 'buffer-switch-to', 

2015 'buffers-list', 

2016 'buffers-list-alphabetically', 

2017 

2018 # Open specific files... (MAYBE MAKE AVAILABLE?) 

2019 # 'ekr-projects', 

2020 'leo-cheat-sheet', # These duplicates are useful. 

2021 'leo-dist-leo', 

2022 'leo-docs-leo', 

2023 'leo-plugins-leo', 

2024 'leo-py-leo', 

2025 'leo-quickstart-leo', 

2026 'leo-scripts-leo', 

2027 'leo-unittest-leo', 

2028 

2029 # 'scripts', 

2030 'settings', 

2031 

2032 'open-cheat-sheet-leo', 

2033 'cheat-sheet-leo', 

2034 'cheat-sheet', 

2035 'open-desktop-integration-leo', 

2036 'desktop-integration-leo', 

2037 'open-leo-dist-leo', 

2038 'leo-dist-leo', 

2039 'open-leo-docs-leo', 

2040 'leo-docs-leo', 

2041 'open-leo-plugins-leo', 

2042 'leo-plugins-leo', 

2043 'open-leo-py-leo', 

2044 'leo-py-leo', 

2045 'open-leo-py-ref-leo', 

2046 'leo-py-ref-leo', 

2047 'open-leo-py', 

2048 'open-leo-settings', 

2049 'open-leo-settings-leo', 

2050 'open-local-settings', 

2051 'my-leo-settings', 

2052 'open-my-leo-settings', 

2053 'open-my-leo-settings-leo', 

2054 'leo-settings' 

2055 'open-quickstart-leo', 

2056 'leo-quickstart-leo' 

2057 'open-scripts-leo', 

2058 'leo-scripts-leo' 

2059 'open-unittest-leo', 

2060 'leo-unittest-leo', 

2061 

2062 # Open other places... 

2063 'desktop-integration-leo', 

2064 

2065 'open-offline-tutorial', 

2066 'open-online-home', 

2067 'open-online-toc', 

2068 'open-online-tutorials', 

2069 'open-online-videos', 

2070 'open-recent-file', 

2071 'open-theme-file', 

2072 'open-url', 

2073 'open-url-under-cursor', 

2074 'open-users-guide', 

2075 

2076 # Diffs - needs open file dialog 

2077 'diff-and-open-leo-files', 

2078 'diff-leo-files', 

2079 

2080 # --- ORIGINAL BAD COMMANDS START HERE --- 

2081 # Abbreviations... 

2082 'abbrev-kill-all', 

2083 'abbrev-list', 

2084 'dabbrev-completion', 

2085 'dabbrev-expands', 

2086 

2087 # Autocompletion... 

2088 'auto-complete', 

2089 'auto-complete-force', 

2090 'disable-autocompleter', 

2091 'disable-calltips', 

2092 'enable-autocompleter', 

2093 'enable-calltips', 

2094 

2095 # Debugger... 

2096 'debug', 

2097 'db-again', 

2098 'db-b', 

2099 'db-c', 

2100 'db-h', 

2101 'db-input', 

2102 'db-l', 

2103 'db-n', 

2104 'db-q', 

2105 'db-r', 

2106 'db-s', 

2107 'db-status', 

2108 'db-w', 

2109 

2110 # File operations... 

2111 'directory-make', 

2112 'directory-remove', 

2113 'file-delete', 

2114 'file-diff-files', 

2115 'file-insert', 

2116 #'file-new', 

2117 #'file-open-by-name', 

2118 

2119 # All others... 

2120 'shell-command', 

2121 'shell-command-on-region', 

2122 'cheat-sheet', 

2123 'dehoist', # Duplicates of de-hoist. 

2124 #'find-clone-all', 

2125 #'find-clone-all-flattened', 

2126 #'find-clone-tag', 

2127 #'find-all', 

2128 'find-all-unique-regex', 

2129 'find-character', 

2130 'find-character-extend-selection', 

2131 #'find-next', 

2132 #'find-prev', 

2133 'find-word', 

2134 'find-word-in-line', 

2135 

2136 'global-search', 

2137 

2138 'isearch-backward', 

2139 'isearch-backward-regexp', 

2140 'isearch-forward', 

2141 'isearch-forward-regexp', 

2142 'isearch-with-present-options', 

2143 

2144 #'replace', 

2145 #'replace-all', 

2146 'replace-current-character', 

2147 #'replace-then-find', 

2148 

2149 're-search-backward', 

2150 're-search-forward', 

2151 

2152 #'search-backward', 

2153 #'search-forward', 

2154 'search-return-to-origin', 

2155 

2156 'set-find-everywhere', 

2157 'set-find-node-only', 

2158 'set-find-suboutline-only', 

2159 'set-replace-string', 

2160 'set-search-string', 

2161 

2162 #'show-find-options', 

2163 

2164 #'start-search', 

2165 

2166 'toggle-find-collapses-nodes', 

2167 #'toggle-find-ignore-case-option', 

2168 #'toggle-find-in-body-option', 

2169 #'toggle-find-in-headline-option', 

2170 #'toggle-find-mark-changes-option', 

2171 #'toggle-find-mark-finds-option', 

2172 #'toggle-find-regex-option', 

2173 #'toggle-find-word-option', 

2174 'toggle-find-wrap-around-option', 

2175 

2176 'word-search-backward', 

2177 'word-search-forward', 

2178 

2179 # Buttons... 

2180 'delete-script-button-button', 

2181 

2182 # Clicks... 

2183 'click-click-box', 

2184 'click-icon-box', 

2185 'ctrl-click-at-cursor', 

2186 'ctrl-click-icon', 

2187 'double-click-icon-box', 

2188 'right-click-icon', 

2189 

2190 # Editors... 

2191 'add-editor', 'editor-add', 

2192 'delete-editor', 'editor-delete', 

2193 'detach-editor-toggle', 

2194 'detach-editor-toggle-max', 

2195 

2196 # Focus... 

2197 'cycle-editor-focus', 'editor-cycle-focus', 

2198 'focus-to-body', 

2199 'focus-to-find', 

2200 'focus-to-log', 

2201 'focus-to-minibuffer', 

2202 'focus-to-nav', 

2203 'focus-to-spell-tab', 

2204 'focus-to-tree', 

2205 

2206 'tab-cycle-next', 

2207 'tab-cycle-previous', 

2208 'tab-detach', 

2209 

2210 # Headlines.. 

2211 'abort-edit-headline', 

2212 'edit-headline', 

2213 'end-edit-headline', 

2214 

2215 # Layout and panes... 

2216 'adoc', 

2217 'adoc-with-preview', 

2218 

2219 'contract-body-pane', 

2220 'contract-log-pane', 

2221 'contract-outline-pane', 

2222 

2223 'edit-pane-csv', 

2224 'edit-pane-test-open', 

2225 'equal-sized-panes', 

2226 'expand-log-pane', 

2227 'expand-body-pane', 

2228 'expand-outline-pane', 

2229 

2230 'free-layout-context-menu', 

2231 'free-layout-load', 

2232 'free-layout-restore', 

2233 'free-layout-zoom', 

2234 

2235 'zoom-in', 

2236 'zoom-out', 

2237 

2238 # Log 

2239 'clear-log', 

2240 

2241 # Menus... 

2242 'activate-cmds-menu', 

2243 'activate-edit-menu', 

2244 'activate-file-menu', 

2245 'activate-help-menu', 

2246 'activate-outline-menu', 

2247 'activate-plugins-menu', 

2248 'activate-window-menu', 

2249 'context-menu-open', 

2250 'menu-shortcut', 

2251 

2252 # Modes... 

2253 'clear-extend-mode', 

2254 

2255 # Outline... (Commented off by Félix, Should work) 

2256 #'contract-or-go-left', 

2257 #'contract-node', 

2258 #'contract-parent', 

2259 

2260 # Scrolling... 

2261 'scroll-down-half-page', 

2262 'scroll-down-line', 

2263 'scroll-down-page', 

2264 'scroll-outline-down-line', 

2265 'scroll-outline-down-page', 

2266 'scroll-outline-left', 

2267 'scroll-outline-right', 

2268 'scroll-outline-up-line', 

2269 'scroll-outline-up-page', 

2270 'scroll-up-half-page', 

2271 'scroll-up-line', 

2272 'scroll-up-page', 

2273 

2274 # Windows... 

2275 'about-leo', 

2276 

2277 'cascade-windows', 

2278 'close-others', 

2279 'close-window', 

2280 

2281 'iconify-frame', 

2282 

2283 'find-tab-hide', 

2284 #'find-tab-open', 

2285 

2286 'hide-body-dock', 

2287 'hide-body-pane', 

2288 'hide-invisibles', 

2289 'hide-log-pane', 

2290 'hide-outline-dock', 

2291 'hide-outline-pane', 

2292 'hide-tabs-dock', 

2293 

2294 'minimize-all', 

2295 

2296 'resize-to-screen', 

2297 

2298 'show-body-dock', 

2299 'show-hide-body-dock', 

2300 'show-hide-outline-dock', 

2301 'show-hide-render-dock', 

2302 'show-hide-tabs-dock', 

2303 'show-tabs-dock', 

2304 'clean-diff', 

2305 'cm-external-editor', 

2306 

2307 'delete-@button-parse-json-button', 

2308 'delete-trace-statements', 

2309 

2310 'disable-idle-time-events', 

2311 

2312 'enable-idle-time-events', 

2313 'enter-quick-command-mode', 

2314 'exit-named-mode', 

2315 

2316 'F6-open-console', 

2317 

2318 'flush-lines', 

2319 'full-command', 

2320 

2321 'get-child-headlines', 

2322 

2323 'history', 

2324 

2325 'insert-file-name', 

2326 

2327 'justify-toggle-auto', 

2328 

2329 'keep-lines', 

2330 'keyboard-quit', 

2331 

2332 'line-number', 

2333 'line-numbering-toggle', 

2334 'line-to-headline', 

2335 

2336 'marked-list', 

2337 

2338 'mode-help', 

2339 

2340 'open-python-window', 

2341 

2342 'open-with-idle', 

2343 'open-with-open-office', 

2344 'open-with-scite', 

2345 'open-with-word', 

2346 

2347 'recolor', 

2348 'redraw', 

2349 

2350 'repeat-complex-command', 

2351 

2352 'session-clear', 

2353 'session-create', 

2354 'session-refresh', 

2355 'session-restore', 

2356 'session-snapshot-load', 

2357 'session-snapshot-save', 

2358 

2359 'set-colors', 

2360 'set-command-state', 

2361 'set-comment-column', 

2362 'set-extend-mode', 

2363 'set-fill-column', 

2364 'set-fill-prefix', 

2365 'set-font', 

2366 'set-insert-state', 

2367 'set-overwrite-state', 

2368 'set-silent-mode', 

2369 

2370 'show-buttons', 

2371 'show-calltips', 

2372 'show-calltips-force', 

2373 'show-color-names', 

2374 'show-color-wheel', 

2375 'show-commands', 

2376 'show-file-line', 

2377 

2378 'show-focus', 

2379 'show-fonts', 

2380 

2381 'show-invisibles', 

2382 'show-node-uas', 

2383 'show-outline-dock', 

2384 'show-plugin-handlers', 

2385 'show-plugins-info', 

2386 'show-settings', 

2387 'show-settings-outline', 

2388 'show-spell-info', 

2389 'show-stats', 

2390 'show-tips', 

2391 

2392 'style-set-selected', 

2393 

2394 'suspend', 

2395 

2396 'toggle-abbrev-mode', 

2397 'toggle-active-pane', 

2398 'toggle-angle-brackets', 

2399 'toggle-at-auto-at-edit', 

2400 'toggle-autocompleter', 

2401 'toggle-calltips', 

2402 'toggle-case-region', 

2403 'toggle-extend-mode', 

2404 'toggle-idle-time-events', 

2405 'toggle-input-state', 

2406 'toggle-invisibles', 

2407 'toggle-line-numbering-root', 

2408 'toggle-sparse-move', 

2409 'toggle-split-direction', 

2410 

2411 'what-line', 

2412 'eval', 

2413 'eval-block', 

2414 'eval-last', 

2415 'eval-last-pretty', 

2416 'eval-replace', 

2417 

2418 'find-quick', 

2419 'find-quick-changed', 

2420 'find-quick-selected', 

2421 'find-quick-test-failures', 

2422 'find-quick-timeline', 

2423 

2424 #'goto-next-history-node', 

2425 #'goto-prev-history-node', 

2426 

2427 'preview', 

2428 'preview-body', 

2429 'preview-expanded-body', 

2430 'preview-expanded-html', 

2431 'preview-html', 

2432 'preview-marked-bodies', 

2433 'preview-marked-html', 

2434 'preview-marked-nodes', 

2435 'preview-node', 

2436 'preview-tree-bodies', 

2437 'preview-tree-html', 

2438 'preview-tree-nodes', 

2439 

2440 'spell-add', 

2441 'spell-as-you-type-next', 

2442 'spell-as-you-type-toggle', 

2443 'spell-as-you-type-undo', 

2444 'spell-as-you-type-wrap', 

2445 'spell-change', 

2446 'spell-change-then-find', 

2447 'spell-find', 

2448 'spell-ignore', 

2449 'spell-tab-hide', 

2450 'spell-tab-open', 

2451 

2452 #'tag-children', 

2453 

2454 'todo-children-todo', 

2455 'todo-dec-pri', 

2456 'todo-find-todo', 

2457 'todo-fix-datetime', 

2458 'todo-inc-pri', 

2459 

2460 'vr', 

2461 'vr-contract', 

2462 'vr-expand', 

2463 'vr-hide', 

2464 'vr-lock', 

2465 'vr-pause-play-movie', 

2466 'vr-show', 

2467 'vr-toggle', 

2468 'vr-unlock', 

2469 'vr-update', 

2470 'vr-zoom', 

2471 

2472 'vs-create-tree', 

2473 'vs-dump', 

2474 'vs-reset', 

2475 'vs-update', 

2476 # Connected client's text editing commands should cover all of these... 

2477 'add-comments', 

2478 'add-space-to-lines', 

2479 'add-tab-to-lines', 

2480 'align-eq-signs', 

2481 

2482 'back-char', 

2483 'back-char-extend-selection', 

2484 'back-page', 

2485 'back-page-extend-selection', 

2486 'back-paragraph', 

2487 'back-paragraph-extend-selection', 

2488 'back-sentence', 

2489 'back-sentence-extend-selection', 

2490 'back-to-home', 

2491 'back-to-home-extend-selection', 

2492 'back-to-indentation', 

2493 'back-word', 

2494 'back-word-extend-selection', 

2495 'back-word-smart', 

2496 'back-word-smart-extend-selection', 

2497 'backward-delete-char', 

2498 'backward-delete-word', 

2499 'backward-delete-word-smart', 

2500 'backward-find-character', 

2501 'backward-find-character-extend-selection', 

2502 'backward-kill-paragraph', 

2503 'backward-kill-sentence', 

2504 'backward-kill-word', 

2505 'beginning-of-buffer', 

2506 'beginning-of-buffer-extend-selection', 

2507 'beginning-of-line', 

2508 'beginning-of-line-extend-selection', 

2509 

2510 'capitalize-word', 

2511 'center-line', 

2512 'center-region', 

2513 'clean-all-blank-lines', 

2514 'clean-all-lines', 

2515 'clean-body', 

2516 'clean-lines', 

2517 'clear-kill-ring', 

2518 'clear-selected-text', 

2519 'convert-blanks', 

2520 'convert-tabs', 

2521 'copy-text', 

2522 'cut-text', 

2523 

2524 'delete-char', 

2525 'delete-comments', 

2526 'delete-indentation', 

2527 'delete-spaces', 

2528 'delete-word', 

2529 'delete-word-smart', 

2530 'downcase-region', 

2531 'downcase-word', 

2532 

2533 'end-of-buffer', 

2534 'end-of-buffer-extend-selection', 

2535 'end-of-line', 

2536 'end-of-line-extend-selection', 

2537 

2538 'exchange-point-mark', 

2539 

2540 'extend-to-line', 

2541 'extend-to-paragraph', 

2542 'extend-to-sentence', 

2543 'extend-to-word', 

2544 

2545 'fill-paragraph', 

2546 'fill-region', 

2547 'fill-region-as-paragraph', 

2548 

2549 'finish-of-line', 

2550 'finish-of-line-extend-selection', 

2551 

2552 'forward-char', 

2553 'forward-char-extend-selection', 

2554 'forward-end-word', 

2555 'forward-end-word-extend-selection', 

2556 'forward-page', 

2557 'forward-page-extend-selection', 

2558 'forward-paragraph', 

2559 'forward-paragraph-extend-selection', 

2560 'forward-sentence', 

2561 'forward-sentence-extend-selection', 

2562 'forward-word', 

2563 'forward-word-extend-selection', 

2564 'forward-word-smart', 

2565 'forward-word-smart-extend-selection', 

2566 

2567 'go-anywhere', 

2568 'go-back', 

2569 'go-forward', 

2570 'goto-char', 

2571 

2572 'indent-region', 

2573 'indent-relative', 

2574 'indent-rigidly', 

2575 'indent-to-comment-column', 

2576 

2577 'insert-hard-tab', 

2578 'insert-newline', 

2579 'insert-parentheses', 

2580 'insert-soft-tab', 

2581 

2582 'kill-line', 

2583 'kill-paragraph', 

2584 'kill-pylint', 

2585 'kill-region', 

2586 'kill-region-save', 

2587 'kill-sentence', 

2588 'kill-to-end-of-line', 

2589 'kill-word', 

2590 'kill-ws', 

2591 

2592 'match-brackets', 

2593 

2594 'move-lines-down', 

2595 'move-lines-up', 

2596 'move-past-close', 

2597 'move-past-close-extend-selection', 

2598 

2599 'newline-and-indent', 

2600 'next-line', 

2601 'next-line-extend-selection', 

2602 'next-or-end-of-line', 

2603 'next-or-end-of-line-extend-selection', 

2604 

2605 'previous-line', 

2606 'previous-line-extend-selection', 

2607 'previous-or-beginning-of-line', 

2608 'previous-or-beginning-of-line-extend-selection', 

2609 

2610 'rectangle-clear', 

2611 'rectangle-close', 

2612 'rectangle-delete', 

2613 'rectangle-kill', 

2614 'rectangle-open', 

2615 'rectangle-string', 

2616 'rectangle-yank', 

2617 

2618 'remove-blank-lines', 

2619 'remove-newlines', 

2620 'remove-space-from-lines', 

2621 'remove-tab-from-lines', 

2622 

2623 'reverse-region', 

2624 'reverse-sort-lines', 

2625 'reverse-sort-lines-ignoring-case', 

2626 

2627 'paste-text', 

2628 'pop-cursor', 

2629 'push-cursor', 

2630 

2631 'select-all', 

2632 'select-next-trace-statement', 

2633 'select-to-matching-bracket', 

2634 

2635 'sort-columns', 

2636 'sort-fields', 

2637 'sort-lines', 

2638 'sort-lines-ignoring-case', 

2639 

2640 'split-defs', 

2641 'split-line', 

2642 

2643 'start-of-line', 

2644 'start-of-line-extend-selection', 

2645 

2646 'tabify', 

2647 'transpose-chars', 

2648 'transpose-lines', 

2649 'transpose-words', 

2650 

2651 'unformat-paragraph', 

2652 'unindent-region', 

2653 

2654 'untabify', 

2655 

2656 'upcase-region', 

2657 'upcase-word', 

2658 'update-ref-file', 

2659 

2660 'yank', 

2661 'yank-pop', 

2662 

2663 'zap-to-character', 

2664 

2665 ] 

2666 bad.extend(bad_list) 

2667 result = list(sorted(bad)) 

2668 return result 

2669 #@+node:felix.20210621233316.74: *6* server._good_commands 

2670 def _good_commands(self): 

2671 """Defined commands that should be available in a connected client""" 

2672 good_list = [ 

2673 

2674 'contract-all', 

2675 'contract-all-other-nodes', 

2676 'clone-node', 

2677 'copy-node', 

2678 'copy-marked-nodes', 

2679 'cut-node', 

2680 

2681 'de-hoist', 

2682 'delete-marked-nodes', 

2683 'delete-node', 

2684 # 'demangle-recent-files', 

2685 'demote', 

2686 'do-nothing', 

2687 'expand-and-go-right', 

2688 'expand-next-level', 

2689 'expand-node', 

2690 'expand-or-go-right', 

2691 'expand-prev-level', 

2692 'expand-to-level-1', 

2693 'expand-to-level-2', 

2694 'expand-to-level-3', 

2695 'expand-to-level-4', 

2696 'expand-to-level-5', 

2697 'expand-to-level-6', 

2698 'expand-to-level-7', 

2699 'expand-to-level-8', 

2700 'expand-to-level-9', 

2701 'expand-all', 

2702 'expand-all-subheads', 

2703 'expand-ancestors-only', 

2704 

2705 'find-next-clone', 

2706 

2707 'goto-first-node', 

2708 'goto-first-sibling', 

2709 'goto-first-visible-node', 

2710 'goto-last-node', 

2711 'goto-last-sibling', 

2712 'goto-last-visible-node', 

2713 'goto-next-changed', 

2714 'goto-next-clone', 

2715 'goto-next-marked', 

2716 'goto-next-node', 

2717 'goto-next-sibling', 

2718 'goto-next-visible', 

2719 'goto-parent', 

2720 'goto-prev-marked', 

2721 'goto-prev-node', 

2722 'goto-prev-sibling', 

2723 'goto-prev-visible', 

2724 

2725 'hoist', 

2726 

2727 'insert-node', 

2728 'insert-node-before', 

2729 'insert-as-first-child', 

2730 'insert-as-last-child', 

2731 'insert-child', 

2732 

2733 'mark', 

2734 'mark-changed-items', 

2735 'mark-first-parents', 

2736 'mark-subheads', 

2737 

2738 'move-marked-nodes', 

2739 'move-outline-down', 

2740 'move-outline-left', 

2741 'move-outline-right', 

2742 'move-outline-up', 

2743 

2744 'paste-node', 

2745 'paste-retaining-clones', 

2746 'promote', 

2747 'promote-bodies', 

2748 'promote-headlines', 

2749 

2750 'sort-children', 

2751 'sort-siblings', 

2752 

2753 'tangle', 

2754 'tangle-all', 

2755 'tangle-marked', 

2756 

2757 'unmark-all', 

2758 'unmark-first-parents', 

2759 #'clean-main-spell-dict', 

2760 #'clean-persistence', 

2761 #'clean-recent-files', 

2762 #'clean-spellpyx', 

2763 #'clean-user-spell-dict', 

2764 

2765 'clear-all-caches', 

2766 'clear-all-hoists', 

2767 'clear-all-uas', 

2768 'clear-cache', 

2769 'clear-node-uas', 

2770 #'clear-recent-files', 

2771 

2772 #'delete-first-icon', # ? maybe move to bad commands? 

2773 #'delete-last-icon', # ? maybe move to bad commands? 

2774 #'delete-node-icons', # ? maybe move to bad commands? 

2775 

2776 'dump-caches', 

2777 'dump-clone-parents', 

2778 'dump-expanded', 

2779 'dump-node', 

2780 'dump-outline', 

2781 

2782 #'insert-icon', # ? maybe move to bad commands? 

2783 

2784 #'set-ua', 

2785 

2786 'show-all-uas', 

2787 'show-bindings', 

2788 'show-clone-ancestors', 

2789 'show-clone-parents', 

2790 

2791 # Export files... 

2792 #'export-headlines', # export 

2793 #'export-jupyter-notebook', # export 

2794 #'outline-to-cweb', # export 

2795 #'outline-to-noweb', # export 

2796 #'remove-sentinels', # import 

2797 'typescript-to-py', 

2798 

2799 # Import files... # done through import all 

2800 'import-MORE-files', 

2801 'import-file', 

2802 'import-free-mind-files', 

2803 'import-jupyter-notebook', 

2804 'import-legacy-external-files', 

2805 'import-mind-jet-files', 

2806 'import-tabbed-files', 

2807 'import-todo-text-files', 

2808 'import-zim-folder', 

2809 

2810 # Read outlines... 

2811 'read-at-auto-nodes', 

2812 'read-at-file-nodes', 

2813 'read-at-shadow-nodes', 

2814 'read-file-into-node', 

2815 'read-outline-only', 

2816 'read-ref-file', 

2817 

2818 # Save Files. 

2819 'file-save', 

2820 'file-save-as', 

2821 'file-save-by-name', 

2822 'file-save-to', 

2823 'save', 

2824 'save-as', 

2825 'save-file', 

2826 'save-file-as', 

2827 'save-file-by-name', 

2828 'save-file-to', 

2829 'save-to', 

2830 

2831 # Write parts of outlines... 

2832 'write-at-auto-nodes', 

2833 'write-at-file-nodes', 

2834 'write-at-shadow-nodes', 

2835 'write-dirty-at-auto-nodes', 

2836 'write-dirty-at-file-nodes', 

2837 'write-dirty-at-shadow-nodes', 

2838 'write-edited-recent-files', 

2839 #'write-file-from-node', 

2840 'write-missing-at-file-nodes', 

2841 'write-outline-only', 

2842 

2843 'clone-find-all', 

2844 'clone-find-all-flattened', 

2845 'clone-find-all-flattened-marked', 

2846 'clone-find-all-marked', 

2847 'clone-find-parents', 

2848 'clone-find-tag', 

2849 'clone-marked-nodes', 

2850 'clone-node-to-last-node', 

2851 

2852 'clone-to-at-spot', 

2853 

2854 #'edit-setting', 

2855 #'edit-shortcut', 

2856 

2857 'execute-pytest', 

2858 'execute-script', 

2859 'extract', 

2860 'extract-names', 

2861 

2862 'goto-any-clone', 

2863 'goto-global-line', 

2864 #'goto-line', 

2865 'git-diff', 'gd', 

2866 

2867 'log-kill-listener', 'kill-log-listener', 

2868 'log-listen', 'listen-to-log', 

2869 

2870 'make-stub-files', 

2871 

2872 #'pdb', 

2873 

2874 'redo', 

2875 'rst3', 

2876 'run-all-unit-tests-externally', 

2877 'run-all-unit-tests-locally', 

2878 'run-marked-unit-tests-externally', 

2879 'run-marked-unit-tests-locally', 

2880 'run-selected-unit-tests-externally', 

2881 'run-selected-unit-tests-locally', 

2882 'run-tests', 

2883 

2884 'undo', 

2885 

2886 #'xdb', 

2887 

2888 # Beautify, blacken, fstringify... 

2889 'beautify-files', 

2890 'beautify-files-diff', 

2891 'blacken-files', 

2892 'blacken-files-diff', 

2893 #'diff-and-open-leo-files', 

2894 'diff-beautify-files', 

2895 'diff-fstringify-files', 

2896 #'diff-leo-files', 

2897 'diff-marked-nodes', 

2898 'fstringify-files', 

2899 'fstringify-files-diff', 

2900 'fstringify-files-silent', 

2901 'pretty-print-c', 

2902 'silent-fstringify-files', 

2903 

2904 # All other commands... 

2905 'at-file-to-at-auto', 

2906 

2907 'beautify-c', 

2908 

2909 'cls', 

2910 'c-to-python', 

2911 'c-to-python-clean-docs', 

2912 'check-derived-file', 

2913 'check-outline', 

2914 'code-to-rst', 

2915 #'compare-two-leo-files', 

2916 'convert-all-blanks', 

2917 'convert-all-tabs', 

2918 'count-children', 

2919 'count-pages', 

2920 'count-region', 

2921 

2922 #'desktop-integration-leo', 

2923 

2924 #'edit-recent-files', 

2925 #'exit-leo', 

2926 

2927 #'file-compare-two-leo-files', 

2928 'find-def', 

2929 'find-long-lines', 

2930 'find-missing-docstrings', 

2931 'flake8-files', 

2932 'flatten-outline', 

2933 'flatten-outline-to-node', 

2934 'flatten-script', 

2935 

2936 'gc-collect-garbage', 

2937 'gc-dump-all-objects', 

2938 'gc-dump-new-objects', 

2939 'gc-dump-objects-verbose', 

2940 'gc-show-summary', 

2941 

2942 #'help', # To do. 

2943 #'help-for-abbreviations', 

2944 #'help-for-autocompletion', 

2945 #'help-for-bindings', 

2946 #'help-for-command', 

2947 #'help-for-creating-external-files', 

2948 #'help-for-debugging-commands', 

2949 #'help-for-drag-and-drop', 

2950 #'help-for-dynamic-abbreviations', 

2951 #'help-for-find-commands', 

2952 #'help-for-keystroke', 

2953 #'help-for-minibuffer', 

2954 #'help-for-python', 

2955 #'help-for-regular-expressions', 

2956 #'help-for-scripting', 

2957 #'help-for-settings', 

2958 

2959 'insert-body-time', # ? 

2960 'insert-headline-time', 

2961 'insert-jupyter-toc', 

2962 'insert-markdown-toc', 

2963 

2964 'find-var', 

2965 

2966 #'join-leo-irc', 

2967 'join-node-above', 

2968 'join-node-below', 

2969 'join-selection-to-node-below', 

2970 

2971 'move-lines-to-next-node', 

2972 

2973 'new', 

2974 

2975 'open-outline', 

2976 

2977 'parse-body', 

2978 'parse-json', 

2979 'pandoc', 

2980 'pandoc-with-preview', 

2981 'paste-as-template', 

2982 

2983 #'print-body', 

2984 #'print-cmd-docstrings', 

2985 #'print-expanded-body', 

2986 #'print-expanded-html', 

2987 #'print-html', 

2988 #'print-marked-bodies', 

2989 #'print-marked-html', 

2990 #'print-marked-nodes', 

2991 #'print-node', 

2992 #'print-sep', 

2993 #'print-tree-bodies', 

2994 #'print-tree-html', 

2995 #'print-tree-nodes', 

2996 #'print-window-state', 

2997 

2998 'pyflakes', 

2999 'pylint', 

3000 'pylint-kill', 

3001 'python-to-coffeescript', 

3002 

3003 #'quit-leo', 

3004 

3005 'reformat-body', 

3006 'reformat-paragraph', 

3007 'refresh-from-disk', 

3008 'reload-settings', 

3009 #'reload-style-sheets', 

3010 'revert', 

3011 

3012 #'save-buffers-kill-leo', 

3013 #'screen-capture-5sec', 

3014 #'screen-capture-now', 

3015 'script-button', # ? 

3016 #'set-reference-file', 

3017 #'show-style-sheet', 

3018 #'sort-recent-files', 

3019 'sphinx', 

3020 'sphinx-with-preview', 

3021 'style-reload', # ? 

3022 

3023 'untangle', 

3024 'untangle-all', 

3025 'untangle-marked', 

3026 

3027 #'view-lossage', # ? 

3028 

3029 'weave', 

3030 

3031 # Dubious commands (to do)... 

3032 'act-on-node', 

3033 

3034 'cfa', 

3035 'cfam', 

3036 'cff', 

3037 'cffm', 

3038 'cft', 

3039 

3040 #'buffer-append-to', 

3041 #'buffer-copy', 

3042 #'buffer-insert', 

3043 #'buffer-kill', 

3044 #'buffer-prepend-to', 

3045 #'buffer-switch-to', 

3046 #'buffers-list', 

3047 #'buffers-list-alphabetically', 

3048 

3049 'chapter-back', 

3050 'chapter-next', 

3051 'chapter-select', 

3052 'chapter-select-main', 

3053 'create-def-list', # ? 

3054 ] 

3055 return good_list 

3056 #@+node:felix.20210621233316.75: *5* server.get_all_server_commands & helpers 

3057 def get_all_server_commands(self, param): 

3058 """ 

3059 Public server method: 

3060 Return the names of all callable public methods of the server. 

3061 """ 

3062 tag = 'get_all_server_commands' 

3063 names = self._get_all_server_commands() 

3064 if self.log_flag: # pragma: no cover 

3065 print(f"\n{tag}: {len(names)} server commands\n", flush=True) 

3066 g.printObj(names, tag=tag) 

3067 print('', flush=True) 

3068 return self._make_response({"server-commands": names}) 

3069 #@+node:felix.20210914231602.1: *6* _get_all_server_commands 

3070 def _get_all_server_commands(self): 

3071 """ 

3072 Private server method: 

3073 Return the names of all callable public methods of the server. 

3074 (Methods that do not start with an underscore '_') 

3075 """ 

3076 members = inspect.getmembers(self, inspect.ismethod) 

3077 return sorted([name for (name, value) in members if not name.startswith('_')]) 

3078 #@+node:felix.20210621233316.76: *5* server.init_connection 

3079 def _init_connection(self, web_socket): # pragma: no cover (tested in client). 

3080 """Begin the connection.""" 

3081 global connectionsTotal 

3082 if connectionsTotal == 1: 

3083 # First connection, so "Master client" setup 

3084 self.web_socket = web_socket 

3085 self.loop = asyncio.get_event_loop() 

3086 else: 

3087 # already exist, so "spectator-clients" setup 

3088 pass # nothing for now 

3089 #@+node:felix.20210621233316.77: *5* server.shut_down 

3090 def shut_down(self, param): 

3091 """Shut down the server.""" 

3092 tag = 'shut_down' 

3093 n = len(g.app.commanders()) 

3094 if n: # pragma: no cover 

3095 raise ServerError(f"{tag}: {n} open outlines") 

3096 raise TerminateServer("client requested shut down") 

3097 #@+node:felix.20210621233316.78: *3* server:server utils 

3098 #@+node:felix.20210621233316.79: *4* server._ap_to_p 

3099 def _ap_to_p(self, ap): 

3100 """ 

3101 Convert ap (archived position, a dict) to a valid Leo position. 

3102 

3103 Return False on any kind of error to support calls to invalid positions 

3104 after a document has been closed of switched and interface interaction 

3105 in the client generated incoming calls to 'getters' already sent. (for the 

3106 now inaccessible leo document commander.) 

3107 """ 

3108 tag = '_ap_to_p' 

3109 c = self._check_c() 

3110 gnx_d = c.fileCommands.gnxDict 

3111 try: 

3112 outer_stack = ap.get('stack') 

3113 if outer_stack is None: # pragma: no cover. 

3114 raise ServerError(f"{tag}: no stack in ap: {ap!r}") 

3115 if not isinstance(outer_stack, (list, tuple)): # pragma: no cover. 

3116 raise ServerError(f"{tag}: stack must be tuple or list: {outer_stack}") 

3117 # 

3118 def d_to_childIndex_v (d): 

3119 """Helper: return childIndex and v from d ["childIndex"] and d["gnx"].""" 

3120 childIndex = d.get('childIndex') 

3121 if childIndex is None: # pragma: no cover. 

3122 raise ServerError(f"{tag}: no childIndex in {d}") 

3123 try: 

3124 childIndex = int(childIndex) 

3125 except Exception: # pragma: no cover. 

3126 raise ServerError(f"{tag}: bad childIndex: {childIndex!r}") 

3127 gnx = d.get('gnx') 

3128 if gnx is None: # pragma: no cover. 

3129 raise ServerError(f"{tag}: no gnx in {d}.") 

3130 v = gnx_d.get(gnx) 

3131 if v is None: # pragma: no cover. 

3132 raise ServerError(f"{tag}: gnx not found: {gnx!r}") 

3133 return childIndex, v 

3134 # 

3135 # Compute p.childIndex and p.v. 

3136 childIndex, v = d_to_childIndex_v(ap) 

3137 # 

3138 # Create p.stack. 

3139 stack = [] 

3140 for stack_d in outer_stack: 

3141 stack_childIndex, stack_v = d_to_childIndex_v(stack_d) 

3142 stack.append((stack_v, stack_childIndex)) 

3143 # 

3144 # Make p and check p. 

3145 p = Position(v, childIndex, stack) 

3146 if not c.positionExists(p): # pragma: no cover. 

3147 raise ServerError(f"{tag}: p does not exist in {c.shortFileName()}") 

3148 except Exception: 

3149 if self.log_flag or traces: 

3150 print( 

3151 f"{tag}: Bad ap: {ap!r}\n" 

3152 # f"{tag}: position: {p!r}\n" 

3153 f"{tag}: v {v!r} childIndex: {childIndex!r}\n" 

3154 f"{tag}: stack: {stack!r}", flush=True) 

3155 return False # Return false on any error so caller can react 

3156 return p 

3157 #@+node:felix.20210621233316.80: *4* server._check_c 

3158 def _check_c(self): 

3159 """Return self.c or raise ServerError if self.c is None.""" 

3160 tag = '_check_c' 

3161 c = self.c 

3162 if not c: # pragma: no cover 

3163 raise ServerError(f"{tag}: no open commander") 

3164 return c 

3165 #@+node:felix.20210621233316.81: *4* server._check_outline 

3166 def _check_outline(self, c): 

3167 """Check self.c for consistency.""" 

3168 # Check that all positions exist. 

3169 self._check_outline_positions(c) 

3170 # Test round-tripping. 

3171 self._test_round_trip_positions(c) 

3172 #@+node:felix.20210621233316.82: *4* server._check_outline_positions 

3173 def _check_outline_positions(self, c): 

3174 """Verify that all positions in c exist.""" 

3175 tag = '_check_outline_positions' 

3176 for p in c.all_positions(copy=False): 

3177 if not c.positionExists(p): # pragma: no cover 

3178 message = f"{tag}: position {p!r} does not exist in {c.shortFileName()}" 

3179 print(message, flush=True) 

3180 self._dump_position(p) 

3181 raise ServerError(message) 

3182 #@+node:felix.20210621233316.84: *4* server._do_leo_command_by_name 

3183 def _do_leo_command_by_name(self, command_name, param): 

3184 """ 

3185 Generic call to a command in Leo's Commands class or any subcommander class. 

3186 

3187 The param["ap"] position is to be selected before having the command run, 

3188 while the param["keep"] parameter specifies wether the original position 

3189 should be re-selected afterward. 

3190 

3191 TODO: The whole of those operations is to be undoable as one undo step. 

3192 

3193 command_name: the name of a Leo command (a kebab-cased string). 

3194 param["ap"]: an archived position. 

3195 param["keep"]: preserve the current selection, if possible. 

3196 

3197 """ 

3198 tag = '_do_leo_command_by_name' 

3199 c = self._check_c() 

3200 

3201 if command_name in self.bad_commands_list: # pragma: no cover 

3202 raise ServerError(f"{tag}: disallowed command: {command_name!r}") 

3203 

3204 keepSelection = False # Set default, optional component of param 

3205 if "keep" in param: 

3206 keepSelection = param["keep"] 

3207 

3208 func = c.commandsDict.get(command_name) # Getting from kebab-cased 'Command Name' 

3209 if not func: # pragma: no cover 

3210 raise ServerError(f"{tag}: Leo command not found: {command_name!r}") 

3211 

3212 p = self._get_p(param) 

3213 try: 

3214 if p == c.p: 

3215 value = func(event={"c":c}) # no need for re-selection 

3216 else: 

3217 old_p = c.p # preserve old position 

3218 c.selectPosition(p) # set position upon which to perform the command 

3219 value = func(event={"c":c}) 

3220 if keepSelection and c.positionExists(old_p): 

3221 # Only if 'keep' old position was set, and old_p still exists 

3222 c.selectPosition(old_p) 

3223 except Exception as e: 

3224 print(f"_do_leo_command Recovered from Error {e!s}", flush=True) 

3225 return self._make_response() # Return empty on error 

3226 # 

3227 # Tag along a possible return value with info sent back by _make_response 

3228 if self._is_jsonable(value): 

3229 return self._make_response({"return-value": value}) 

3230 return self._make_response() 

3231 #@+node:ekr.20210722184932.1: *4* server._do_leo_function_by_name 

3232 def _do_leo_function_by_name(self, function_name, param): 

3233 """ 

3234 Generic call to a method in Leo's Commands class or any subcommander class. 

3235 

3236 The param["ap"] position is to be selected before having the command run, 

3237 while the param["keep"] parameter specifies wether the original position 

3238 should be re-selected afterward. 

3239 

3240 TODO: The whole of those operations is to be undoable as one undo step. 

3241 

3242 command: the name of a method 

3243 param["ap"]: an archived position. 

3244 param["keep"]: preserve the current selection, if possible. 

3245 

3246 """ 

3247 tag = '_do_leo_function_by_name' 

3248 c = self._check_c() 

3249 

3250 keepSelection = False # Set default, optional component of param 

3251 if "keep" in param: 

3252 keepSelection = param["keep"] 

3253 

3254 func = self._get_commander_method(function_name) # GET FUNC 

3255 if not func: # pragma: no cover 

3256 raise ServerError(f"{tag}: Leo command not found: {function_name!r}") 

3257 

3258 p = self._get_p(param) 

3259 try: 

3260 if p == c.p: 

3261 value = func(event={"c":c}) # no need for re-selection 

3262 else: 

3263 old_p = c.p # preserve old position 

3264 c.selectPosition(p) # set position upon which to perform the command 

3265 value = func(event={"c":c}) 

3266 if keepSelection and c.positionExists(old_p): 

3267 # Only if 'keep' old position was set, and old_p still exists 

3268 c.selectPosition(old_p) 

3269 except Exception as e: 

3270 print(f"_do_leo_command Recovered from Error {e!s}", flush=True) 

3271 return self._make_response() # Return empty on error 

3272 # 

3273 # Tag along a possible return value with info sent back by _make_response 

3274 if self._is_jsonable(value): 

3275 return self._make_response({"return-value": value}) 

3276 return self._make_response() 

3277 #@+node:felix.20210621233316.85: *4* server._do_message 

3278 def _do_message(self, d): 

3279 """ 

3280 Handle d, a python dict representing the incoming request. 

3281 The d dict must have the three (3) following keys: 

3282 

3283 "id": A positive integer. 

3284 

3285 "action": A string, which is either: 

3286 - The name of public method of this class, prefixed with '!'. 

3287 - The name of a Leo command, prefixed with '-' 

3288 - The name of a method of a Leo class, without prefix. 

3289 

3290 "param": A dict to be passed to the called "action" method. 

3291 (Passed to the public method, or the _do_leo_command. Often contains ap, text & keep) 

3292 

3293 Return a dict, created by _make_response or _make_minimal_response 

3294 that contains at least an 'id' key. 

3295 

3296 """ 

3297 global traces 

3298 tag = '_do_message' 

3299 trace, verbose = 'request' in traces, 'verbose' in traces 

3300 

3301 # Require "id" and "action" keys 

3302 id_ = d.get("id") 

3303 if id_ is None: # pragma: no cover 

3304 raise ServerError(f"{tag}: no id") 

3305 action = d.get("action") 

3306 if action is None: # pragma: no cover 

3307 raise ServerError(f"{tag}: no action") 

3308 

3309 # TODO : make/force always an object from the client connected. 

3310 param = d.get('param', {}) # Can be none or a string 

3311 # Set log flag. 

3312 if param: 

3313 self.log_flag = param.get("log") 

3314 pass 

3315 else: 

3316 param = {} 

3317 

3318 # Handle traces. 

3319 if trace and verbose: # pragma: no cover 

3320 g.printObj(d, tag=f"request {id_}") 

3321 print('', flush=True) 

3322 elif trace: # pragma: no cover 

3323 keys = sorted(param.keys()) 

3324 if action == '!set_config': 

3325 keys_s = f"({len(keys)} keys)" 

3326 elif len(keys) > 5: 

3327 keys_s = '\n ' + '\n '.join(keys) 

3328 else: 

3329 keys_s = ', '.join(keys) 

3330 print(f" request {id_:<4} {action:<30} {keys_s}", flush=True) 

3331 

3332 # Set the current_id and action ivars for _make_response. 

3333 self.current_id = id_ 

3334 self.action = action 

3335 

3336 # Execute the requested action. 

3337 if action[0] == "!": 

3338 action = action[1:] # Remove exclamation point "!" 

3339 func = self._do_server_command # Server has this method. 

3340 elif action[0] == '-': 

3341 action = action[1:] # Remove dash "-" 

3342 func = self._do_leo_command_by_name # It's a command name. 

3343 else: 

3344 func = self._do_leo_function_by_name # It's the name of a method in some commander. 

3345 result = func(action, param) 

3346 if result is None: # pragma: no cover 

3347 raise ServerError(f"{tag}: no response: {action!r}") 

3348 return result 

3349 #@+node:felix.20210621233316.86: *4* server._do_server_command 

3350 def _do_server_command(self, action, param): 

3351 tag = '_do_server_command' 

3352 # Disallow hidden methods. 

3353 if action.startswith('_'): # pragma: no cover 

3354 raise ServerError(f"{tag}: action starts with '_': {action!r}") 

3355 # Find and execute the server method. 

3356 func = getattr(self, action, None) 

3357 if not func: 

3358 raise ServerError(f"{tag}: action not found: {action!r}") # pragma: no cover 

3359 if not callable(func): 

3360 raise ServerError(f"{tag}: not callable: {func!r}") # pragma: no cover 

3361 return func(param) 

3362 #@+node:felix.20210621233316.87: *4* server._dump_* 

3363 def _dump_outline(self, c): # pragma: no cover 

3364 """Dump the outline.""" 

3365 tag = '_dump_outline' 

3366 print(f"{tag}: {c.shortFileName()}...\n", flush=True) 

3367 for p in c.all_positions(): 

3368 self._dump_position(p) 

3369 print('', flush=True) 

3370 

3371 def _dump_position(self, p): # pragma: no cover 

3372 level_s = ' ' * 2 * p.level() 

3373 print(f"{level_s}{p.childIndex():2} {p.v.gnx} {p.h}", flush=True) 

3374 #@+node:felix.20210624160812.1: *4* server._emit_signon 

3375 def _emit_signon(self): 

3376 """Simulate the Initial Leo Log Entry""" 

3377 tag = 'emit_signon' 

3378 if self.loop: 

3379 g.app.computeSignon() 

3380 signon = [] 

3381 for z in (g.app.signon, g.app.signon1): 

3382 for z2 in z.split('\n'): 

3383 signon.append(z2.strip()) 

3384 g.es("\n".join(signon)) 

3385 else: 

3386 raise ServerError(f"{tag}: no loop ready for emit_signon") 

3387 #@+node:felix.20210625230236.1: *4* server._get_commander_method 

3388 def _get_commander_method(self, command): 

3389 """ Return the given method (p_command) in the Commands class or subcommanders.""" 

3390 # First, try the commands class. 

3391 c = self._check_c() 

3392 func = getattr(c, command, None) 

3393 if func: 

3394 return func 

3395 # Otherwise, search all subcommanders for the method. 

3396 table = ( # This table comes from c.initObjectIvars. 

3397 'abbrevCommands', 

3398 'bufferCommands', 

3399 'chapterCommands', 

3400 'controlCommands', 

3401 'convertCommands', 

3402 'debugCommands', 

3403 'editCommands', 

3404 'editFileCommands', 

3405 'evalController', 

3406 'gotoCommands', 

3407 'helpCommands', 

3408 'keyHandler', 

3409 'keyHandlerCommands', 

3410 'killBufferCommands', 

3411 'leoCommands', 

3412 'leoTestManager', 

3413 'macroCommands', 

3414 'miniBufferWidget', 

3415 'printingController', 

3416 'queryReplaceCommands', 

3417 'rectangleCommands', 

3418 'searchCommands', 

3419 'spellCommands', 

3420 'vimCommands', # Not likely to be useful. 

3421 ) 

3422 for ivar in table: 

3423 subcommander = getattr(c, ivar, None) 

3424 if subcommander: 

3425 func = getattr(subcommander, command, None) 

3426 if func: 

3427 return func 

3428 return None 

3429 #@+node:felix.20210621233316.91: *4* server._get_focus 

3430 def _get_focus(self): 

3431 """Server helper method to get the focused panel name string""" 

3432 tag = '_get_focus' 

3433 try: 

3434 w = g.app.gui.get_focus() 

3435 focus = g.app.gui.widget_name(w) 

3436 except Exception as e: 

3437 raise ServerError(f"{tag}: exception trying to get the focused widget: {e}") 

3438 return focus 

3439 #@+node:felix.20210621233316.90: *4* server._get_p 

3440 def _get_p(self, param, strict = False): 

3441 """ 

3442 Return _ap_to_p(param["ap"]) or c.p., 

3443 or False if the strict flag is set 

3444 """ 

3445 tag = '_get_ap' 

3446 c = self.c 

3447 if not c: # pragma: no cover 

3448 raise ServerError(f"{tag}: no c") 

3449 

3450 ap = param.get("ap") 

3451 if ap: 

3452 p = self._ap_to_p(ap) # Convertion 

3453 if p: 

3454 if not c.positionExists(p): # pragma: no cover 

3455 raise ServerError(f"{tag}: position does not exist. ap: {ap!r}") 

3456 return p # Return the position 

3457 if strict: 

3458 return False 

3459 # Fallback to c.p 

3460 if not c.p: # pragma: no cover 

3461 raise ServerError(f"{tag}: no c.p") 

3462 

3463 return c.p 

3464 #@+node:felix.20210621233316.92: *4* server._get_position_d 

3465 def _get_position_d(self, p): 

3466 """ 

3467 Return a python dict that is adding 

3468 graphical representation data and flags 

3469 to the base 'ap' dict from _p_to_ap. 

3470 (To be used by the connected client GUI.) 

3471 """ 

3472 d = self._p_to_ap(p) 

3473 d['headline'] = p.h 

3474 d['level'] = p.level() 

3475 if p.v.u: 

3476 if g.leoServer.leoServerConfig and g.leoServer.leoServerConfig.get("uAsBoolean", False): 

3477 # uAsBoolean is 'thruthy' 

3478 d['u'] = True 

3479 else: 

3480 # Normal output if no options set 

3481 d['u'] = p.v.u 

3482 if bool(p.b): 

3483 d['hasBody'] = True 

3484 if p.hasChildren(): 

3485 d['hasChildren'] = True 

3486 if p.isCloned(): 

3487 d['cloned'] = True 

3488 if p.isDirty(): 

3489 d['dirty'] = True 

3490 if p.isExpanded(): 

3491 d['expanded'] = True 

3492 if p.isMarked(): 

3493 d['marked'] = True 

3494 if p.isAnyAtFileNode(): 

3495 d['atFile'] = True 

3496 if p == self.c.p: 

3497 d['selected'] = True 

3498 return d 

3499 #@+node:felix.20210705211625.1: *4* server._is_jsonable 

3500 def _is_jsonable(self, x): 

3501 try: 

3502 json.dumps(x, cls=SetEncoder) 

3503 return True 

3504 except (TypeError, OverflowError): 

3505 return False 

3506 #@+node:felix.20210621233316.94: *4* server._make_minimal_response 

3507 def _make_minimal_response(self, package=None): 

3508 """ 

3509 Return a json string representing a response dict. 

3510 

3511 The 'package' kwarg, if present, must be a python dict describing a 

3512 response. package may be an empty dict or None. 

3513 

3514 The 'p' kwarg, if present, must be a position. 

3515 

3516 First, this method creates a response (a python dict) containing all 

3517 the keys in the 'package' dict. 

3518 

3519 Then it adds 'id' to the package. 

3520 

3521 Finally, this method returns the json string corresponding to the 

3522 response. 

3523 """ 

3524 if package is None: 

3525 package = {} 

3526 

3527 # Always add id. 

3528 package ["id"] = self.current_id 

3529 

3530 return json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

3531 #@+node:felix.20210621233316.93: *4* server._make_response 

3532 def _make_response(self, package=None): 

3533 """ 

3534 Return a json string representing a response dict. 

3535 

3536 The 'package' kwarg, if present, must be a python dict describing a 

3537 response. package may be an empty dict or None. 

3538 

3539 The 'p' kwarg, if present, must be a position. 

3540 

3541 First, this method creates a response (a python dict) containing all 

3542 the keys in the 'package' dict, with the following added keys: 

3543 

3544 - "id": The incoming id. 

3545 - "commander": A dict describing self.c. 

3546 - "node": None, or an archived position describing self.c.p. 

3547 

3548 Finally, this method returns the json string corresponding to the 

3549 response. 

3550 """ 

3551 global traces 

3552 tag = '_make_response' 

3553 trace = self.log_flag or 'response' in traces 

3554 verbose = 'verbose' in traces 

3555 c = self.c # It is valid for c to be None. 

3556 if package is None: 

3557 package = {} 

3558 p = package.get("p") 

3559 if p: 

3560 del package ["p"] 

3561 # Raise an *internal* error if checks fail. 

3562 if isinstance(package, str): # pragma: no cover 

3563 raise InternalServerError(f"{tag}: bad package kwarg: {package!r}") 

3564 if p and not isinstance(p, Position): # pragma: no cover 

3565 raise InternalServerError(f"{tag}: bad p kwarg: {p!r}") 

3566 if p and not c: # pragma: no cover 

3567 raise InternalServerError(f"{tag}: p but not c") 

3568 if p and not c.positionExists(p): # pragma: no cover 

3569 raise InternalServerError(f"{tag}: p does not exist: {p!r}") 

3570 if c and not c.p: # pragma: no cover 

3571 raise InternalServerError(f"{tag}: empty c.p") 

3572 

3573 # Always add id 

3574 package ["id"] = self.current_id 

3575 

3576 # The following keys are relevant only if there is an open commander. 

3577 if c: 

3578 # Allow commands, especially _get_redraw_d, to specify p! 

3579 p = p or c.p 

3580 package ["commander"] = { 

3581 "changed": c.isChanged(), 

3582 "fileName": c.fileName(), # Can be None for new files. 

3583 } 

3584 # Add all the node data, including: 

3585 # - "node": self._p_to_ap(p) # Contains p.gnx, p.childIndex and p.stack. 

3586 # - All the *cheap* redraw data for p. 

3587 redraw_d = self._get_position_d(p) 

3588 package ["node"] = redraw_d 

3589 

3590 # Handle traces. 

3591 if trace and verbose: # pragma: no cover 

3592 g.printObj(package, tag=f"response {self.current_id}") 

3593 print('', flush=True) 

3594 elif trace: # pragma: no cover 

3595 keys = sorted(package.keys()) 

3596 keys_s = ', '.join(keys) 

3597 print(f"response {self.current_id:<4} {keys_s}", flush=True) 

3598 

3599 return json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

3600 #@+node:felix.20210621233316.95: *4* server._p_to_ap 

3601 def _p_to_ap(self, p): 

3602 """ 

3603 * From Leo plugin leoflexx.py * 

3604 

3605 Convert Leo position p to a serializable archived position. 

3606 

3607 This returns only position-related data. 

3608 get_position_data returns all data needed to redraw the screen. 

3609 """ 

3610 self._check_c() 

3611 stack = [{'gnx': v.gnx, 'childIndex': childIndex} 

3612 for (v, childIndex) in p.stack] 

3613 return { 

3614 'childIndex': p._childIndex, 

3615 'gnx': p.v.gnx, 

3616 'stack': stack, 

3617 } 

3618 #@+node:felix.20210621233316.96: *4* server._positionFromGnx 

3619 def _positionFromGnx(self, gnx): 

3620 """Return first p node with this gnx or false""" 

3621 c = self._check_c() 

3622 for p in c.all_unique_positions(): 

3623 if p.v.gnx == gnx: 

3624 return p 

3625 return False 

3626 #@+node:felix.20210622232409.1: *4* server._send_async_output & helper 

3627 def _send_async_output(self, package, toAll = False): 

3628 """ 

3629 Send data asynchronously to the client 

3630 """ 

3631 tag = "send async output" 

3632 jsonPackage = json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

3633 if "async" not in package: 

3634 InternalServerError(f"\n{tag}: async member missing in package {jsonPackage} \n") 

3635 if self.loop: 

3636 self.loop.create_task(self._async_output(jsonPackage, toAll)) 

3637 else: 

3638 InternalServerError(f"\n{tag}: loop not ready {jsonPackage} \n") 

3639 #@+node:felix.20210621233316.89: *5* server._async_output 

3640 async def _async_output(self, json, toAll = False): # pragma: no cover (tested in server) 

3641 """Output json string to the web_socket""" 

3642 global connectionsTotal 

3643 tag = '_async_output' 

3644 outputBytes = bytes(json, 'utf-8') 

3645 if toAll: 

3646 if connectionsPool: # asyncio.wait doesn't accept an empty list 

3647 await asyncio.wait([asyncio.create_task(client.send(outputBytes)) for client in connectionsPool]) 

3648 else: 

3649 g.trace(f"{tag}: no web socket. json: {json!r}") 

3650 else: 

3651 if self.web_socket: 

3652 await self.web_socket.send(outputBytes) 

3653 else: 

3654 g.trace(f"{tag}: no web socket. json: {json!r}") 

3655 #@+node:felix.20210621233316.97: *4* server._test_round_trip_positions 

3656 def _test_round_trip_positions(self, c): # pragma: no cover (tested in client). 

3657 """Test the round tripping of p_to_ap and ap_to_p.""" 

3658 tag = '_test_round_trip_positions' 

3659 for p in c.all_unique_positions(): 

3660 ap = self._p_to_ap(p) 

3661 p2 = self._ap_to_p(ap) 

3662 if p != p2: 

3663 self._dump_outline(c) 

3664 raise ServerError(f"{tag}: round-trip failed: ap: {ap!r}, p: {p!r}, p2: {p2!r}") 

3665 #@+node:felix.20210625002950.1: *4* server._yieldAllRootChildren 

3666 def _yieldAllRootChildren(self): 

3667 """Return all root children P nodes""" 

3668 c = self._check_c() 

3669 p = c.rootPosition() 

3670 while p: 

3671 yield p 

3672 p.moveToNext() 

3673 

3674 #@-others 

3675#@+node:felix.20210621233316.105: ** function: main & helpers 

3676def main(): # pragma: no cover (tested in client) 

3677 """python script for leo integration via leoBridge""" 

3678 global websockets 

3679 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile 

3680 if not websockets: 

3681 print('websockets not found') 

3682 print('pip install websockets') 

3683 return 

3684 

3685 #@+others 

3686 #@+node:felix.20210807214524.1: *3* function: cancel_tasks 

3687 def cancel_tasks(to_cancel, loop): 

3688 if not to_cancel: 

3689 return 

3690 

3691 for task in to_cancel: 

3692 task.cancel() 

3693 

3694 loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) 

3695 

3696 for task in to_cancel: 

3697 if task.cancelled(): 

3698 continue 

3699 if task.exception() is not None: 

3700 loop.call_exception_handler( 

3701 { 

3702 "message": "unhandled exception during asyncio.run() shutdown", 

3703 "exception": task.exception(), 

3704 "task": task, 

3705 } 

3706 ) 

3707 #@+node:ekr.20210825115746.1: *3* function: center_tk_frame 

3708 def center_tk_frame(top): 

3709 """Center the top-level Frame.""" 

3710 # https://stackoverflow.com/questions/3352918 

3711 top.update_idletasks() 

3712 screen_width = top.winfo_screenwidth() 

3713 screen_height = top.winfo_screenheight() 

3714 size = tuple(int(_) for _ in top.geometry().split('+')[0].split('x')) 

3715 x = screen_width/2 - size[0]/2 

3716 y = screen_height/2 - size[1]/2 

3717 top.geometry("+%d+%d" % (x, y)) 

3718 #@+node:felix.20210804130751.1: *3* function: close_server 

3719 def close_Server(): 

3720 """ 

3721 Close the server by stopping the loop 

3722 """ 

3723 print('Closing Leo Server', flush=True) 

3724 if loop.is_running(): 

3725 loop.stop() 

3726 else: 

3727 print('Loop was not running', flush=True) 

3728 #@+node:ekr.20210825172913.1: *3* function: general_yes_no_dialog & helpers 

3729 def general_yes_no_dialog( 

3730 c, 

3731 title, # Not used. 

3732 message=None, # Must exist. 

3733 yesMessage="&Yes", # Not used. 

3734 noMessage="&No", # Not used. 

3735 yesToAllMessage=None, # Not used. 

3736 defaultButton="Yes", # Not used 

3737 cancelMessage=None, # Not used. 

3738 ): 

3739 """ 

3740 Monkey-patched implementation of LeoQtGui.runAskYesNoCancelDialog 

3741 offering *only* Yes/No buttons. 

3742 

3743 This will fallback to a tk implementation if the qt library is unavailable. 

3744 

3745 This raises a dialog and return either 'yes' or 'no'. 

3746 """ 

3747 #@+others # define all helper functions. 

3748 #@+node:ekr.20210801175921.1: *4* function: tk_runAskYesNoCancelDialog & helpers 

3749 def tk_runAskYesNoCancelDialog(c): 

3750 """ 

3751 Tk version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons. 

3752 """ 

3753 if g.unitTesting: 

3754 return None 

3755 root = top = val = None # Non-locals 

3756 #@+others # define helper functions 

3757 #@+node:ekr.20210801180311.4: *5* function: create_yes_no_frame 

3758 def create_yes_no_frame(message, top): 

3759 """Create the dialog's frame.""" 

3760 frame = Tk.Frame(top) 

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

3762 label = Tk.Label(frame, text=message, bg="white") 

3763 label.pack(pady=10) 

3764 # Create buttons. 

3765 f = Tk.Frame(top) 

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

3767 b = Tk.Button(f, width=6, text="Yes", bd=4, underline=0, command=yesButton) 

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

3769 b = Tk.Button(f, width=6, text="No", bd=2, underline=0, command=noButton) 

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

3771 #@+node:ekr.20210801180311.5: *5* function: callbacks 

3772 def noButton(event=None): 

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

3774 nonlocal val 

3775 print(f"Not saved: {c.fileName()}") 

3776 val = "no" 

3777 top.destroy() 

3778 

3779 def yesButton(event=None): 

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

3781 nonlocal val 

3782 print(f"Saved: {c.fileName()}") 

3783 val = "yes" 

3784 top.destroy() 

3785 #@-others 

3786 root = Tk.Tk() 

3787 root.withdraw() 

3788 root.update() 

3789 

3790 top = Tk.Toplevel(root) 

3791 top.title("Saved changed outline?") 

3792 create_yes_no_frame(message, top) 

3793 top.bind("<Return>", yesButton) 

3794 top.bind("y", yesButton) 

3795 top.bind("Y", yesButton) 

3796 top.bind("n", noButton) 

3797 top.bind("N", noButton) 

3798 top.lift() 

3799 

3800 center_tk_frame(top) 

3801 

3802 top.grab_set() # Make the dialog a modal dialog. 

3803 

3804 root.update() 

3805 root.wait_window(top) 

3806 

3807 top.destroy() 

3808 root.destroy() 

3809 return val 

3810 #@+node:ekr.20210825170952.1: *4* function: qt_runAskYesNoCancelDialog 

3811 def qt_runAskYesNoCancelDialog(c): 

3812 """ 

3813 Qt version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons. 

3814 """ 

3815 if g.unitTesting: 

3816 return None 

3817 dialog = QtWidgets.QMessageBox(None) 

3818 dialog.setIcon(Information.Warning) 

3819 dialog.setWindowTitle("Saved changed outline?") 

3820 if message: 

3821 dialog.setText(message) 

3822 # Creation order determines returned value. 

3823 yes = dialog.addButton(yesMessage, ButtonRole.YesRole) 

3824 dialog.addButton(noMessage, ButtonRole.NoRole) 

3825 dialog.setDefaultButton(yes) 

3826 # Set the Leo icon. 

3827 core_dir = os.path.dirname(__file__) 

3828 icon_path = os.path.join(core_dir, "..", "Icons", "leoApp.ico") 

3829 if os.path.exists(icon_path): 

3830 pixmap = QtGui.QPixmap() 

3831 pixmap.load(icon_path) 

3832 icon = QtGui.QIcon(pixmap) 

3833 dialog.setWindowIcon(icon) 

3834 # None of these grabs focus from the console window. 

3835 dialog.raise_() 

3836 dialog.setFocus() 

3837 app.processEvents() # type:ignore 

3838 # val is the same as the creation order. 

3839 # Tested with both Qt6 and Qt5. 

3840 val = dialog.exec() if isQt6 else dialog.exec_() 

3841 if val == 0: 

3842 print(f"Saved: {c.fileName()}") 

3843 return 'yes' 

3844 print(f"Not saved: {c.fileName()}") 

3845 return 'no' 

3846 #@-others 

3847 try: 

3848 # Careful: raise the Tk dialog if there are errors in the Qt code. 

3849 if 1: # Prefer Qt. 

3850 from leo.core.leoQt import isQt6, QtGui, QtWidgets 

3851 from leo.core.leoQt import ButtonRole, Information 

3852 if QtGui and QtWidgets: 

3853 app = QtWidgets.QApplication([]) 

3854 assert app 

3855 val = qt_runAskYesNoCancelDialog(c) 

3856 assert val in ('yes', 'no') 

3857 return val 

3858 except Exception: 

3859 pass 

3860 return tk_runAskYesNoCancelDialog(c) 

3861 #@+node:felix.20210621233316.107: *3* function: get_args 

3862 def get_args(): # pragma: no cover 

3863 """ 

3864 Get arguments from the command line and sets them globally. 

3865 """ 

3866 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile, traces 

3867 

3868 def leo_file(s): 

3869 if os.path.exists(s): 

3870 return s 

3871 print(f"\nNot a .leo file: {s!r}") 

3872 sys.exit(1) 

3873 

3874 description = ''.join([ 

3875 " leoserver.py\n", 

3876 " ------------\n", 

3877 " Offers single or multiple concurrent websockets\n", 

3878 " for JSON based remote-procedure-calls\n", 

3879 " to a shared instance of leo.core.leoBridge\n", 

3880 " \n", 

3881 " Clients may be written in any language:\n", 

3882 " - leo.core.leoclient is an example client written in python.\n", 

3883 " - leoInteg (https://github.com/boltex/leointeg) is written in typescript.\n" 

3884 ]) 

3885 # usage = 'leoserver.py [-a <address>] [-p <port>] [-l <limit>] [-f <file>] [--dirty] [--persist]' 

3886 usage = 'python leo.core.leoserver [options...]' 

3887 trace_s = 'request,response,verbose' 

3888 valid_traces = [z.strip() for z in trace_s.split(',')] 

3889 parser = argparse.ArgumentParser(description=description, usage=usage, 

3890 formatter_class=argparse.RawTextHelpFormatter) 

3891 add = parser.add_argument 

3892 add('-a', '--address', dest='wsHost', type=str, default=wsHost, metavar='STR', 

3893 help='server address. Defaults to ' + str(wsHost)) 

3894 add('-p', '--port', dest='wsPort', type=int, default=wsPort, metavar='N', 

3895 help='port number. Defaults to ' + str(wsPort)) 

3896 add('-l', '--limit', dest='wsLimit', type=int, default=wsLimit, metavar='N', 

3897 help='maximum number of clients. Defaults to '+ str(wsLimit)) 

3898 add('-f', '--file', dest='argFile', type=leo_file, metavar='PATH', 

3899 help='open a .leo file at startup') 

3900 add('--persist', dest='wsPersist', action='store_true', 

3901 help='do not quit when last client disconnects') 

3902 add('-d', '--dirty', dest='wsSkipDirty', action='store_true', 

3903 help='do not warn about dirty files when quitting') 

3904 add('--trace', dest='traces', type=str, metavar='STRINGS', 

3905 help=f"comma-separated list of {trace_s}") 

3906 add('-v', '--version', dest='v', action='store_true', 

3907 help='show version and exit') 

3908 # Parse 

3909 args = parser.parse_args() 

3910 # Handle the args and set them up globally 

3911 wsHost = args.wsHost 

3912 wsPort = args.wsPort 

3913 wsLimit = args.wsLimit 

3914 wsPersist = bool(args.wsPersist) 

3915 wsSkipDirty = bool(args.wsSkipDirty) 

3916 argFile = args.argFile 

3917 if args.traces: 

3918 ok = True 

3919 for z in args.traces.split(','): 

3920 if z in valid_traces: 

3921 traces.append(z) 

3922 else: 

3923 ok = False 

3924 print(f"Ignoring invalid --trace value: {z!r}", flush=True) 

3925 if not ok: 

3926 print(f"Valid traces are: {','.join(valid_traces)}", flush=True) 

3927 print(f"--trace={','.join(traces)}", flush=True) 

3928 if args.v: 

3929 print(__version__) 

3930 sys.exit(0) 

3931 # Sanitize limit. 

3932 if wsLimit < 1: 

3933 wsLimit = 1 

3934 #@+node:felix.20210803174312.1: *3* function: notify_clients 

3935 async def notify_clients(action, excludedConn = None): 

3936 global connectionsTotal 

3937 if connectionsPool: # asyncio.wait doesn't accept an empty list 

3938 opened = bool(controller.c) # c can be none if no files opened 

3939 m = json.dumps({ 

3940 "async": "refresh", 

3941 "action": action, 

3942 "opened": opened, 

3943 }, separators=(',', ':'), cls=SetEncoder) 

3944 clientSetCopy = connectionsPool.copy() 

3945 if excludedConn: 

3946 clientSetCopy.discard(excludedConn) 

3947 if clientSetCopy: 

3948 # if still at least one to notify 

3949 await asyncio.wait([asyncio.create_task(client.send(m)) for client in clientSetCopy]) 

3950 

3951 #@+node:felix.20210803174312.2: *3* function: register_client 

3952 async def register_client(websocket): 

3953 global connectionsTotal 

3954 connectionsPool.add(websocket) 

3955 await notify_clients("unregister", websocket) 

3956 #@+node:felix.20210807160828.1: *3* function: save_dirty 

3957 def save_dirty(): 

3958 """ 

3959 Ask the user about dirty files if any remained opened. 

3960 """ 

3961 # Monkey-patch the dialog method first. 

3962 g.app.gui.runAskYesNoCancelDialog = general_yes_no_dialog 

3963 # Loop all commanders and 'close' them for dirty check 

3964 commanders = g.app.commanders() 

3965 for commander in commanders: 

3966 if commander.isChanged() and commander.fileName(): 

3967 commander.close() # Patched 'ask' methods will open dialog 

3968 #@+node:felix.20210803174312.3: *3* function: unregister_client 

3969 async def unregister_client(websocket): 

3970 global connectionsTotal 

3971 connectionsPool.remove(websocket) 

3972 await notify_clients("unregister") 

3973 #@+node:felix.20210621233316.106: *3* function: ws_handler (server) 

3974 async def ws_handler(websocket, path): 

3975 """ 

3976 The web socket handler: server.ws_server. 

3977 

3978 It must be a coroutine accepting two arguments: a WebSocketServerProtocol and the request URI. 

3979 """ 

3980 global connectionsTotal, wsLimit 

3981 tag = 'server' 

3982 trace = False 

3983 verbose = False 

3984 connected = False 

3985 

3986 try: 

3987 # Websocket connection startup 

3988 if connectionsTotal >= wsLimit: 

3989 print(f"{tag}: User Refused, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True) 

3990 await websocket.close(1001) 

3991 return 

3992 connected = True # local variable 

3993 connectionsTotal += 1 # global variable 

3994 print(f"{tag}: User Connected, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True) 

3995 # If first connection set it as the main client connection 

3996 controller._init_connection(websocket) 

3997 await register_client(websocket) 

3998 # Start by sending empty as 'ok'. 

3999 n = 0 

4000 await websocket.send(controller._make_response()) 

4001 controller._emit_signon() 

4002 

4003 # Websocket connection message handling loop 

4004 async for json_message in websocket: 

4005 try: 

4006 n += 1 

4007 d = None 

4008 d = json.loads(json_message) 

4009 if trace and verbose: 

4010 print(f"{tag}: got: {d}", flush=True) 

4011 elif trace: 

4012 print(f"{tag}: got: {d}", flush=True) 

4013 answer = controller._do_message(d) 

4014 except TerminateServer as e: 

4015 # pylint: disable=no-value-for-parameter,unexpected-keyword-arg 

4016 raise websockets.exceptions.ConnectionClosed(code=1000, reason=e) 

4017 except ServerError as e: 

4018 data = f"{d}" if d else f"json syntax error: {json_message!r}" 

4019 error = f"{tag}: ServerError: {e}...\n{tag}: {data}" 

4020 print("", flush=True) 

4021 print(error, flush=True) 

4022 print("", flush=True) 

4023 package = { 

4024 "id": controller.current_id, 

4025 "action": controller.action, 

4026 "request": data, 

4027 "ServerError": f"{e}", 

4028 } 

4029 answer = json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4030 except InternalServerError as e: # pragma: no cover 

4031 print(f"{tag}: InternalServerError {e}", flush=True) 

4032 break 

4033 except Exception as e: # pragma: no cover 

4034 print(f"{tag}: Unexpected Exception! {e}", flush=True) 

4035 g.print_exception() 

4036 print('', flush=True) 

4037 break 

4038 await websocket.send(answer) 

4039 

4040 # If not a 'getter' send refresh signal to other clients 

4041 if controller.action[0:5] != "!get_" and controller.action != "!do_nothing": 

4042 await notify_clients(controller.action, websocket) 

4043 

4044 except websockets.exceptions.ConnectionClosedError as e: # pragma: no cover 

4045 print(f"{tag}: connection closed error: {e}") 

4046 except websockets.exceptions.ConnectionClosed as e: 

4047 print(f"{tag}: connection closed: {e}") 

4048 finally: 

4049 if connected: 

4050 connectionsTotal -= 1 

4051 await unregister_client(websocket) 

4052 print(f"{tag} connection finished. Total: {connectionsTotal}, Limit: {wsLimit}") 

4053 # Check for persistence flag if all connections are closed 

4054 if connectionsTotal == 0 and not wsPersist: 

4055 print("Shutting down leoserver") 

4056 # Preemptive closing of tasks 

4057 for task in asyncio.all_tasks(): 

4058 task.cancel() 

4059 close_Server() # Stops the run_forever loop 

4060 #@-others 

4061 

4062 # Make the first real line of output more visible. 

4063 print("", flush=True) 

4064 

4065 # Sets sHost, wsPort, wsLimit, wsPersist, wsSkipDirty fileArg and traces 

4066 get_args() # Set global values from the command line arguments 

4067 print("Starting LeoBridge... (Launch with -h for help)", flush=True) 

4068 

4069 # Open leoBridge. 

4070 controller = LeoServer() # Single instance of LeoServer, i.e., an instance of leoBridge 

4071 if argFile: 

4072 # Open specified file argument 

4073 try: 

4074 print(f"Opening file: {argFile}", flush=True) 

4075 controller.open_file({"filename":argFile}) 

4076 except Exception: 

4077 print("Opening file failed", flush=True) 

4078 

4079 # Start the server. 

4080 loop = asyncio.get_event_loop() 

4081 

4082 try: 

4083 try: 

4084 server = websockets.serve(ws_handler, wsHost, wsPort, max_size=None) # pylint: disable=no-member 

4085 realtime_server = loop.run_until_complete(server) 

4086 except OSError as e: 

4087 print(e) 

4088 print("Trying with IPv4 Family", flush=True) 

4089 server = websockets.serve(ws_handler, wsHost, wsPort, family=socket.AF_INET, max_size=None) # pylint: disable=no-member 

4090 realtime_server = loop.run_until_complete(server) 

4091 

4092 signon = SERVER_STARTED_TOKEN + f" at {wsHost} on port: {wsPort}.\n" 

4093 if wsPersist: 

4094 signon = signon + "Persistent server\n" 

4095 if wsSkipDirty: 

4096 signon = signon + "No prompt about dirty file(s) when closing server\n" 

4097 if wsLimit > 1: 

4098 signon = signon + f"Total client limit is {wsLimit}.\n" 

4099 signon = signon + "Ctrl+c to break" 

4100 print(signon, flush=True) 

4101 loop.run_forever() 

4102 

4103 except KeyboardInterrupt: 

4104 print("Process interrupted", flush=True) 

4105 

4106 finally: 

4107 # Execution continues here after server is interupted (e.g. with ctrl+c) 

4108 realtime_server.close() 

4109 if not wsSkipDirty: 

4110 print("Checking for changed commanders...", flush=True) 

4111 save_dirty() 

4112 cancel_tasks(asyncio.all_tasks(loop), loop) 

4113 loop.run_until_complete(loop.shutdown_asyncgens()) 

4114 loop.close() 

4115 asyncio.set_event_loop(None) 

4116 print("Stopped leobridge server", flush=True) 

4117#@-others 

4118if __name__ == '__main__': 

4119 # pytest will *not* execute this code. 

4120 main() 

4121#@-leo