Coverage for C:\Repos\leo-editor\leo\core\leoserver.py: 58%

1682 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

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 fnmatch 

16import inspect 

17import itertools 

18import json 

19import os 

20from collections import OrderedDict 

21import re 

22import sys 

23import socket 

24import textwrap 

25import time 

26from typing import Any, Dict, List, Union 

27# Third-party. 

28try: 

29 import tkinter as Tk 

30except Exception: 

31 Tk = None 

32# #2300 

33try: 

34 import websockets 

35except Exception: 

36 websockets = None 

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

38core_dir = os.path.dirname(__file__) 

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

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

41if leo_path not in sys.path: 

42 sys.path.append(leo_path) 

43# Leo 

44from leo.core.leoNodes import Position, PosList 

45from leo.core.leoGui import StringFindTabManager 

46from leo.core.leoExternalFiles import ExternalFilesController 

47#@-<< imports >> 

48version_tuple = (1, 0, 1) 

49v1, v2, v3 = version_tuple 

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

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

52 

53# Server defaults 

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

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

56connectionsPool = set() # type:ignore 

57connectionsTotal = 0 # Current connected client total 

58# Customizable server options 

59argFile = "" 

60traces: List = [] # list of traces names, to be used as flags to output traces 

61wsLimit = 1 

62wsPersist = False 

63wsSkipDirty = False 

64wsHost = "localhost" 

65wsPort = 32125 

66 

67#@+others 

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

69class SetEncoder(json.JSONEncoder): 

70 def default(self, obj): 

71 if isinstance(obj, set): 

72 return list(obj) 

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

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

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

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

77 pass 

78 

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

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

81 pass 

82 

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

84 """Ask the server to terminate.""" 

85 pass 

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

87class ServerExternalFilesController(ExternalFilesController): 

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

89 # pylint: disable=no-else-return 

90 

91 #@+others 

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

93 def __init__(self): 

94 """Ctor for ServerExternalFiles class.""" 

95 super().__init__() 

96 

97 self.on_idle_count = 0 

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

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

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

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

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

103 

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

105 self.infoMessage = None 

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

107 

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

109 

110 self.waitingForAnswer = False 

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

112 self.lastCommander = None 

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

114 def clientResult(self, p_result): 

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

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

117 if not self.waitingForAnswer: 

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

119 return 

120 

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

122 # act accordingly 

123 

124 # 1- if ok, unblock 'warn' 

125 # 2- if no, unblock 'ask' 

126 # ------------------------------------------ Nothing special to do 

127 

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

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

130 self.yesno_all_time = time.time() 

131 self.yesno_all_answer = p_result.lower() 

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

133 

134 path = "" 

135 if self.lastPNode: 

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

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

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

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

140 self.lastCommander.selectPosition(self.lastPNode) 

141 self.lastCommander.refreshFromDisk() 

142 elif self.lastCommander: 

143 path = self.lastCommander.fileName() 

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

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

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

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

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

149 

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

151 if path: 

152 self.set_time(path) 

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

154 

155 self.waitingForAnswer = False # unblock 

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

157 if self.lastCommander: 

158 self.idle_check_commander(self.lastCommander) 

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

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

161 def check_overwrite(self, c, path): 

162 if self.has_changed(path): 

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

164 g.leoServer._send_async_output(package, True) 

165 return True 

166 

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

168 def on_idle(self): 

169 """ 

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

171 for which @bool check_for_changed_external_file is True. 

172 """ 

173 # Fix for flushing the terminal console to pass through 

174 sys.stdout.flush() 

175 

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

177 return 

178 if self.waitingForAnswer: 

179 return 

180 

181 self.on_idle_count += 1 

182 

183 if self.unchecked_commanders: 

184 # Check the next commander for which 

185 # @bool check_for_changed_external_file is True. 

186 c = self.unchecked_commanders.pop() 

187 self.lastCommander = c 

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

189 self.idle_check_commander(c) 

190 else: 

191 # Add all commanders for which 

192 # @bool check_for_changed_external_file is True. 

193 self.unchecked_commanders = [ 

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

195 ] 

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

197 def idle_check_commander(self, c): 

198 """ 

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

200 changes. 

201 """ 

202 self.infoMessage = None # reset infoMessage 

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

204 

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

206 self.idle_check_leo_file(c) 

207 # 

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

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

210 for p in c.all_unique_positions(): 

211 if self.waitingForAnswer: 

212 break 

213 if p.isAnyAtFileNode(): 

214 self.idle_check_at_file_node(c, p) 

215 

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

217 if self.infoMessage: 

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

219 g.leoServer._send_async_output(package, True) 

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

221 def idle_check_leo_file(self, c): 

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

223 path = c.fileName() 

224 if not self.has_changed(path): 

225 return 

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

227 self.set_time(path) 

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

229 # For now, ignore the #1888 fix method 

230 if self.ask(c, path): 

231 #reload Commander 

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

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

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

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

236 def idle_check_at_file_node(self, c, p): 

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

238 trace = False 

239 path = g.fullPath(c, p) 

240 has_changed = self.has_changed(path) 

241 if trace: 

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

243 if has_changed: 

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

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

246 # Fix #1081: issue a warning. 

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

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

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

250 c.selectPosition(self.lastPNode) 

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

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

253 if g.leoServer.leoServerConfig: 

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

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

256 c.selectPosition(old_p) 

257 

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

259 self.set_time(path) 

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

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

262 def open_with(self, c, d): 

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

264 return 

265 

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

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

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

269 """ 

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

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

272 client, blocking further checks until result received. 

273 """ 

274 # check with leoServer's config first 

275 if g.leoServer.leoServerConfig: 

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

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

278 if bool('yes' in check_config): 

279 self.infoMessage = "refreshed" 

280 return True 

281 else: 

282 self.infoMessage = "ignored" 

283 return False 

284 # let original function resolve 

285 

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

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

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

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

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

291 if not p: 

292 where = 'the outline node' 

293 else: 

294 where = p.h 

295 

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

297 

298 if _is_leo: 

299 s = '\n'.join([ 

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

301 'Reload it?' 

302 ]) 

303 else: 

304 s = '\n'.join([ 

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

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

307 ]) 

308 

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

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

311 

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

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

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

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

316 def is_enabled(self, c): 

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

318 # check with the leoServer config first 

319 if g.leoServer.leoServerConfig: 

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

321 if bool('check' in check_config): 

322 return True 

323 if bool('ignore' in check_config): 

324 return False 

325 # let original function resolve 

326 return super().is_enabled(c) 

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

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

329 """ 

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

331 

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

333 """ 

334 # check with leoServer's config first 

335 if g.leoServer.leoServerConfig: 

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

337 

338 if check_config != "none": 

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

340 if not self.infoMessage: 

341 self.infoMessage = "warn" 

342 return 

343 

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

345 return 

346 if not p: 

347 g.trace('NO P') 

348 return 

349 

350 s = '\n'.join([ 

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

352 path), 

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

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

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

356 ]) 

357 

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

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

360 

361 g.leoServer._send_async_output(package, True) 

362 self.waitingForAnswer = True 

363 #@-others 

364#@+node:jlunz.20151027094647.1: ** class OrderedDefaultDict (OrderedDict) 

365class OrderedDefaultDict(OrderedDict): 

366 """ 

367 Credit: http://stackoverflow.com/questions/4126348/ 

368 how-do-i-rewrite-this-function-to-implement-ordereddict/4127426#4127426 

369 """ 

370 def __init__(self, *args, **kwargs): 

371 if not args: 

372 self.default_factory = None 

373 else: 

374 if not (args[0] is None or callable(args[0])): 

375 raise TypeError('first argument must be callable or None') 

376 self.default_factory = args[0] 

377 args = args[1:] 

378 super().__init__(*args, **kwargs) 

379 

380 def __missing__(self, key): 

381 if self.default_factory is None: 

382 raise KeyError(key) 

383 self[key] = default = self.default_factory() 

384 return default 

385 

386 def __reduce__(self): # optional, for pickle support 

387 args = (self.default_factory,) if self.default_factory else () 

388 return self.__class__, args, None, None, self.items() 

389#@+node:felix.20220225003906.1: ** class QuickSearchController 

390class QuickSearchController: 

391 

392 #@+others 

393 #@+node:felix.20220225003906.2: *3* __init__ 

394 def __init__(self, c): 

395 self.c = c 

396 self.lw = [] # empty list 

397 

398 # Keys are id(w),values are either tuples in tuples (w (p,pos)) or tuples (w, f) 

399 # (function f is when built from addGeneric) 

400 self.its = {} 

401 

402 # self.worker = threadutil.UnitWorker() 

403 # self.widgetUI = ui 

404 self.fileDirectives = ["@clean", "@file", "@asis", "@edit", 

405 "@auto", "@auto-md", "@auto-org", 

406 "@auto-otl", "@auto-rst"] 

407 

408 self._search_patterns = [] 

409 

410 self.navText = '' 

411 self.showParents = True 

412 self.isTag = False # added concept to combine tag pane functionality 

413 self.searchOptions = 0 

414 self.searchOptionsStrings = ["All", "Subtree", "File", 

415 "Chapter", "Node"] 

416 

417 #@+node:felix.20220225224130.1: *3* matchlines 

418 def matchlines(self, b, miter): 

419 res = [] 

420 for m in miter: 

421 st, en = g.getLine(b, m.start()) 

422 li = b[st:en].strip() 

423 res.append((li, (m.start(), m.end()))) 

424 return res 

425 

426 #@+node:felix.20220225003906.4: *3* addItem 

427 def addItem(self, it, val): 

428 self.its[id(it)] = (it, val) 

429 # changed to 999 from 3000 to replace old threadutil behavior 

430 return len(self.its) > 999 # Limit to 999 for now 

431 #@+node:felix.20220225003906.5: *3* addBodyMatches 

432 def addBodyMatches(self, poslist): 

433 lineMatchHits = 0 

434 for p in poslist: 

435 it = {"type": "headline", "label": p.h} 

436 # it = QtWidgets.QListWidgetItem(p.h, self.lw) 

437 # f = it.font() 

438 # f.setBold(True) 

439 # it.setFont(f) 

440 if self.addItem(it, (p, None)): 

441 return lineMatchHits 

442 ms = self.matchlines(p.b, p.matchiter) 

443 for ml, pos in ms: 

444 lineMatchHits += 1 

445 # it = QtWidgets.QListWidgetItem(" " + ml, self.lw) 

446 it = {"type": "body", "label": ml} 

447 if self.addItem(it, (p, pos)): 

448 return lineMatchHits 

449 return lineMatchHits 

450 #@+node:felix.20220225003906.6: *3* addParentMatches 

451 def addParentMatches(self, parent_list): 

452 lineMatchHits = 0 

453 for parent_key, parent_value in parent_list.items(): 

454 if isinstance(parent_key, str): 

455 v = self.c.fileCommands.gnxDict.get(parent_key) 

456 h = v.h if v else parent_key 

457 # it = QtWidgets.QListWidgetItem(h, self.lw) 

458 it = {"type": "parent", "label": h} 

459 else: 

460 # it = QtWidgets.QListWidgetItem(parent_key.h, self.lw) 

461 it = {"type": "parent", "label": parent_key.h} 

462 # f = it.font() 

463 # f.setItalic(True) 

464 # it.setFont(f) 

465 if self.addItem(it, (parent_key, None)): 

466 return lineMatchHits 

467 for p in parent_value: 

468 # it = QtWidgets.QListWidgetItem(" " + p.h, self.lw) 

469 it = {"type": "headline", "label": p.h} 

470 # f = it.font() 

471 # f.setBold(True) 

472 # it.setFont(f) 

473 if self.addItem(it, (p, None)): 

474 return lineMatchHits 

475 if hasattr(p, "matchiter"): #p might be not have body matches 

476 ms = self.matchlines(p.b, p.matchiter) 

477 for ml, pos in ms: 

478 lineMatchHits += 1 

479 # it = QtWidgets.QListWidgetItem(" " + " " + ml, self.lw) 

480 it = {"type": "body", "label": ml} 

481 if self.addItem(it, (p, pos)): 

482 return lineMatchHits 

483 return lineMatchHits 

484 

485 #@+node:felix.20220225003906.7: *3* addGeneric 

486 def addGeneric(self, text, f): 

487 """ Add generic callback """ 

488 # it = QtWidgets.QListWidgetItem(text, self.lw) 

489 it = {"type": "generic", "label": text} 

490 self.its[id(it)] = (it, f) 

491 return it 

492 

493 #@+node:felix.20220318222437.1: *3* addTag 

494 def addTag(self, text): 

495 """ add Tag label """ 

496 it = {"type": "tag", "label": text} 

497 self.its[id(it)] = (it, None) 

498 return it 

499 

500 #@+node:felix.20220225003906.8: *3* addHeadlineMatches 

501 def addHeadlineMatches(self, poslist): 

502 for p in poslist: 

503 it = {"type": "headline", "label": p.h} 

504 # it = QtWidgets.QListWidgetItem(p.h, self.lw) 

505 # f = it.font() 

506 # f.setBold(True) 

507 # it.setFont(f) 

508 if self.addItem(it, (p, None)): 

509 return 

510 #@+node:felix.20220225003906.9: *3* clear 

511 def clear(self): 

512 self.its = {} 

513 self.lw.clear() 

514 

515 #@+node:felix.20220225003906.10: *3* doNodeHistory 

516 def doNodeHistory(self): 

517 nh = PosList(po[0] for po in self.c.nodeHistory.beadList) 

518 nh.reverse() 

519 self.clear() 

520 self.addHeadlineMatches(nh) 

521 

522 #@+node:felix.20220225003906.11: *3* doSearchHistory 

523 def doSearchHistory(self): 

524 self.clear() 

525 def sHistSelect(x): 

526 def _f(): 

527 # self.widgetUI.lineEdit.setText(x) 

528 self.c.scon.navText = x 

529 self.doSearch(x) 

530 return _f 

531 for pat in self._search_patterns: 

532 self.addGeneric(pat, sHistSelect(pat)) 

533 

534 def pushSearchHistory(self, pat): 

535 if pat in self._search_patterns: 

536 return 

537 self._search_patterns = ([pat] + self._search_patterns)[:30] 

538 

539 #@+node:felix.20220225003906.12: *3* doTimeline 

540 def doTimeline(self): 

541 c = self.c 

542 timeline = [p.copy() for p in c.all_unique_positions()] 

543 timeline.sort(key=lambda x: x.gnx, reverse=True) 

544 self.clear() 

545 self.addHeadlineMatches(timeline) 

546 #@+node:felix.20220225003906.13: *3* doChanged 

547 def doChanged(self): 

548 c = self.c 

549 changed = [p.copy() for p in c.all_unique_positions() if p.isDirty()] 

550 self.clear() 

551 self.addHeadlineMatches(changed) 

552 #@+node:felix.20220225003906.14: *3* doSearch 

553 def doSearch(self, pat): 

554 hitBase = False 

555 self.clear() 

556 self.pushSearchHistory(pat) 

557 if not pat.startswith('r:'): 

558 hpat = fnmatch.translate('*' + pat + '*').replace(r"\Z(?ms)", "") 

559 bpat = fnmatch.translate(pat).rstrip('$').replace(r"\Z(?ms)", "") 

560 # in python 3.6 there is no (?ms) at the end 

561 # only \Z 

562 bpat = bpat.replace(r'\Z', '') 

563 flags = re.IGNORECASE 

564 else: 

565 hpat = pat[2:] 

566 bpat = pat[2:] 

567 flags = 0 # type:ignore 

568 combo = self.searchOptionsStrings[self.searchOptions] 

569 if combo == "All": 

570 hNodes = self.c.all_positions() 

571 bNodes = self.c.all_positions() 

572 elif combo == "Subtree": 

573 hNodes = self.c.p.self_and_subtree() 

574 bNodes = self.c.p.self_and_subtree() 

575 elif combo == "File": 

576 found = False 

577 node = self.c.p 

578 while not found and not hitBase: 

579 h = node.h 

580 if h: 

581 h = h.split()[0] 

582 if h in self.fileDirectives: 

583 found = True 

584 else: 

585 if node.level() == 0: 

586 hitBase = True 

587 else: 

588 node = node.parent() 

589 hNodes = node.self_and_subtree() 

590 bNodes = node.self_and_subtree() 

591 elif combo == "Chapter": 

592 found = False 

593 node = self.c.p 

594 while not found and not hitBase: 

595 h = node.h 

596 if h: 

597 h = h.split()[0] 

598 if h == "@chapter": 

599 found = True 

600 else: 

601 if node.level() == 0: 

602 hitBase = True 

603 else: 

604 node = node.parent() 

605 if hitBase: 

606 # If I hit the base then revert to all positions 

607 # this is basically the "main" chapter 

608 hitBase = False #reset 

609 hNodes = self.c.all_positions() 

610 bNodes = self.c.all_positions() 

611 else: 

612 hNodes = node.self_and_subtree() 

613 bNodes = node.self_and_subtree() 

614 

615 else: 

616 hNodes = [self.c.p] 

617 bNodes = [self.c.p] 

618 

619 if not hitBase: 

620 hm = self.find_h(hpat, hNodes, flags) 

621 bm = self.find_b(bpat, bNodes, flags) 

622 bm_keys = [match.key() for match in bm] 

623 numOfHm = len(hm) #do this before trim to get accurate count 

624 hm = [match for match in hm if match.key() not in bm_keys] 

625 if self.showParents: 

626 parents = OrderedDefaultDict(list) 

627 for nodeList in [hm, bm]: 

628 for node in nodeList: 

629 if node.level() == 0: 

630 parents["Root"].append(node) 

631 else: 

632 parents[node.parent().gnx].append(node) 

633 lineMatchHits = self.addParentMatches(parents) 

634 else: 

635 self.addHeadlineMatches(hm) 

636 lineMatchHits = self.addBodyMatches(bm) 

637 

638 hits = numOfHm + lineMatchHits 

639 self.lw.insert(0, "{} hits".format(hits)) 

640 

641 else: 

642 if combo == "File": 

643 self.lw.insert(0, "External file directive not found " + 

644 "during search") 

645 #@+node:felix.20220313183922.1: *3* doTag 

646 def doTag(self, pat): 

647 """ 

648 Search for tags: outputs position list 

649 If empty pattern, list tags *strings* instead 

650 """ 

651 if not pat: 

652 # No pattern! list all tags as string 

653 c = self.c 

654 self.clear() 

655 d: Dict[str, Any] = {} 

656 for p in c.all_unique_positions(): 

657 u = p.v.u 

658 tags = set(u.get('__node_tags', set([]))) 

659 for tag in tags: 

660 aList = d.get(tag, []) 

661 aList.append(p.h) 

662 d[tag] = aList 

663 if d: 

664 for key in sorted(d): 

665 # key is unique tag 

666 self.addTag(key) 

667 return 

668 # else: non empty pattern, so find tag! 

669 hm = self.find_tag(pat) 

670 self.clear() # needed for external client ui replacement: fills self.its 

671 self.addHeadlineMatches(hm) # added for external client ui replacement: fills self.its 

672 #@+node:felix.20220225003906.15: *3* bgSearch 

673 def bgSearch(self, pat): 

674 if not pat.startswith('r:'): 

675 hpat = fnmatch.translate('*' + pat + '*').replace(r"\Z(?ms)", "") 

676 # bpat = fnmatch.translate(pat).rstrip('$').replace(r"\Z(?ms)","") 

677 flags = re.IGNORECASE 

678 else: 

679 hpat = pat[2:] 

680 # bpat = pat[2:] 

681 flags = 0 # type:ignore 

682 combo = self.searchOptionsStrings[self.searchOptions] 

683 if combo == "All": 

684 hNodes = self.c.all_positions() 

685 elif combo == "Subtree": 

686 hNodes = self.c.p.self_and_subtree() 

687 else: 

688 hNodes = [self.c.p] 

689 hm = self.find_h(hpat, hNodes, flags) 

690 

691 self.clear() # needed for external client ui replacement: fills self.its 

692 self.addHeadlineMatches(hm) # added for external client ui replacement: fills self.its 

693 

694 # bm = self.c.find_b(bpat, flags) 

695 # self.addBodyMatches(bm) 

696 return hm, [] 

697 # self.lw.insertItem(0, "%d hits"%self.lw.count()) 

698 #@+node:felix.20220225003906.16: *3* find_h 

699 def find_h(self, regex, nodes, flags=re.IGNORECASE): 

700 """ 

701 Return list (a PosList) of all nodes where zero or more characters at 

702 the beginning of the headline match regex 

703 """ 

704 res = PosList() 

705 try: 

706 pat = re.compile(regex, flags) 

707 except Exception: 

708 return res 

709 for p in nodes: 

710 m = re.match(pat, p.h) 

711 if m: 

712 # #2012: Don't inject pc.mo. 

713 pc = p.copy() 

714 res.append(pc) 

715 return res 

716 #@+node:felix.20220313185430.1: *3* find_tag 

717 def find_tag(self, pat): 

718 """ 

719 Return list (a PosList) of all nodes that have matching tags 

720 """ 

721 # USE update_list(self) from @file ../plugins/nodetags.py 

722 c = self.c 

723 

724 res = PosList() 

725 

726 tc = getattr(c, 'theTagController', None) 

727 gnxDict = c.fileCommands.gnxDict 

728 key = pat.strip() 

729 

730 query = re.split(r'(&|\||-|\^)', key) 

731 tags = [] 

732 operations = [] 

733 for i, s in enumerate(query): 

734 if i % 2 == 0: 

735 tags.append(s.strip()) 

736 else: 

737 operations.append(s.strip()) 

738 tags.reverse() 

739 operations.reverse() 

740 

741 resultset = set(tc.get_tagged_gnxes(tags.pop())) 

742 while operations: 

743 op = operations.pop() 

744 nodes = set(tc.get_tagged_gnxes(tags.pop())) 

745 if op == '&': 

746 resultset &= nodes 

747 elif op == '|': 

748 resultset |= nodes 

749 elif op == '-': 

750 resultset -= nodes 

751 elif op == '^': 

752 resultset ^= nodes 

753 

754 for gnx in resultset: 

755 n = gnxDict.get(gnx) 

756 if n is not None: 

757 p = c.vnode2position(n) 

758 pc = p.copy() 

759 res.append(pc) 

760 # item = QtWidgets.QListWidgetItem(n.h) 

761 # self.listWidget.addItem(item) 

762 # self.mapping[id(item)] = n 

763 # count = self.listWidget.count() 

764 # self.label.clear() 

765 # self.label.setText("Total: %s nodes" % count) 

766 return res 

767 #@+node:felix.20220225003906.17: *3* find_b 

768 def find_b(self, regex, nodes, flags=re.IGNORECASE | re.MULTILINE): 

769 """ 

770 Return list (a PosList) of all nodes whose body matches regex 

771 one or more times. 

772 

773 """ 

774 res = PosList() 

775 try: 

776 pat = re.compile(regex, flags) 

777 except Exception: 

778 return res 

779 for p in nodes: 

780 m = re.finditer(pat, p.b) 

781 t1, t2 = itertools.tee(m, 2) 

782 try: 

783 t1.__next__() 

784 except StopIteration: 

785 continue 

786 pc = p.copy() 

787 pc.matchiter = t2 

788 res.append(pc) 

789 return res 

790 #@+node:felix.20220225003906.18: *3* doShowMarked 

791 def doShowMarked(self): 

792 self.clear() 

793 c = self.c 

794 pl = PosList() 

795 for p in c.all_positions(): 

796 if p.isMarked(): 

797 pl.append(p.copy()) 

798 self.addHeadlineMatches(pl) 

799 #@+node:felix.20220225003906.19: *3* Event handlers 

800 #@+node:felix.20220225003906.20: *4* onSelectItem (quicksearch.py) 

801 def onSelectItem(self, it, it_prev=None): 

802 c = self.c 

803 tgt = self.its.get(it) 

804 if not tgt: 

805 if not g.unitTesting: 

806 print("onSelectItem: no target found for 'it' as key:" + str(it)) 

807 return 

808 

809 # generic callable 

810 try: 

811 if callable(tgt[1]): 

812 tgt() 

813 elif len(tgt[1]) == 2: 

814 p, pos = tgt[1] 

815 if hasattr(p, 'v'): #p might be "Root" 

816 if not c.positionExists(p): 

817 g.es("Node moved or deleted.\nMaybe re-do search.", 

818 color='red') 

819 return 

820 c.selectPosition(p) 

821 if pos is not None: 

822 if hasattr(g.app.gui, 'show_find_success'): # pragma: no cover 

823 g.app.gui.show_find_success(c, False, 0, p) 

824 st, en = pos 

825 w = c.frame.body.wrapper 

826 w.setSelectionRange(st, en) 

827 w.seeInsertPoint() 

828 c.bodyWantsFocus() 

829 c.bodyWantsFocusNow() 

830 else: 

831 if hasattr(g.app.gui, 'show_find_success'): # pragma: no cover 

832 g.app.gui.show_find_success(c, True, 0, p) 

833 except Exception: 

834 raise ServerError("QuickSearchController onSelectItem error") 

835 

836 

837 #@-others 

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

839class LeoServer: 

840 """Leo Server Controller""" 

841 #@+others 

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

843 def __init__(self, testing=False): 

844 

845 import leo.core.leoApp as leoApp 

846 import leo.core.leoBridge as leoBridge 

847 

848 global g 

849 t1 = time.process_time() 

850 # 

851 # Init ivars first. 

852 self.c = None # Currently Selected Commander. 

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

854 self.action = None 

855 self.bad_commands_list = [] # Set below. 

856 # 

857 # Debug utilities 

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

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

860 # 

861 # Start the bridge. 

862 self.bridge = leoBridge.controller( 

863 gui='nullGui', 

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

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

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

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

868 ) 

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

870 g.in_leo_server = True # #2098. 

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

872 self.leoServerConfig = None 

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

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

875 # 

876 # Set in _init_connection 

877 self.web_socket = None # Main Control Client 

878 self.loop = None 

879 # 

880 # To inspect commands 

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

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

883 # 

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

885 g.getScript = self._getScript 

886 g.IdleTime = self._idleTime 

887 # 

888 # override for "revert to file" operation 

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

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

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

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

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

894 # 

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

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

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

898 g.app.idleTimeManager.start() 

899 t2 = time.process_time() 

900 if not testing: 

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

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

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

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

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

906 # Called by many commands in Leo 

907 if message: 

908 s = title + " " + message 

909 else: 

910 s = title 

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

912 g.leoServer._send_async_output(package) 

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

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

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

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

917 # used in revert with title: 'Revert' 

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

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

920 s = "runAskYesNoDialog called" 

921 if title.startswith('Overwrite'): 

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

923 elif title.startswith('Revert'): 

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

925 elif title.startswith('Create'): 

926 s = "myLeoSettings.leo created" 

927 elif title.startswith('Move'): 

928 s = "Marked nodes were moved" 

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

930 g.leoServer._send_async_output(package) 

931 return "yes" 

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

933 def _runAskYesNoCancelDialog(self, c, title, 

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

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

936 ): 

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

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

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

940 s = "runAskYesNoCancelDialog called" 

941 if title.startswith('Overwrite'): 

942 s = "File Overwritten" 

943 elif title.startswith('Confirm'): 

944 s = "File Saved" 

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

946 g.leoServer._send_async_output(package) 

947 return "yes" 

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

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

950 """Output to the Log Pane""" 

951 d = { 

952 'color': None, 

953 'commas': False, 

954 'newline': True, 

955 'spaces': True, 

956 'tabName': 'Log', 

957 'nodeLink': None, 

958 } 

959 d = g.doKeywordArgs(keys, d) 

960 color = d.get('color') 

961 color = g.actualColor(color) 

962 s = g.translateArgs(args, d) 

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

964 if color: 

965 package["color"] = color 

966 self._send_async_output(package, True) 

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

968 def _getScript(self, c, p, 

969 useSelectedText=True, 

970 forcePythonSentinels=True, 

971 useSentinels=True, 

972 ): 

973 """ 

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

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

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

977 """ 

978 w = c.frame.body.wrapper 

979 if not p: 

980 p = c.p 

981 try: 

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

983 s = w.getSelectedText() 

984 else: 

985 s = p.b 

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

987 s = textwrap.dedent(s) 

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

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

990 forcePythonSentinels=forcePythonSentinels, 

991 useSentinels=useSentinels) 

992 except Exception: 

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

994 g.es_exception() 

995 script = '' 

996 return script 

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

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

999 while True: 

1000 await asyncio.sleep(seconds) 

1001 func(self) 

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

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

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

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

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

1007 """Handle a successful find match.""" 

1008 if in_headline: 

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

1010 # no return 

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

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

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

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

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

1016 """ 

1017 Check that a button command is possible. 

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

1019 """ 

1020 c = self._check_c() 

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

1022 if not sc: 

1023 # This will happen unless mod_scripting is loaded! 

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

1025 return sc.buttonsDict 

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

1027 def _get_rclickTree(self, rclicks): 

1028 rclickList = [] 

1029 

1030 for rc in rclicks: 

1031 children = [] 

1032 if rc.children: 

1033 children = self._get_rclickTree(rc.children) 

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

1035 

1036 return rclickList 

1037 

1038 

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

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

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

1042 tag = 'click_button' 

1043 index = param.get("index") 

1044 if not index: 

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

1046 d = self._check_button_command(tag) 

1047 button = None 

1048 for key in d: 

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

1050 if str(key) == index: 

1051 button = key 

1052 

1053 if not button: 

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

1055 

1056 try: 

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

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

1059 # not zero 

1060 toChooseFrom = button.rclicks 

1061 for i_rc in w_rclick: 

1062 w_rclickChosen = toChooseFrom[i_rc] 

1063 toChooseFrom = w_rclickChosen.children 

1064 if w_rclickChosen: 

1065 c = self._check_c() 

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

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

1068 

1069 else: 

1070 button.command() 

1071 except Exception as e: 

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

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

1074 return self._make_response() 

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

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

1077 """ 

1078 Gets the currently opened file's @buttons list 

1079 as an array of dict. 

1080 

1081 Typescript RClick recursive interface: 

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

1083 

1084 Typescript return interface: 

1085 { 

1086 name: string; 

1087 index: string; 

1088 rclicks: RClick[]; 

1089 }[] 

1090 """ 

1091 d = self._check_button_command('get_buttons') 

1092 

1093 buttons = [] 

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

1095 for key in d: 

1096 rclickList = [] 

1097 if hasattr(key, 'rclicks'): 

1098 rclickList = self._get_rclickTree(key.rclicks) 

1099 # buttonRClicks = key.rclicks 

1100 # for rc in buttonRClicks: 

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

1102 

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

1104 buttons.append(entry) 

1105 

1106 return self._make_minimal_response({ 

1107 "buttons": buttons 

1108 }) 

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

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

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

1112 tag = 'remove_button' 

1113 index = param.get("index") 

1114 if not index: 

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

1116 d = self._check_button_command(tag) 

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

1118 key = None 

1119 for i_key in d: 

1120 if str(i_key) == index: 

1121 key = i_key 

1122 if key: 

1123 try: 

1124 del d[key] 

1125 except Exception as e: 

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

1127 else: 

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

1129 

1130 return self._make_response() 

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

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

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

1134 tag = 'goto_script' 

1135 index = param.get("index") 

1136 if not index: 

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

1138 d = self._check_button_command(tag) 

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

1140 key = None 

1141 for i_key in d: 

1142 if str(i_key) == index: 

1143 key = i_key 

1144 if key: 

1145 try: 

1146 gnx = key.command.gnx 

1147 c = self._check_c() 

1148 # pylint: disable=undefined-loop-variable 

1149 for p in c.all_positions(): 

1150 if p.gnx == gnx: 

1151 break 

1152 if p: 

1153 assert c.positionExists(p) 

1154 c.selectPosition(p) 

1155 else: 

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

1157 except Exception as e: 

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

1159 else: 

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

1161 

1162 return self._make_response() 

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

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

1165 def open_file(self, param): 

1166 """ 

1167 Open a leo file with the given filename. 

1168 Create a new document if no name. 

1169 """ 

1170 found, tag = False, 'open_file' 

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

1172 if filename: 

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

1174 if c.fileName() == filename: 

1175 found = True 

1176 if not found: 

1177 c = self.bridge.openLeoFile(filename) 

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

1179 c.findCommands.ftm = StringFindTabManager(c) 

1180 cc = QuickSearchController(c) 

1181 setattr(c, 'scon', cc) # Patch up quick-search controller to the commander 

1182 if not c: # pragma: no cover 

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

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

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

1186 # Assign self.c 

1187 self.c = c 

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

1189 # Check the outline! 

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

1191 self._check_outline(c) 

1192 if self.log_flag: # pragma: no cover 

1193 self._dump_outline(c) 

1194 

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

1196 

1197 return self._make_response(result) 

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

1199 def open_files(self, param): 

1200 """ 

1201 Opens an array of leo files. 

1202 Returns an object with total opened files 

1203 and name of currently last opened & selected document. 

1204 """ 

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

1206 if files: 

1207 for i_file in files: 

1208 if os.path.isfile(i_file): 

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

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

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

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

1213 return self._make_response(result) 

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

1215 def set_opened_file(self, param): 

1216 """ 

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

1218 Returns an object with total opened files 

1219 and name of currently last opened & selected document. 

1220 """ 

1221 tag = 'set_opened_file' 

1222 index = param.get('index') 

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

1224 if total and index < total: 

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

1226 # maybe needed for frame wrapper 

1227 self.c.selectPosition(self.c.p) 

1228 self._check_outline(self.c) 

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

1230 return self._make_response(result) 

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

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

1233 def close_file(self, param): 

1234 """ 

1235 Closes an outline opened with open_file. 

1236 Use a 'forced' flag to force close. 

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

1238 """ 

1239 c = self._check_c() 

1240 forced = param.get("forced") 

1241 if c: 

1242 # First, revert to prevent asking user. 

1243 if forced and c.changed: 

1244 if c.fileName(): 

1245 c.revert() 

1246 else: 

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

1248 # Then, if still possible, close it. 

1249 if forced or not c.changed: 

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

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

1252 else: 

1253 # Cannot close, return empty response without 'total' 

1254 # (ask to save, ignore or cancel) 

1255 return self._make_response() 

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

1257 commanders = g.app.commanders() 

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

1259 if self.c: 

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

1261 else: 

1262 result = {"total": 0} 

1263 return self._make_response(result) 

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

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

1266 """Save the leo outline.""" 

1267 tag = 'save_file' 

1268 c = self._check_c() 

1269 if c: 

1270 try: 

1271 if "name" in param: 

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

1273 else: 

1274 c.save() 

1275 except Exception as e: 

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

1277 print(e, flush=True) 

1278 

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

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

1281 def import_any_file(self, param): 

1282 """ 

1283 Import file(s) from array of file names 

1284 """ 

1285 tag = 'import_any_file' 

1286 c = self._check_c() 

1287 ic = c.importCommands 

1288 names = param.get('filenames') 

1289 if names: 

1290 g.chdir(names[0]) 

1291 if not names: 

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

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

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

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

1296 if derived: 

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

1298 for fn in others: 

1299 junk, ext = g.os_path_splitext(fn) 

1300 ext = ext.lower() # #1522 

1301 if ext.startswith('.'): 

1302 ext = ext[1:] 

1303 if ext == 'csv': 

1304 ic.importMindMap([fn]) 

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

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

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

1308 # elif ext == 'json': 

1309 # ic.importJSON([fn]) 

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

1311 ic.importFreeMind([fn]) 

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

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

1314 elif ext == 'more': 

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

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

1317 elif ext == 'txt': 

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

1319 # #1522: Create an @edit node. 

1320 c.import_txt_file(c, fn) 

1321 else: 

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

1323 last = c.lastTopLevel() 

1324 parent = last.insertAfter() 

1325 parent.v.h = 'Imported Files' 

1326 ic.importFilesCommand( 

1327 files=[fn], 

1328 parent=parent, 

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

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

1331 ) 

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

1333 #@+node:felix.20220309010334.1: *4* server.nav commands 

1334 #@+node:felix.20220305211743.1: *5* server.nav_headline_search 

1335 def nav_headline_search(self, param): 

1336 """ 

1337 Performs nav 'headline only' search and fills results of go to panel 

1338 Triggered by just typing in nav input box 

1339 """ 

1340 tag = 'nav_headline_search' 

1341 c = self._check_c() 

1342 # Tag search override! 

1343 try: 

1344 inp = c.scon.navText 

1345 if c.scon.isTag: 

1346 c.scon.doTag(inp) 

1347 else: 

1348 exp = inp.replace(" ", "*") 

1349 c.scon.bgSearch(exp) 

1350 except Exception as e: 

1351 raise ServerError(f"{tag}: exception doing nav headline search: {e}") 

1352 return self._make_response() 

1353 

1354 

1355 #@+node:felix.20220305211828.1: *5* server.nav_search 

1356 def nav_search(self, param): 

1357 """ 

1358 Performs nav search and fills results of go to panel 

1359 Triggered by pressing 'Enter' in the nav input box 

1360 """ 

1361 tag = 'nav_search' 

1362 c = self._check_c() 

1363 # Tag search override! 

1364 try: 

1365 inp = c.scon.navText 

1366 if c.scon.isTag: 

1367 c.scon.doTag(inp) 

1368 else: 

1369 c.scon.doSearch(inp) 

1370 except Exception as e: 

1371 raise ServerError(f"{tag}: exception doing nav search: {e}") 

1372 return self._make_response() 

1373 

1374 

1375 #@+node:felix.20220305215239.1: *5* server.get_goto_panel 

1376 def get_goto_panel(self, param): 

1377 """ 

1378 Gets the content of the goto panel 

1379 """ 

1380 tag = 'get_goto_panel' 

1381 c = self._check_c() 

1382 try: 

1383 result: Dict[str, Any] = {} 

1384 navlist = [ 

1385 { 

1386 "key": k, 

1387 "h": c.scon.its[k][0]["label"], 

1388 "t": c.scon.its[k][0]["type"] 

1389 } for k in c.scon.its.keys() 

1390 ] 

1391 result["navList"] = navlist 

1392 result["messages"] = c.scon.lw 

1393 result["navText"] = c.scon.navText 

1394 result["navOptions"] = {"isTag": c.scon.isTag, "showParents": c.scon.showParents} 

1395 except Exception as e: 

1396 raise ServerError(f"{tag}: exception doing nav search: {e}") 

1397 return self._make_response(result) 

1398 

1399 

1400 #@+node:felix.20220309010558.1: *5* server.find_quick_timeline 

1401 def find_quick_timeline(self, param): 

1402 # fill with timeline order gnx nodes 

1403 c = self._check_c() 

1404 c.scon.doTimeline() 

1405 return self._make_response() 

1406 

1407 #@+node:felix.20220309010607.1: *5* server.find_quick_changed 

1408 def find_quick_changed(self, param): 

1409 # fill with list of all dirty nodes 

1410 c = self._check_c() 

1411 c.scon.doChanged() 

1412 return self._make_response() 

1413 

1414 #@+node:felix.20220309010647.1: *5* server.find_quick_history 

1415 def find_quick_history(self, param): 

1416 # fill with list from history 

1417 c = self._check_c() 

1418 c.scon.doNodeHistory() 

1419 return self._make_response() 

1420 

1421 #@+node:felix.20220309010704.1: *5* server.find_quick_marked 

1422 def find_quick_marked(self, param): 

1423 # fill with list of marked nodes 

1424 c = self._check_c() 

1425 c.scon.doShowMarked() 

1426 return self._make_response() 

1427 

1428 #@+node:felix.20220309205509.1: *5* server.goto_nav_entry 

1429 def goto_nav_entry(self, param): 

1430 # activate entry in c.scon.its 

1431 tag = 'goto_nav_entry' 

1432 c = self._check_c() 

1433 # c.scon.doTimeline() 

1434 try: 

1435 it = param.get('key') 

1436 c.scon.onSelectItem(it) 

1437 focus = self._get_focus() 

1438 result = {"focus": focus} 

1439 except Exception as e: 

1440 raise ServerError(f"{tag}: exception selecting a nav entry: {e}") 

1441 return self._make_response(result) 

1442 

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

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

1445 def get_search_settings(self, param): 

1446 """ 

1447 Gets search options 

1448 """ 

1449 tag = 'get_search_settings' 

1450 c = self._check_c() 

1451 try: 

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

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

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

1455 result["searchSettings"]["nav_text"] = c.scon.navText 

1456 result["searchSettings"]["show_parents"] = c.scon.showParents 

1457 result["searchSettings"]["is_tag"] = c.scon.isTag 

1458 result["searchSettings"]["search_options"] = c.scon.searchOptions 

1459 except Exception as e: 

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

1461 return self._make_response(result) 

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

1463 def set_search_settings(self, param): 

1464 """ 

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

1466 """ 

1467 tag = 'set_search_settings' 

1468 c = self._check_c() 

1469 find = c.findCommands 

1470 ftm = c.findCommands.ftm 

1471 searchSettings = param.get('searchSettings') 

1472 if not searchSettings: 

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

1474 # Try to set the search settings 

1475 try: 

1476 # nav settings 

1477 c.scon.navText = searchSettings.get('nav_text') 

1478 c.scon.showParents = searchSettings.get('show_parents') 

1479 c.scon.isTag = searchSettings.get('is_tag') 

1480 c.scon.searchOptions = searchSettings.get('search_options') 

1481 

1482 # Find/change text boxes. 

1483 table = ( 

1484 ('find_findbox', 'find_text', ''), 

1485 ('find_replacebox', 'change_text', ''), 

1486 ) 

1487 for widget_ivar, setting_name, default in table: 

1488 w = getattr(ftm, widget_ivar) 

1489 s = searchSettings.get(setting_name) or default 

1490 w.clear() 

1491 w.insert(s) 

1492 # Check boxes. 

1493 table2 = ( 

1494 ('ignore_case', 'check_box_ignore_case'), 

1495 ('mark_changes', 'check_box_mark_changes'), 

1496 ('mark_finds', 'check_box_mark_finds'), 

1497 ('pattern_match', 'check_box_regexp'), 

1498 ('search_body', 'check_box_search_body'), 

1499 ('search_headline', 'check_box_search_headline'), 

1500 ('whole_word', 'check_box_whole_word'), 

1501 ) 

1502 for setting_name, widget_ivar in table2: 

1503 w = getattr(ftm, widget_ivar) 

1504 val = searchSettings.get(setting_name) 

1505 setattr(find, setting_name, val) 

1506 if val != w.isChecked(): 

1507 w.toggle() 

1508 # Radio buttons 

1509 table3 = ( 

1510 ('node_only', 'node_only', 'radio_button_node_only'), 

1511 ('entire_outline', None, 'radio_button_entire_outline'), 

1512 ('suboutline_only', 'suboutline_only', 'radio_button_suboutline_only'), 

1513 ) 

1514 for setting_name, ivar, widget_ivar in table3: 

1515 w = getattr(ftm, widget_ivar) 

1516 val = searchSettings.get(setting_name, False) 

1517 if ivar is not None: 

1518 assert hasattr(find, setting_name), setting_name 

1519 setattr(find, setting_name, val) 

1520 if val != w.isChecked(): 

1521 w.toggle() 

1522 # Ensure one radio button is set. 

1523 w = ftm.radio_button_entire_outline 

1524 nodeOnly = searchSettings.get('node_only', False) 

1525 suboutlineOnly = searchSettings.get('suboutline_only', False) 

1526 if not nodeOnly and not suboutlineOnly: 

1527 setattr(find, 'entire_outline', True) 

1528 if not w.isChecked(): 

1529 w.toggle() 

1530 else: 

1531 setattr(find, 'entire_outline', False) 

1532 if w.isChecked(): 

1533 w.toggle() 

1534 except Exception as e: 

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

1536 # Confirm by sending back the settings to the client 

1537 try: 

1538 settings = ftm.get_settings() 

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

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

1541 except Exception as e: 

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

1543 return self._make_response(result) 

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

1545 def find_all(self, param): 

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

1547 tag = 'find_all' 

1548 c = self._check_c() 

1549 fc = c.findCommands 

1550 try: 

1551 settings = fc.ftm.get_settings() 

1552 result = fc.do_find_all(settings) 

1553 except Exception as e: 

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

1555 focus = self._get_focus() 

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

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

1558 def find_next(self, param): 

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

1560 tag = 'find_next' 

1561 c = self._check_c() 

1562 p = c.p 

1563 fc = c.findCommands 

1564 fromOutline = param.get("fromOutline") 

1565 fromBody = not fromOutline 

1566 # 

1567 focus = self._get_focus() 

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

1569 inBody = not inOutline 

1570 # 

1571 if fromOutline and inBody: 

1572 fc.in_headline = True 

1573 elif fromBody and inOutline: 

1574 fc.in_headline = False 

1575 c.bodyWantsFocus() 

1576 c.bodyWantsFocusNow() 

1577 # 

1578 if fc.in_headline: 

1579 ins = len(p.h) 

1580 gui_w = c.edit_widget(p) 

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

1582 # 

1583 try: 

1584 # Let cursor as-is 

1585 settings = fc.ftm.get_settings() 

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

1587 except Exception as e: 

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

1589 # 

1590 # get focus again after the operation 

1591 focus = self._get_focus() 

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

1593 "newpos": newpos, "focus": focus} 

1594 return self._make_response(result) 

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

1596 def find_previous(self, param): 

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

1598 tag = 'find_previous' 

1599 c = self._check_c() 

1600 p = c.p 

1601 fc = c.findCommands 

1602 fromOutline = param.get("fromOutline") 

1603 fromBody = not fromOutline 

1604 # 

1605 focus = self._get_focus() 

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

1607 inBody = not inOutline 

1608 # 

1609 if fromOutline and inBody: 

1610 fc.in_headline = True 

1611 elif fromBody and inOutline: 

1612 fc.in_headline = False 

1613 # w = c.frame.body.wrapper 

1614 c.bodyWantsFocus() 

1615 c.bodyWantsFocusNow() 

1616 # 

1617 if fc.in_headline: 

1618 gui_w = c.edit_widget(p) 

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

1620 # 

1621 try: 

1622 # set widget cursor pos to 0 if in headline 

1623 settings = fc.ftm.get_settings() 

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

1625 except Exception as e: 

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

1627 # 

1628 # get focus again after the operation 

1629 focus = self._get_focus() 

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

1631 "newpos": newpos, "focus": focus} 

1632 return self._make_response(result) 

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

1634 def replace(self, param): 

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

1636 tag = 'replace' 

1637 c = self._check_c() 

1638 fc = c.findCommands 

1639 try: 

1640 settings = fc.ftm.get_settings() 

1641 fc.change(settings) 

1642 except Exception as e: 

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

1644 focus = self._get_focus() 

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

1646 return self._make_response(result) 

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

1648 def replace_then_find(self, param): 

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

1650 tag = 'replace_then_find' 

1651 c = self._check_c() 

1652 fc = c.findCommands 

1653 try: 

1654 settings = fc.ftm.get_settings() 

1655 result = fc.do_change_then_find(settings) 

1656 except Exception as e: 

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

1658 focus = self._get_focus() 

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

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

1661 def replace_all(self, param): 

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

1663 tag = 'replace_all' 

1664 c = self._check_c() 

1665 fc = c.findCommands 

1666 try: 

1667 settings = fc.ftm.get_settings() 

1668 result = fc.do_change_all(settings) 

1669 except Exception as e: 

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

1671 focus = self._get_focus() 

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

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

1674 def clone_find_all(self, param): 

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

1676 tag = 'clone_find_all' 

1677 c = self._check_c() 

1678 fc = c.findCommands 

1679 try: 

1680 settings = fc.ftm.get_settings() 

1681 result = fc.do_clone_find_all(settings) 

1682 except Exception as e: 

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

1684 focus = self._get_focus() 

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

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

1687 def clone_find_all_flattened(self, param): 

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

1689 tag = 'clone_find_all_flattened' 

1690 c = self._check_c() 

1691 fc = c.findCommands 

1692 try: 

1693 settings = fc.ftm.get_settings() 

1694 result = fc.do_clone_find_all_flattened(settings) 

1695 except Exception as e: 

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

1697 focus = self._get_focus() 

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

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

1700 def find_var(self, param): 

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

1702 tag = 'find_var' 

1703 c = self._check_c() 

1704 fc = c.findCommands 

1705 try: 

1706 fc.find_var() 

1707 except Exception as e: 

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

1709 focus = self._get_focus() 

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

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

1712 def clone_find_all_flattened_marked(self, param): 

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

1714 tag = 'clone_find_all_flattened_marked' 

1715 c = self._check_c() 

1716 fc = c.findCommands 

1717 try: 

1718 fc.do_find_marked(flatten=True) 

1719 except Exception as e: 

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

1721 focus = self._get_focus() 

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

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

1724 def clone_find_all_marked(self, param): 

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

1726 tag = 'clone_find_all_marked' 

1727 c = self._check_c() 

1728 fc = c.findCommands 

1729 try: 

1730 fc.do_find_marked(flatten=False) 

1731 except Exception as e: 

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

1733 focus = self._get_focus() 

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

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

1736 def find_def(self, param): 

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

1738 tag = 'find_def' 

1739 c = self._check_c() 

1740 fc = c.findCommands 

1741 try: 

1742 fc.find_def() 

1743 except Exception as e: 

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

1745 focus = self._get_focus() 

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

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

1748 def goto_global_line(self, param): 

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

1750 tag = 'goto_global_line' 

1751 c = self._check_c() 

1752 gc = c.gotoCommands 

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

1754 try: 

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

1756 except Exception as e: 

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

1758 focus = self._get_focus() 

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

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

1761 def clone_find_tag(self, param): 

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

1763 tag = 'clone_find_tag' 

1764 c = self._check_c() 

1765 fc = c.findCommands 

1766 tag_param = param.get("tag") 

1767 if not tag_param: # pragma: no cover 

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

1769 settings = fc.ftm.get_settings() 

1770 if self.log_flag: # pragma: no cover 

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

1772 n, p = fc.do_clone_find_tag(tag_param) 

1773 if self.log_flag: # pragma: no cover 

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

1775 print('', flush=True) 

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

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

1778 def tag_children(self, param): 

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

1780 # This is not a find command! 

1781 tag = 'tag_children' 

1782 c = self._check_c() 

1783 fc = c.findCommands 

1784 tag_param = param.get("tag") 

1785 if tag_param is None: # pragma: no cover 

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

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

1788 fc.do_tag_children(c.p, tag_param) 

1789 return self._make_response() 

1790 #@+node:felix.20220313215348.1: *5* server.tag_node 

1791 def tag_node(self, param): 

1792 """Set tag on selected node""" 

1793 # This is not a find command! 

1794 tag = 'tag_node' 

1795 c = self._check_c() 

1796 tag_param = param.get("tag") 

1797 if tag_param is None: # pragma: no cover 

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

1799 try: 

1800 p = self._get_p(param) 

1801 tc = getattr(c, 'theTagController', None) 

1802 if hasattr(tc, 'add_tag'): 

1803 tc.add_tag(p, tag_param) 

1804 except Exception as e: 

1805 raise ServerError(f"{tag}: Running tag_node gave exception: {e}") 

1806 return self._make_response() 

1807 #@+node:felix.20220313215353.1: *5* server.remove_tag 

1808 def remove_tag(self, param): 

1809 """Remove specific tag on selected node""" 

1810 # This is not a find command! 

1811 tag = 'remove_tag' 

1812 c = self._check_c() 

1813 tag_param = param.get("tag") 

1814 if tag_param is None: # pragma: no cover 

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

1816 try: 

1817 p = self._get_p(param) 

1818 v = p.v 

1819 tc = getattr(c, 'theTagController', None) 

1820 if v.u and '__node_tags' in v.u: 

1821 tc.remove_tag(p, tag_param) 

1822 except Exception as e: 

1823 raise ServerError(f"{tag}: Running remove_tag gave exception: {e}") 

1824 return self._make_response() 

1825 #@+node:felix.20220313220807.1: *5* server.remove_tags 

1826 def remove_tags(self, param): 

1827 """Remove all tags on selected node""" 

1828 # This is not a find command! 

1829 tag = 'remove_tags' 

1830 c = self._check_c() 

1831 try: 

1832 p = self._get_p(param) 

1833 v = p.v 

1834 if v.u and '__node_tags' in v.u: 

1835 del v.u['__node_tags'] 

1836 tc = getattr(c, 'theTagController', None) 

1837 tc.initialize_taglist() # reset tag list: some may have been removed 

1838 except Exception as e: 

1839 raise ServerError(f"{tag}: Running remove_tags gave exception: {e}") 

1840 return self._make_response() 

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

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

1843 def get_all_open_commanders(self, param): 

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

1845 files = [ 

1846 { 

1847 "changed": c.isChanged(), 

1848 "name": c.fileName(), 

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

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

1851 ] 

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

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

1854 def get_all_positions(self, param): 

1855 """ 

1856 Return a list of position data for all positions. 

1857 

1858 Useful as a sanity check for debugging. 

1859 """ 

1860 c = self._check_c() 

1861 result = [ 

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

1863 ] 

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

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

1866 def get_all_gnx(self, param): 

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

1868 if self.log_flag: # pragma: no cover 

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

1870 c = self._check_c() 

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

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

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

1874 def get_body(self, param): 

1875 """ 

1876 Return the body content body specified via GNX. 

1877 """ 

1878 c = self._check_c() 

1879 gnx = param.get("gnx") 

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

1881 body = "" 

1882 if v: 

1883 body = v.b or "" 

1884 # Support asking for unknown gnx when client switches rapidly 

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

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

1887 def get_body_length(self, param): 

1888 """ 

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

1890 """ 

1891 c = self._check_c() 

1892 gnx = param.get("gnx") 

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

1894 if w_v: 

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

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

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

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

1899 def get_body_states(self, param): 

1900 """ 

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

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

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

1904 """ 

1905 c = self._check_c() 

1906 p = self._get_p(param) 

1907 wrapper = c.frame.body.wrapper 

1908 

1909 def row_col_wrapper_dict(i): 

1910 if not i: 

1911 i = 0 # prevent none type 

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

1913 # g.convertPythonIndexToRowCol instead ! 

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

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

1916 

1917 def row_col_pv_dict(i, s): 

1918 if not i: 

1919 i = 0 # prevent none type 

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

1921 # g.convertPythonIndexToRowCol instead ! 

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

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

1924 

1925 # Get the language. 

1926 aList = g.get_directives_dict_list(p) 

1927 d = g.scanAtCommentAndAtLanguageDirectives(aList) 

1928 language = ( 

1929 d and d.get('language') 

1930 or g.getLanguageFromAncestorAtFileNode(p) 

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

1932 or 'plain' 

1933 ) 

1934 # Get the body wrap state 

1935 wrap = g.scanAllAtWrapDirectives(c, p) 

1936 tabWidth = g.scanAllAtTabWidthDirectives(c, p) 

1937 if not isinstance(tabWidth, int): 

1938 tabWidth = False 

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

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

1941 insert = wrapper.getInsertPoint() 

1942 start, end = wrapper.getSelectionRange(True) 

1943 scroll = wrapper.getYScrollPosition() 

1944 states = { 

1945 'language': language.lower(), 

1946 'wrap': wrap, 

1947 'tabWidth': tabWidth, 

1948 'selection': { 

1949 "gnx": p.v.gnx, 

1950 "scroll": scroll, 

1951 "insert": row_col_wrapper_dict(insert), 

1952 "start": row_col_wrapper_dict(start), 

1953 "end": row_col_wrapper_dict(end) 

1954 } 

1955 } 

1956 else: # pragma: no cover 

1957 insert = p.v.insertSpot 

1958 start = p.v.selectionStart 

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

1960 scroll = p.v.scrollBarSpot 

1961 states = { 

1962 'language': language.lower(), 

1963 'wrap': wrap, 

1964 'tabWidth': tabWidth, 

1965 'selection': { 

1966 "gnx": p.v.gnx, 

1967 "scroll": scroll, 

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

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

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

1971 } 

1972 } 

1973 return self._make_minimal_response(states) 

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

1975 def get_children(self, param): 

1976 """ 

1977 Return the node data for children of p, 

1978 where p is root if param.ap is missing 

1979 """ 

1980 c = self._check_c() 

1981 children = [] # default empty array 

1982 if param.get("ap"): 

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

1984 # _get_p called with the strict=True parameter because 

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

1986 p = self._get_p(param, True) 

1987 if p and p.hasChildren(): 

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

1989 else: 

1990 if c.hoistStack: 

1991 # Always start hoisted tree with single hoisted root node 

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

1993 else: 

1994 # this outputs all Root Children 

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

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

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

1998 def get_focus(self, param): 

1999 """ 

2000 Return a representation of the focused widget, 

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

2002 """ 

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

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

2005 def get_parent(self, param): 

2006 """ 

2007 Return the node data for the parent of position p, 

2008 where p is c.p if param["ap"] is missing. 

2009 """ 

2010 self._check_c() 

2011 p = self._get_p(param) 

2012 parent = p.parent() 

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

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

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

2016 def get_position_data(self, param): 

2017 """ 

2018 Return a dict of position data for all positions. 

2019 

2020 Useful as a sanity check for debugging. 

2021 """ 

2022 c = self._check_c() 

2023 result = { 

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

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

2026 } 

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

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

2029 def get_ua(self, param): 

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

2031 self._check_c() 

2032 p = self._get_p(param) 

2033 try: 

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

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

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

2037 except Exception: # pragma: no cover 

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

2039 # _make_response adds all the cheap redraw data. 

2040 return self._make_response(response) 

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

2042 def get_ui_states(self, param): 

2043 """ 

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

2045 """ 

2046 c = self._check_c() 

2047 tag = 'get_ui_states' 

2048 try: 

2049 states = { 

2050 "changed": c and c.changed, 

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

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

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

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

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

2056 } 

2057 except Exception as e: # pragma: no cover 

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

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

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

2061 def get_undos(self, param): 

2062 """Return list of undo operations""" 

2063 c = self._check_c() 

2064 undoer = c.undoer 

2065 undos = [] 

2066 try: 

2067 for bead in undoer.beads: 

2068 undos.append(bead.undoType) 

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

2070 except Exception: # pragma: no cover 

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

2072 # _make_response adds all the cheap redraw data. 

2073 return self._make_minimal_response(response) 

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

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

2076 def clone_node(self, param): 

2077 """ 

2078 Clone a node. 

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

2080 """ 

2081 c = self._check_c() 

2082 p = self._get_p(param) 

2083 if p == c.p: 

2084 c.clone() 

2085 else: 

2086 oldPosition = c.p 

2087 c.selectPosition(p) 

2088 c.clone() 

2089 if c.positionExists(oldPosition): 

2090 c.selectPosition(oldPosition) 

2091 # return selected node either ways 

2092 return self._make_response() 

2093 

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

2095 def contract_node(self, param): 

2096 """ 

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

2098 """ 

2099 p = self._get_p(param) 

2100 p.contract() 

2101 return self._make_response() 

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

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

2104 """ 

2105 Copy a node, don't select it. 

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

2107 """ 

2108 c = self._check_c() 

2109 p = self._get_p(param) 

2110 if p == c.p: 

2111 s = c.fileCommands.outline_to_clipboard_string() 

2112 else: 

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

2114 c.selectPosition(p) 

2115 s = c.fileCommands.outline_to_clipboard_string() 

2116 if c.positionExists(oldPosition): 

2117 # select if old position still valid 

2118 c.selectPosition(oldPosition) 

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

2120 

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

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

2123 """ 

2124 Cut a node, don't select it. 

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

2126 """ 

2127 c = self._check_c() 

2128 p = self._get_p(param) 

2129 if p == c.p: 

2130 s = c.fileCommands.outline_to_clipboard_string() 

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

2132 else: 

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

2134 c.selectPosition(p) 

2135 s = c.fileCommands.outline_to_clipboard_string() 

2136 c.cutOutline() 

2137 if c.positionExists(oldPosition): 

2138 # select if old position still valid 

2139 c.selectPosition(oldPosition) 

2140 else: 

2141 oldPosition._childIndex = oldPosition._childIndex - 1 

2142 # Try again with childIndex decremented 

2143 if c.positionExists(oldPosition): 

2144 # additional try with lowered childIndex 

2145 c.selectPosition(oldPosition) 

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

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

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

2149 """ 

2150 Delete a node, don't select it. 

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

2152 """ 

2153 c = self._check_c() 

2154 p = self._get_p(param) 

2155 if p == c.p: 

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

2157 else: 

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

2159 c.selectPosition(p) 

2160 c.deleteOutline() 

2161 if c.positionExists(oldPosition): 

2162 # select if old position still valid 

2163 c.selectPosition(oldPosition) 

2164 else: 

2165 oldPosition._childIndex = oldPosition._childIndex - 1 

2166 # Try again with childIndex decremented 

2167 if c.positionExists(oldPosition): 

2168 # additional try with lowered childIndex 

2169 c.selectPosition(oldPosition) 

2170 return self._make_response() 

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

2172 def expand_node(self, param): 

2173 """ 

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

2175 """ 

2176 p = self._get_p(param) 

2177 p.expand() 

2178 return self._make_response() 

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

2180 def insert_node(self, param): 

2181 """ 

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

2183 """ 

2184 c = self._check_c() 

2185 p = self._get_p(param) 

2186 c.selectPosition(p) 

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

2188 return self._make_response() 

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

2190 def insert_child_node(self, param): 

2191 """ 

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

2193 """ 

2194 c = self._check_c() 

2195 p = self._get_p(param) 

2196 c.selectPosition(p) 

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

2198 return self._make_response() 

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

2200 def insert_named_node(self, param): 

2201 """ 

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

2203 """ 

2204 c = self._check_c() 

2205 p = self._get_p(param) 

2206 newHeadline = param.get('name') 

2207 bunch = c.undoer.beforeInsertNode(p) 

2208 newNode = p.insertAfter() 

2209 # set this node's new headline 

2210 newNode.h = newHeadline 

2211 newNode.setDirty() 

2212 c.setChanged() 

2213 c.undoer.afterInsertNode( 

2214 newNode, 'Insert Node', bunch) 

2215 c.selectPosition(newNode) 

2216 c.setChanged() 

2217 return self._make_response() 

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

2219 def insert_child_named_node(self, param): 

2220 """ 

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

2222 """ 

2223 c = self._check_c() 

2224 p = self._get_p(param) 

2225 newHeadline = param.get('name') 

2226 bunch = c.undoer.beforeInsertNode(p) 

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

2228 newNode = p.insertAsLastChild() 

2229 else: 

2230 newNode = p.insertAsNthChild(0) 

2231 # set this node's new headline 

2232 newNode.h = newHeadline 

2233 newNode.setDirty() 

2234 c.setChanged() 

2235 c.undoer.afterInsertNode( 

2236 newNode, 'Insert Node', bunch) 

2237 c.selectPosition(newNode) 

2238 return self._make_response() 

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

2240 def page_down(self, param): 

2241 """ 

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

2243 """ 

2244 c = self._check_c() 

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

2246 for z in range(n): 

2247 c.selectVisNext() 

2248 return self._make_response() 

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

2250 def page_up(self, param): 

2251 """ 

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

2253 """ 

2254 c = self._check_c() 

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

2256 for z in range(n): 

2257 c.selectVisBack() 

2258 return self._make_response() 

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

2260 def paste_node(self, param): 

2261 """ 

2262 Pastes a node, 

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

2264 """ 

2265 tag = 'paste_node' 

2266 c = self._check_c() 

2267 p = self._get_p(param) 

2268 s = param.get('name') 

2269 if s is None: # pragma: no cover 

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

2271 if p == c.p: 

2272 c.pasteOutline(s=s) 

2273 else: 

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

2275 c.selectPosition(p) 

2276 c.pasteOutline(s=s) 

2277 if c.positionExists(oldPosition): 

2278 # select if old position still valid 

2279 c.selectPosition(oldPosition) 

2280 else: 

2281 oldPosition._childIndex = oldPosition._childIndex + 1 

2282 # Try again with childIndex incremented 

2283 if c.positionExists(oldPosition): 

2284 # additional try with higher childIndex 

2285 c.selectPosition(oldPosition) 

2286 return self._make_response() 

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

2288 def paste_as_clone_node(self, param): 

2289 """ 

2290 Pastes a node as a clone, 

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

2292 """ 

2293 tag = 'paste_as_clone_node' 

2294 c = self._check_c() 

2295 p = self._get_p(param) 

2296 s = param.get('name') 

2297 if s is None: # pragma: no cover 

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

2299 if p == c.p: 

2300 c.pasteOutlineRetainingClones(s=s) 

2301 else: 

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

2303 c.selectPosition(p) 

2304 c.pasteOutlineRetainingClones(s=s) 

2305 if c.positionExists(oldPosition): 

2306 # select if old position still valid 

2307 c.selectPosition(oldPosition) 

2308 else: 

2309 oldPosition._childIndex = oldPosition._childIndex + 1 

2310 # Try again with childIndex incremented 

2311 if c.positionExists(oldPosition): 

2312 # additional try with higher childIndex 

2313 c.selectPosition(oldPosition) 

2314 return self._make_response() 

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

2316 def redo(self, param): 

2317 """Undo last un-doable operation""" 

2318 c = self._check_c() 

2319 u = c.undoer 

2320 if u.canRedo(): 

2321 u.redo() 

2322 return self._make_response() 

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

2324 def set_body(self, param): 

2325 """ 

2326 Undoably set body text of a v node. 

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

2328 """ 

2329 tag = 'set_body' 

2330 c = self._check_c() 

2331 gnx = param.get('gnx') 

2332 body = param.get('body') 

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

2334 if body is None: # pragma: no cover 

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

2336 for p in c.all_positions(): 

2337 if p.v.gnx == gnx: 

2338 if body == p.v.b: 

2339 return self._make_response() 

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

2341 bunch = u.beforeChangeNodeContents(p) 

2342 p.v.setBodyString(body) 

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

2344 if c.p == p: 

2345 wrapper.setAllText(body) 

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

2347 c.setChanged() 

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

2349 p.setDirty() 

2350 break 

2351 # additional forced string setting 

2352 if gnx: 

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

2354 if v: 

2355 v.b = body 

2356 return self._make_response() 

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

2358 def set_current_position(self, param): 

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

2360 tag = "set_current_position" 

2361 c = self._check_c() 

2362 p = self._get_p(param) 

2363 if p: 

2364 if c.positionExists(p): 

2365 # set this node as selection 

2366 c.selectPosition(p) 

2367 else: 

2368 ap = param.get('ap') 

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

2370 if foundPNode: 

2371 c.selectPosition(foundPNode) 

2372 else: 

2373 print( 

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

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

2376 

2377 return self._make_response() 

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

2379 def set_headline(self, param): 

2380 """ 

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

2382 """ 

2383 c = self._check_c() 

2384 p = self._get_p(param) 

2385 u = c.undoer 

2386 h: str = param.get('name', '') 

2387 oldH: str = p.h 

2388 if h == oldH: 

2389 return self._make_response() 

2390 bunch = u.beforeChangeNodeContents(p) 

2391 p.setDirty() 

2392 c.setChanged() 

2393 p.h = h 

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

2395 return self._make_response() 

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

2397 def set_selection(self, param): 

2398 """ 

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

2400 

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

2402 

2403 Package has these keys: 

2404 

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

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

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

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

2409 - "scroll": An optional scroll position. 

2410 

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

2412 or as numbers directly for convenience. 

2413 """ 

2414 c = self._check_c() 

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

2416 v = p.v 

2417 wrapper = c.frame.body.wrapper 

2418 convert = g.convertRowColToPythonIndex 

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

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

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

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

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

2424 if isinstance(active, int): 

2425 insert = active 

2426 startSel = start 

2427 endSel = end 

2428 else: 

2429 # otherwise convert from line+col data. 

2430 insert = convert( 

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

2432 startSel = convert( 

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

2434 endSel = convert( 

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

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

2437 if p == c.p: 

2438 wrapper.setSelectionRange(startSel, endSel, insert) 

2439 wrapper.setYScrollPosition(scroll) 

2440 # Always set vnode attrs. 

2441 v.scrollBarSpot = scroll 

2442 v.insertSpot = insert 

2443 v.selectionStart = startSel 

2444 v.selectionLength = abs(startSel - endSel) 

2445 return self._make_response() 

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

2447 def set_ua_member(self, param): 

2448 """ 

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

2450 """ 

2451 self._check_c() 

2452 p = self._get_p(param) 

2453 name = param.get('name') 

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

2455 if not p.v.u: 

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

2457 if name and isinstance(name, str): 

2458 p.v.u[name] = value 

2459 return self._make_response() 

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

2461 def set_ua(self, param): 

2462 """ 

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

2464 """ 

2465 self._check_c() 

2466 p = self._get_p(param) 

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

2468 p.v.u = ua 

2469 return self._make_response() 

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

2471 def toggle_mark(self, param): 

2472 """ 

2473 Toggle the mark at position p. 

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

2475 """ 

2476 c = self._check_c() 

2477 p = self._get_p(param) 

2478 if p == c.p: 

2479 c.markHeadline() 

2480 else: 

2481 oldPosition = c.p 

2482 c.selectPosition(p) 

2483 c.markHeadline() 

2484 if c.positionExists(oldPosition): 

2485 c.selectPosition(oldPosition) 

2486 # return selected node either ways 

2487 return self._make_response() 

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

2489 def mark_node(self, param): 

2490 """ 

2491 Mark a node. 

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

2493 """ 

2494 # pylint: disable=no-else-return 

2495 self._check_c() 

2496 p = self._get_p(param) 

2497 if p.isMarked(): 

2498 return self._make_response() 

2499 else: 

2500 return self.toggle_mark(param) 

2501 

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

2503 def unmark_node(self, param): 

2504 """ 

2505 Unmark a node. 

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

2507 """ 

2508 # pylint: disable=no-else-return 

2509 self._check_c() 

2510 p = self._get_p(param) 

2511 if not p.isMarked(): 

2512 return self._make_response() 

2513 else: 

2514 return self.toggle_mark(param) 

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

2516 def undo(self, param): 

2517 """Undo last un-doable operation""" 

2518 c = self._check_c() 

2519 u = c.undoer 

2520 if u.canUndo(): 

2521 u.undo() 

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

2523 return self._make_response() 

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

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

2526 def get_version(self, param): 

2527 """ 

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

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

2530 """ 

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

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

2533 return self._make_minimal_response(result) 

2534 #@+node:felix.20220326190000.1: *5* server.get_leoid 

2535 def get_leoid(self, param): 

2536 """ 

2537 returns g.app.leoID 

2538 """ 

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

2540 result = {"leoID": g.app.leoID} 

2541 return self._make_minimal_response(result) 

2542 #@+node:felix.20220326190008.1: *5* server.set_leoid 

2543 def set_leoid(self, param): 

2544 """ 

2545 sets g.app.leoID 

2546 """ 

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

2548 leoID = param.get('leoID', '') 

2549 # Same test/fix as in Leo 

2550 if leoID: 

2551 try: 

2552 leoID = leoID.replace('.', '').replace(',', '').replace('"', '').replace("'", '') 

2553 # Remove *all* whitespace: https://stackoverflow.com/questions/3739909 

2554 leoID = ''.join(leoID.split()) 

2555 except Exception: 

2556 g.es_exception() 

2557 leoID = 'None' 

2558 if len(leoID) > 2: 

2559 g.app.leoID = leoID 

2560 g.app.nodeIndices.defaultId = leoID 

2561 g.app.nodeIndices.userId = leoID 

2562 return self._make_response() 

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

2564 def do_nothing(self, param): 

2565 """Simply return states from _make_response""" 

2566 return self._make_response() 

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

2568 def set_ask_result(self, param): 

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

2570 tag = "set_ask_result" 

2571 result = param.get("result") 

2572 if not result: 

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

2574 g.app.externalFilesController.clientResult(result) 

2575 return self._make_response() 

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

2577 def set_config(self, param): 

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

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

2580 return self._make_response() 

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

2582 def error(self, param): 

2583 """For unit testing. Raise ServerError""" 

2584 raise ServerError("error called") 

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

2586 def get_all_leo_commands(self, param): 

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

2588 tag = 'get_all_leo_commands' 

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

2590 c = self.c 

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

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

2593 good_names = self._good_commands() 

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

2595 if duplicates: # pragma: no cover 

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

2597 for z in sorted(duplicates): 

2598 print(z, flush=True) 

2599 result = [] 

2600 for command_name in sorted(d): 

2601 func = d.get(command_name) 

2602 if not func: # pragma: no cover 

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

2604 continue 

2605 if command_name in bad_names: # #92. 

2606 continue 

2607 doc = func.__doc__ or '' 

2608 result.append({ 

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

2610 "detail": doc, 

2611 }) 

2612 if self.log_flag: # pragma: no cover 

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

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

2615 print('', flush=True) 

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

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

2618 def _bad_commands(self, c): 

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

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

2621 bad = [] 

2622 # 

2623 # leoInteg #173: Remove only vim commands. 

2624 for command_name in sorted(d): 

2625 if command_name.startswith(':'): 

2626 bad.append(command_name) 

2627 # 

2628 # Remove other commands. 

2629 # This is a hand-curated list. 

2630 bad_list = [ 

2631 'demangle-recent-files', 

2632 'clean-main-spell-dict', 

2633 'clean-persistence', 

2634 'clean-recent-files', 

2635 'clean-spellpyx', 

2636 'clean-user-spell-dict', 

2637 'clear-recent-files', 

2638 'delete-first-icon', 

2639 'delete-last-icon', 

2640 'delete-node-icons', 

2641 'insert-icon', 

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

2643 'export-headlines', # export TODO 

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

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

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

2647 'remove-sentinels', # import TODO 

2648 

2649 'save-all', 

2650 'save-file-as-zipped', 

2651 'write-file-from-node', 

2652 'edit-setting', 

2653 'edit-shortcut', 

2654 'goto-line', 

2655 'pdb', 

2656 'xdb', 

2657 'compare-two-leo-files', 

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

2659 'edit-recent-files', 

2660 'exit-leo', 

2661 'help', # To do. 

2662 'help-for-abbreviations', 

2663 'help-for-autocompletion', 

2664 'help-for-bindings', 

2665 'help-for-command', 

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

2667 'help-for-debugging-commands', 

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

2669 'help-for-dynamic-abbreviations', 

2670 'help-for-find-commands', 

2671 'help-for-keystroke', 

2672 'help-for-minibuffer', 

2673 'help-for-python', 

2674 'help-for-regular-expressions', 

2675 'help-for-scripting', 

2676 'help-for-settings', 

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

2678 

2679 'print-body', 

2680 'print-cmd-docstrings', 

2681 'print-expanded-body', 

2682 'print-expanded-html', 

2683 'print-html', 

2684 'print-marked-bodies', 

2685 'print-marked-html', 

2686 'print-marked-nodes', 

2687 'print-node', 

2688 'print-sep', 

2689 'print-tree-bodies', 

2690 'print-tree-html', 

2691 'print-tree-nodes', 

2692 'print-window-state', 

2693 'quit-leo', 

2694 'reload-style-sheets', 

2695 'save-buffers-kill-leo', 

2696 'screen-capture-5sec', 

2697 'screen-capture-now', 

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

2699 'show-style-sheet', 

2700 'sort-recent-files', 

2701 'view-lossage', 

2702 

2703 # Buffers commands (Usage?) 

2704 'buffer-append-to', 

2705 'buffer-copy', 

2706 'buffer-insert', 

2707 'buffer-kill', 

2708 'buffer-prepend-to', 

2709 'buffer-switch-to', 

2710 'buffers-list', 

2711 'buffers-list-alphabetically', 

2712 

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

2714 # 'ekr-projects', 

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

2716 'leo-dist-leo', 

2717 'leo-docs-leo', 

2718 'leo-plugins-leo', 

2719 'leo-py-leo', 

2720 'leo-quickstart-leo', 

2721 'leo-scripts-leo', 

2722 'leo-unittest-leo', 

2723 

2724 # 'scripts', 

2725 'settings', 

2726 

2727 'open-cheat-sheet-leo', 

2728 'cheat-sheet-leo', 

2729 'cheat-sheet', 

2730 'open-desktop-integration-leo', 

2731 'desktop-integration-leo', 

2732 'open-leo-dist-leo', 

2733 'leo-dist-leo', 

2734 'open-leo-docs-leo', 

2735 'leo-docs-leo', 

2736 'open-leo-plugins-leo', 

2737 'leo-plugins-leo', 

2738 'open-leo-py-leo', 

2739 'leo-py-leo', 

2740 'open-leo-py-ref-leo', 

2741 'leo-py-ref-leo', 

2742 'open-leo-py', 

2743 'open-leo-settings', 

2744 'open-leo-settings-leo', 

2745 'open-local-settings', 

2746 'my-leo-settings', 

2747 'open-my-leo-settings', 

2748 'open-my-leo-settings-leo', 

2749 'leo-settings' 

2750 'open-quickstart-leo', 

2751 'leo-quickstart-leo' 

2752 'open-scripts-leo', 

2753 'leo-scripts-leo' 

2754 'open-unittest-leo', 

2755 'leo-unittest-leo', 

2756 

2757 # Open other places... 

2758 'desktop-integration-leo', 

2759 

2760 'open-offline-tutorial', 

2761 'open-online-home', 

2762 'open-online-toc', 

2763 'open-online-tutorials', 

2764 'open-online-videos', 

2765 'open-recent-file', 

2766 'open-theme-file', 

2767 'open-url', 

2768 'open-url-under-cursor', 

2769 'open-users-guide', 

2770 

2771 # Diffs - needs open file dialog 

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

2773 'diff-leo-files', 

2774 

2775 # --- ORIGINAL BAD COMMANDS START HERE --- 

2776 # Abbreviations... 

2777 'abbrev-kill-all', 

2778 'abbrev-list', 

2779 'dabbrev-completion', 

2780 'dabbrev-expands', 

2781 

2782 # Autocompletion... 

2783 'auto-complete', 

2784 'auto-complete-force', 

2785 'disable-autocompleter', 

2786 'disable-calltips', 

2787 'enable-autocompleter', 

2788 'enable-calltips', 

2789 

2790 # Debugger... 

2791 'debug', 

2792 'db-again', 

2793 'db-b', 

2794 'db-c', 

2795 'db-h', 

2796 'db-input', 

2797 'db-l', 

2798 'db-n', 

2799 'db-q', 

2800 'db-r', 

2801 'db-s', 

2802 'db-status', 

2803 'db-w', 

2804 

2805 # File operations... 

2806 'directory-make', 

2807 'directory-remove', 

2808 'file-delete', 

2809 'file-diff-files', 

2810 'file-insert', 

2811 #'file-new', 

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

2813 

2814 # All others... 

2815 'shell-command', 

2816 'shell-command-on-region', 

2817 'cheat-sheet', 

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

2819 #'find-clone-all', 

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

2821 #'find-clone-tag', 

2822 #'find-all', 

2823 'find-all-unique-regex', 

2824 'find-character', 

2825 'find-character-extend-selection', 

2826 #'find-next', 

2827 #'find-prev', 

2828 'find-word', 

2829 'find-word-in-line', 

2830 

2831 'global-search', 

2832 

2833 'isearch-backward', 

2834 'isearch-backward-regexp', 

2835 'isearch-forward', 

2836 'isearch-forward-regexp', 

2837 'isearch-with-present-options', 

2838 

2839 #'replace', 

2840 #'replace-all', 

2841 'replace-current-character', 

2842 #'replace-then-find', 

2843 

2844 're-search-backward', 

2845 're-search-forward', 

2846 

2847 #'search-backward', 

2848 #'search-forward', 

2849 'search-return-to-origin', 

2850 

2851 'set-find-everywhere', 

2852 'set-find-node-only', 

2853 'set-find-suboutline-only', 

2854 'set-replace-string', 

2855 'set-search-string', 

2856 

2857 #'show-find-options', 

2858 

2859 #'start-search', 

2860 

2861 'toggle-find-collapses-nodes', 

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

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

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

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

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

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

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

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

2870 

2871 'word-search-backward', 

2872 'word-search-forward', 

2873 

2874 # Buttons... 

2875 'delete-script-button-button', 

2876 

2877 # Clicks... 

2878 'click-click-box', 

2879 'click-icon-box', 

2880 'ctrl-click-at-cursor', 

2881 'ctrl-click-icon', 

2882 'double-click-icon-box', 

2883 'right-click-icon', 

2884 

2885 # Editors... 

2886 'add-editor', 'editor-add', 

2887 'delete-editor', 'editor-delete', 

2888 'detach-editor-toggle', 

2889 'detach-editor-toggle-max', 

2890 

2891 # Focus... 

2892 'cycle-editor-focus', 'editor-cycle-focus', 

2893 'focus-to-body', 

2894 'focus-to-find', 

2895 'focus-to-log', 

2896 'focus-to-minibuffer', 

2897 'focus-to-nav', 

2898 'focus-to-spell-tab', 

2899 'focus-to-tree', 

2900 

2901 'tab-cycle-next', 

2902 'tab-cycle-previous', 

2903 'tab-detach', 

2904 

2905 # Headlines.. 

2906 'abort-edit-headline', 

2907 'edit-headline', 

2908 'end-edit-headline', 

2909 

2910 # Layout and panes... 

2911 'adoc', 

2912 'adoc-with-preview', 

2913 

2914 'contract-body-pane', 

2915 'contract-log-pane', 

2916 'contract-outline-pane', 

2917 

2918 'edit-pane-csv', 

2919 'edit-pane-test-open', 

2920 'equal-sized-panes', 

2921 'expand-log-pane', 

2922 'expand-body-pane', 

2923 'expand-outline-pane', 

2924 

2925 'free-layout-context-menu', 

2926 'free-layout-load', 

2927 'free-layout-restore', 

2928 'free-layout-zoom', 

2929 

2930 'zoom-in', 

2931 'zoom-out', 

2932 

2933 # Log 

2934 'clear-log', 

2935 

2936 # Menus... 

2937 'activate-cmds-menu', 

2938 'activate-edit-menu', 

2939 'activate-file-menu', 

2940 'activate-help-menu', 

2941 'activate-outline-menu', 

2942 'activate-plugins-menu', 

2943 'activate-window-menu', 

2944 'context-menu-open', 

2945 'menu-shortcut', 

2946 

2947 # Modes... 

2948 'clear-extend-mode', 

2949 

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

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

2952 #'contract-node', 

2953 #'contract-parent', 

2954 

2955 # Scrolling... 

2956 'scroll-down-half-page', 

2957 'scroll-down-line', 

2958 'scroll-down-page', 

2959 'scroll-outline-down-line', 

2960 'scroll-outline-down-page', 

2961 'scroll-outline-left', 

2962 'scroll-outline-right', 

2963 'scroll-outline-up-line', 

2964 'scroll-outline-up-page', 

2965 'scroll-up-half-page', 

2966 'scroll-up-line', 

2967 'scroll-up-page', 

2968 

2969 # Windows... 

2970 'about-leo', 

2971 

2972 'cascade-windows', 

2973 'close-others', 

2974 'close-window', 

2975 

2976 'iconify-frame', 

2977 

2978 'find-tab-hide', 

2979 #'find-tab-open', 

2980 

2981 'hide-body-dock', 

2982 'hide-body-pane', 

2983 'hide-invisibles', 

2984 'hide-log-pane', 

2985 'hide-outline-dock', 

2986 'hide-outline-pane', 

2987 'hide-tabs-dock', 

2988 

2989 'minimize-all', 

2990 

2991 'resize-to-screen', 

2992 

2993 'show-body-dock', 

2994 'show-hide-body-dock', 

2995 'show-hide-outline-dock', 

2996 'show-hide-render-dock', 

2997 'show-hide-tabs-dock', 

2998 'show-tabs-dock', 

2999 'clean-diff', 

3000 'cm-external-editor', 

3001 

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

3003 'delete-trace-statements', 

3004 

3005 'disable-idle-time-events', 

3006 

3007 'enable-idle-time-events', 

3008 'enter-quick-command-mode', 

3009 'exit-named-mode', 

3010 

3011 'F6-open-console', 

3012 

3013 'flush-lines', 

3014 'full-command', 

3015 

3016 'get-child-headlines', 

3017 

3018 'history', 

3019 

3020 'insert-file-name', 

3021 

3022 'justify-toggle-auto', 

3023 

3024 'keep-lines', 

3025 'keyboard-quit', 

3026 

3027 'line-number', 

3028 'line-numbering-toggle', 

3029 'line-to-headline', 

3030 

3031 'marked-list', 

3032 

3033 'mode-help', 

3034 

3035 'open-python-window', 

3036 

3037 'open-with-idle', 

3038 'open-with-open-office', 

3039 'open-with-scite', 

3040 'open-with-word', 

3041 

3042 'recolor', 

3043 'redraw', 

3044 

3045 'repeat-complex-command', 

3046 

3047 'session-clear', 

3048 'session-create', 

3049 'session-refresh', 

3050 'session-restore', 

3051 'session-snapshot-load', 

3052 'session-snapshot-save', 

3053 

3054 'set-colors', 

3055 'set-command-state', 

3056 'set-comment-column', 

3057 'set-extend-mode', 

3058 'set-fill-column', 

3059 'set-fill-prefix', 

3060 'set-font', 

3061 'set-insert-state', 

3062 'set-overwrite-state', 

3063 'set-silent-mode', 

3064 

3065 'show-buttons', 

3066 'show-calltips', 

3067 'show-calltips-force', 

3068 'show-color-names', 

3069 'show-color-wheel', 

3070 'show-commands', 

3071 'show-file-line', 

3072 

3073 'show-focus', 

3074 'show-fonts', 

3075 

3076 'show-invisibles', 

3077 'show-node-uas', 

3078 'show-outline-dock', 

3079 'show-plugin-handlers', 

3080 'show-plugins-info', 

3081 'show-settings', 

3082 'show-settings-outline', 

3083 'show-spell-info', 

3084 'show-stats', 

3085 'show-tips', 

3086 

3087 'style-set-selected', 

3088 

3089 'suspend', 

3090 

3091 'toggle-abbrev-mode', 

3092 'toggle-active-pane', 

3093 'toggle-angle-brackets', 

3094 'toggle-at-auto-at-edit', 

3095 'toggle-autocompleter', 

3096 'toggle-calltips', 

3097 'toggle-case-region', 

3098 'toggle-extend-mode', 

3099 'toggle-idle-time-events', 

3100 'toggle-input-state', 

3101 'toggle-invisibles', 

3102 'toggle-line-numbering-root', 

3103 'toggle-sparse-move', 

3104 'toggle-split-direction', 

3105 

3106 'what-line', 

3107 'eval', 

3108 'eval-block', 

3109 'eval-last', 

3110 'eval-last-pretty', 

3111 'eval-replace', 

3112 

3113 'find-quick', 

3114 'find-quick-changed', 

3115 'find-quick-selected', 

3116 'find-quick-test-failures', 

3117 'find-quick-timeline', 

3118 

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

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

3121 

3122 'preview', 

3123 'preview-body', 

3124 'preview-expanded-body', 

3125 'preview-expanded-html', 

3126 'preview-html', 

3127 'preview-marked-bodies', 

3128 'preview-marked-html', 

3129 'preview-marked-nodes', 

3130 'preview-node', 

3131 'preview-tree-bodies', 

3132 'preview-tree-html', 

3133 'preview-tree-nodes', 

3134 

3135 'spell-add', 

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

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

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

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

3140 'spell-change', 

3141 'spell-change-then-find', 

3142 'spell-find', 

3143 'spell-ignore', 

3144 'spell-tab-hide', 

3145 'spell-tab-open', 

3146 

3147 #'tag-children', 

3148 

3149 'todo-children-todo', 

3150 'todo-dec-pri', 

3151 'todo-find-todo', 

3152 'todo-fix-datetime', 

3153 'todo-inc-pri', 

3154 

3155 'vr', 

3156 'vr-contract', 

3157 'vr-expand', 

3158 'vr-hide', 

3159 'vr-lock', 

3160 'vr-pause-play-movie', 

3161 'vr-show', 

3162 'vr-toggle', 

3163 'vr-unlock', 

3164 'vr-update', 

3165 'vr-zoom', 

3166 

3167 'vs-create-tree', 

3168 'vs-dump', 

3169 'vs-reset', 

3170 'vs-update', 

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

3172 'add-comments', 

3173 'add-space-to-lines', 

3174 'add-tab-to-lines', 

3175 'align-eq-signs', 

3176 

3177 'back-char', 

3178 'back-char-extend-selection', 

3179 'back-page', 

3180 'back-page-extend-selection', 

3181 'back-paragraph', 

3182 'back-paragraph-extend-selection', 

3183 'back-sentence', 

3184 'back-sentence-extend-selection', 

3185 'back-to-home', 

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

3187 'back-to-indentation', 

3188 'back-word', 

3189 'back-word-extend-selection', 

3190 'back-word-smart', 

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

3192 'backward-delete-char', 

3193 'backward-delete-word', 

3194 'backward-delete-word-smart', 

3195 'backward-find-character', 

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

3197 'backward-kill-paragraph', 

3198 'backward-kill-sentence', 

3199 'backward-kill-word', 

3200 'beginning-of-buffer', 

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

3202 'beginning-of-line', 

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

3204 

3205 'capitalize-word', 

3206 'center-line', 

3207 'center-region', 

3208 'clean-all-blank-lines', 

3209 'clean-all-lines', 

3210 'clean-body', 

3211 'clean-lines', 

3212 'clear-kill-ring', 

3213 'clear-selected-text', 

3214 'convert-blanks', 

3215 'convert-tabs', 

3216 'copy-text', 

3217 'cut-text', 

3218 

3219 'delete-char', 

3220 'delete-comments', 

3221 'delete-indentation', 

3222 'delete-spaces', 

3223 'delete-word', 

3224 'delete-word-smart', 

3225 'downcase-region', 

3226 'downcase-word', 

3227 

3228 'end-of-buffer', 

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

3230 'end-of-line', 

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

3232 

3233 'exchange-point-mark', 

3234 

3235 'extend-to-line', 

3236 'extend-to-paragraph', 

3237 'extend-to-sentence', 

3238 'extend-to-word', 

3239 

3240 'fill-paragraph', 

3241 'fill-region', 

3242 'fill-region-as-paragraph', 

3243 

3244 'finish-of-line', 

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

3246 

3247 'forward-char', 

3248 'forward-char-extend-selection', 

3249 'forward-end-word', 

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

3251 'forward-page', 

3252 'forward-page-extend-selection', 

3253 'forward-paragraph', 

3254 'forward-paragraph-extend-selection', 

3255 'forward-sentence', 

3256 'forward-sentence-extend-selection', 

3257 'forward-word', 

3258 'forward-word-extend-selection', 

3259 'forward-word-smart', 

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

3261 

3262 'go-anywhere', 

3263 'go-back', 

3264 'go-forward', 

3265 'goto-char', 

3266 

3267 'indent-region', 

3268 'indent-relative', 

3269 'indent-rigidly', 

3270 'indent-to-comment-column', 

3271 

3272 'insert-hard-tab', 

3273 'insert-newline', 

3274 'insert-parentheses', 

3275 'insert-soft-tab', 

3276 

3277 'kill-line', 

3278 'kill-paragraph', 

3279 'kill-pylint', 

3280 'kill-region', 

3281 'kill-region-save', 

3282 'kill-sentence', 

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

3284 'kill-word', 

3285 'kill-ws', 

3286 

3287 'match-brackets', 

3288 

3289 'move-lines-down', 

3290 'move-lines-up', 

3291 'move-past-close', 

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

3293 

3294 'newline-and-indent', 

3295 'next-line', 

3296 'next-line-extend-selection', 

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

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

3299 

3300 'previous-line', 

3301 'previous-line-extend-selection', 

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

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

3304 

3305 'rectangle-clear', 

3306 'rectangle-close', 

3307 'rectangle-delete', 

3308 'rectangle-kill', 

3309 'rectangle-open', 

3310 'rectangle-string', 

3311 'rectangle-yank', 

3312 

3313 'remove-blank-lines', 

3314 'remove-newlines', 

3315 'remove-space-from-lines', 

3316 'remove-tab-from-lines', 

3317 

3318 'reverse-region', 

3319 'reverse-sort-lines', 

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

3321 

3322 'paste-text', 

3323 'pop-cursor', 

3324 'push-cursor', 

3325 

3326 'select-all', 

3327 'select-next-trace-statement', 

3328 'select-to-matching-bracket', 

3329 

3330 'sort-columns', 

3331 'sort-fields', 

3332 'sort-lines', 

3333 'sort-lines-ignoring-case', 

3334 

3335 'split-defs', 

3336 'split-line', 

3337 

3338 'start-of-line', 

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

3340 

3341 'tabify', 

3342 'transpose-chars', 

3343 'transpose-lines', 

3344 'transpose-words', 

3345 

3346 'unformat-paragraph', 

3347 'unindent-region', 

3348 

3349 'untabify', 

3350 

3351 'upcase-region', 

3352 'upcase-word', 

3353 'update-ref-file', 

3354 

3355 'yank', 

3356 'yank-pop', 

3357 

3358 'zap-to-character', 

3359 

3360 ] 

3361 bad.extend(bad_list) 

3362 result = list(sorted(bad)) 

3363 return result 

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

3365 def _good_commands(self): 

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

3367 good_list = [ 

3368 

3369 'contract-all', 

3370 'contract-all-other-nodes', 

3371 'clone-node', 

3372 'copy-node', 

3373 'copy-marked-nodes', 

3374 'cut-node', 

3375 

3376 'de-hoist', 

3377 'delete-marked-nodes', 

3378 'delete-node', 

3379 # 'demangle-recent-files', 

3380 'demote', 

3381 'do-nothing', 

3382 'expand-and-go-right', 

3383 'expand-next-level', 

3384 'expand-node', 

3385 'expand-or-go-right', 

3386 'expand-prev-level', 

3387 'expand-to-level-1', 

3388 'expand-to-level-2', 

3389 'expand-to-level-3', 

3390 'expand-to-level-4', 

3391 'expand-to-level-5', 

3392 'expand-to-level-6', 

3393 'expand-to-level-7', 

3394 'expand-to-level-8', 

3395 'expand-to-level-9', 

3396 'expand-all', 

3397 'expand-all-subheads', 

3398 'expand-ancestors-only', 

3399 

3400 'find-next-clone', 

3401 

3402 'goto-first-node', 

3403 'goto-first-sibling', 

3404 'goto-first-visible-node', 

3405 'goto-last-node', 

3406 'goto-last-sibling', 

3407 'goto-last-visible-node', 

3408 'goto-next-changed', 

3409 'goto-next-clone', 

3410 'goto-next-marked', 

3411 'goto-next-node', 

3412 'goto-next-sibling', 

3413 'goto-next-visible', 

3414 'goto-parent', 

3415 'goto-prev-marked', 

3416 'goto-prev-node', 

3417 'goto-prev-sibling', 

3418 'goto-prev-visible', 

3419 

3420 'hoist', 

3421 

3422 'insert-node', 

3423 'insert-node-before', 

3424 'insert-as-first-child', 

3425 'insert-as-last-child', 

3426 'insert-child', 

3427 

3428 'mark', 

3429 'mark-changed-items', 

3430 'mark-first-parents', 

3431 'mark-subheads', 

3432 

3433 'move-marked-nodes', 

3434 'move-outline-down', 

3435 'move-outline-left', 

3436 'move-outline-right', 

3437 'move-outline-up', 

3438 

3439 'paste-node', 

3440 'paste-retaining-clones', 

3441 'promote', 

3442 'promote-bodies', 

3443 'promote-headlines', 

3444 

3445 'sort-children', 

3446 'sort-siblings', 

3447 

3448 'tangle', 

3449 'tangle-all', 

3450 'tangle-marked', 

3451 

3452 'unmark-all', 

3453 'unmark-first-parents', 

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

3455 #'clean-persistence', 

3456 #'clean-recent-files', 

3457 #'clean-spellpyx', 

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

3459 

3460 'clear-all-caches', 

3461 'clear-all-hoists', 

3462 'clear-all-uas', 

3463 'clear-cache', 

3464 'clear-node-uas', 

3465 #'clear-recent-files', 

3466 

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

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

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

3470 

3471 'dump-caches', 

3472 'dump-clone-parents', 

3473 'dump-expanded', 

3474 'dump-node', 

3475 'dump-outline', 

3476 

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

3478 

3479 #'set-ua', 

3480 

3481 'show-all-uas', 

3482 'show-bindings', 

3483 'show-clone-ancestors', 

3484 'show-clone-parents', 

3485 

3486 # Export files... 

3487 #'export-headlines', # export 

3488 #'export-jupyter-notebook', # export 

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

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

3491 #'remove-sentinels', # import 

3492 'typescript-to-py', 

3493 

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

3495 'import-MORE-files', 

3496 'import-file', 

3497 'import-free-mind-files', 

3498 'import-jupyter-notebook', 

3499 'import-legacy-external-files', 

3500 'import-mind-jet-files', 

3501 'import-tabbed-files', 

3502 'import-todo-text-files', 

3503 'import-zim-folder', 

3504 

3505 # Read outlines... 

3506 'read-at-auto-nodes', 

3507 'read-at-file-nodes', 

3508 'read-at-shadow-nodes', 

3509 'read-file-into-node', 

3510 'read-outline-only', 

3511 'read-ref-file', 

3512 

3513 # Save Files. 

3514 'file-save', 

3515 'file-save-as', 

3516 'file-save-by-name', 

3517 'file-save-to', 

3518 'save', 

3519 'save-as', 

3520 'save-file', 

3521 'save-file-as', 

3522 'save-file-by-name', 

3523 'save-file-to', 

3524 'save-to', 

3525 

3526 # Write parts of outlines... 

3527 'write-at-auto-nodes', 

3528 'write-at-file-nodes', 

3529 'write-at-shadow-nodes', 

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

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

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

3533 'write-edited-recent-files', 

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

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

3536 'write-outline-only', 

3537 

3538 'clone-find-all', 

3539 'clone-find-all-flattened', 

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

3541 'clone-find-all-marked', 

3542 'clone-find-parents', 

3543 'clone-find-tag', 

3544 'clone-marked-nodes', 

3545 'clone-node-to-last-node', 

3546 

3547 'clone-to-at-spot', 

3548 

3549 #'edit-setting', 

3550 #'edit-shortcut', 

3551 

3552 'execute-pytest', 

3553 'execute-script', 

3554 'extract', 

3555 'extract-names', 

3556 

3557 'goto-any-clone', 

3558 'goto-global-line', 

3559 #'goto-line', 

3560 'git-diff', 'gd', 

3561 

3562 'log-kill-listener', 'kill-log-listener', 

3563 'log-listen', 'listen-to-log', 

3564 

3565 'make-stub-files', 

3566 

3567 #'pdb', 

3568 

3569 'redo', 

3570 'rst3', 

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

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

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

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

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

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

3577 'run-tests', 

3578 

3579 'undo', 

3580 

3581 #'xdb', 

3582 

3583 # Beautify, blacken, fstringify... 

3584 'beautify-files', 

3585 'beautify-files-diff', 

3586 'blacken-files', 

3587 'blacken-files-diff', 

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

3589 'diff-beautify-files', 

3590 'diff-fstringify-files', 

3591 #'diff-leo-files', 

3592 'diff-marked-nodes', 

3593 'fstringify-files', 

3594 'fstringify-files-diff', 

3595 'fstringify-files-silent', 

3596 'pretty-print-c', 

3597 'silent-fstringify-files', 

3598 

3599 # All other commands... 

3600 'at-file-to-at-auto', 

3601 

3602 'beautify-c', 

3603 

3604 'cls', 

3605 'c-to-python', 

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

3607 'check-derived-file', 

3608 'check-outline', 

3609 'code-to-rst', 

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

3611 'convert-all-blanks', 

3612 'convert-all-tabs', 

3613 'count-children', 

3614 'count-pages', 

3615 'count-region', 

3616 

3617 #'desktop-integration-leo', 

3618 

3619 #'edit-recent-files', 

3620 #'exit-leo', 

3621 

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

3623 'find-def', 

3624 'find-long-lines', 

3625 'find-missing-docstrings', 

3626 'flake8-files', 

3627 'flatten-outline', 

3628 'flatten-outline-to-node', 

3629 'flatten-script', 

3630 

3631 'gc-collect-garbage', 

3632 'gc-dump-all-objects', 

3633 'gc-dump-new-objects', 

3634 'gc-dump-objects-verbose', 

3635 'gc-show-summary', 

3636 

3637 #'help', # To do. 

3638 #'help-for-abbreviations', 

3639 #'help-for-autocompletion', 

3640 #'help-for-bindings', 

3641 #'help-for-command', 

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

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

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

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

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

3647 #'help-for-keystroke', 

3648 #'help-for-minibuffer', 

3649 #'help-for-python', 

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

3651 #'help-for-scripting', 

3652 #'help-for-settings', 

3653 

3654 'insert-body-time', # ? 

3655 'insert-headline-time', 

3656 'insert-jupyter-toc', 

3657 'insert-markdown-toc', 

3658 

3659 'find-var', 

3660 

3661 #'join-leo-irc', 

3662 'join-node-above', 

3663 'join-node-below', 

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

3665 

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

3667 

3668 'new', 

3669 

3670 'open-outline', 

3671 

3672 'parse-body', 

3673 'parse-json', 

3674 'pandoc', 

3675 'pandoc-with-preview', 

3676 'paste-as-template', 

3677 

3678 #'print-body', 

3679 #'print-cmd-docstrings', 

3680 #'print-expanded-body', 

3681 #'print-expanded-html', 

3682 #'print-html', 

3683 #'print-marked-bodies', 

3684 #'print-marked-html', 

3685 #'print-marked-nodes', 

3686 #'print-node', 

3687 #'print-sep', 

3688 #'print-tree-bodies', 

3689 #'print-tree-html', 

3690 #'print-tree-nodes', 

3691 #'print-window-state', 

3692 

3693 'pyflakes', 

3694 'pylint', 

3695 'pylint-kill', 

3696 'python-to-coffeescript', 

3697 

3698 #'quit-leo', 

3699 

3700 'reformat-body', 

3701 'reformat-paragraph', 

3702 'refresh-from-disk', 

3703 'reload-settings', 

3704 #'reload-style-sheets', 

3705 'revert', 

3706 

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

3708 #'screen-capture-5sec', 

3709 #'screen-capture-now', 

3710 'script-button', # ? 

3711 #'set-reference-file', 

3712 #'show-style-sheet', 

3713 #'sort-recent-files', 

3714 'sphinx', 

3715 'sphinx-with-preview', 

3716 'style-reload', # ? 

3717 

3718 'untangle', 

3719 'untangle-all', 

3720 'untangle-marked', 

3721 

3722 #'view-lossage', # ? 

3723 

3724 'weave', 

3725 

3726 # Dubious commands (to do)... 

3727 'act-on-node', 

3728 

3729 'cfa', 

3730 'cfam', 

3731 'cff', 

3732 'cffm', 

3733 'cft', 

3734 

3735 #'buffer-append-to', 

3736 #'buffer-copy', 

3737 #'buffer-insert', 

3738 #'buffer-kill', 

3739 #'buffer-prepend-to', 

3740 #'buffer-switch-to', 

3741 #'buffers-list', 

3742 #'buffers-list-alphabetically', 

3743 

3744 'chapter-back', 

3745 'chapter-next', 

3746 'chapter-select', 

3747 'chapter-select-main', 

3748 'create-def-list', # ? 

3749 ] 

3750 return good_list 

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

3752 def get_all_server_commands(self, param): 

3753 """ 

3754 Public server method: 

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

3756 """ 

3757 tag = 'get_all_server_commands' 

3758 names = self._get_all_server_commands() 

3759 if self.log_flag: # pragma: no cover 

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

3761 g.printObj(names, tag=tag) 

3762 print('', flush=True) 

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

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

3765 def _get_all_server_commands(self): 

3766 """ 

3767 Private server method: 

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

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

3770 """ 

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

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

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

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

3775 """Begin the connection.""" 

3776 global connectionsTotal 

3777 if connectionsTotal == 1: 

3778 # First connection, so "Master client" setup 

3779 self.web_socket = web_socket 

3780 self.loop = asyncio.get_event_loop() 

3781 else: 

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

3783 pass # nothing for now 

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

3785 def shut_down(self, param): 

3786 """Shut down the server.""" 

3787 tag = 'shut_down' 

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

3789 if n: # pragma: no cover 

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

3791 raise TerminateServer("client requested shut down") 

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

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

3794 def _ap_to_p(self, ap): 

3795 """ 

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

3797 

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

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

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

3801 now inaccessible leo document commander.) 

3802 """ 

3803 tag = '_ap_to_p' 

3804 c = self._check_c() 

3805 gnx_d = c.fileCommands.gnxDict 

3806 try: 

3807 outer_stack = ap.get('stack') 

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

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

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

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

3812 # 

3813 def d_to_childIndex_v(d): 

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

3815 childIndex = d.get('childIndex') 

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

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

3818 try: 

3819 childIndex = int(childIndex) 

3820 except Exception: # pragma: no cover. 

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

3822 gnx = d.get('gnx') 

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

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

3825 v = gnx_d.get(gnx) 

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

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

3828 return childIndex, v 

3829 # 

3830 # Compute p.childIndex and p.v. 

3831 childIndex, v = d_to_childIndex_v(ap) 

3832 # 

3833 # Create p.stack. 

3834 stack = [] 

3835 for stack_d in outer_stack: 

3836 stack_childIndex, stack_v = d_to_childIndex_v(stack_d) 

3837 stack.append((stack_v, stack_childIndex)) 

3838 # 

3839 # Make p and check p. 

3840 p = Position(v, childIndex, stack) 

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

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

3843 except Exception: 

3844 if self.log_flag or traces: 

3845 print( 

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

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

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

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

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

3851 return p 

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

3853 def _check_c(self): 

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

3855 tag = '_check_c' 

3856 c = self.c 

3857 if not c: # pragma: no cover 

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

3859 return c 

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

3861 def _check_outline(self, c): 

3862 """Check self.c for consistency.""" 

3863 # Check that all positions exist. 

3864 self._check_outline_positions(c) 

3865 # Test round-tripping. 

3866 self._test_round_trip_positions(c) 

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

3868 def _check_outline_positions(self, c): 

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

3870 tag = '_check_outline_positions' 

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

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

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

3874 print(message, flush=True) 

3875 self._dump_position(p) 

3876 raise ServerError(message) 

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

3878 def _do_leo_command_by_name(self, command_name, param): 

3879 """ 

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

3881 

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

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

3884 should be re-selected afterward. 

3885 

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

3887 

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

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

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

3891 

3892 """ 

3893 tag = '_do_leo_command_by_name' 

3894 c = self._check_c() 

3895 

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

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

3898 

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

3900 if "keep" in param: 

3901 keepSelection = param["keep"] 

3902 

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

3904 if not func: # pragma: no cover 

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

3906 

3907 p = self._get_p(param) 

3908 try: 

3909 if p == c.p: 

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

3911 else: 

3912 old_p = c.p # preserve old position 

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

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

3915 if keepSelection and c.positionExists(old_p): 

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

3917 c.selectPosition(old_p) 

3918 except Exception as e: 

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

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

3921 # 

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

3923 if self._is_jsonable(value): 

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

3925 return self._make_response() 

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

3927 def _do_leo_function_by_name(self, function_name, param): 

3928 """ 

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

3930 

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

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

3933 should be re-selected afterward. 

3934 

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

3936 

3937 command: the name of a method 

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

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

3940 

3941 """ 

3942 tag = '_do_leo_function_by_name' 

3943 c = self._check_c() 

3944 

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

3946 if "keep" in param: 

3947 keepSelection = param["keep"] 

3948 

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

3950 if not func: # pragma: no cover 

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

3952 

3953 p = self._get_p(param) 

3954 try: 

3955 if p == c.p: 

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

3957 else: 

3958 old_p = c.p # preserve old position 

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

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

3961 if keepSelection and c.positionExists(old_p): 

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

3963 c.selectPosition(old_p) 

3964 except Exception as e: 

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

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

3967 # 

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

3969 if self._is_jsonable(value): 

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

3971 return self._make_response() 

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

3973 def _do_message(self, d): 

3974 """ 

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

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

3977 

3978 "id": A positive integer. 

3979 

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

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

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

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

3984 

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

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

3987 

3988 Return a dict, created by _make_response or _make_minimal_response 

3989 that contains at least an 'id' key. 

3990 

3991 """ 

3992 global traces 

3993 tag = '_do_message' 

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

3995 

3996 # Require "id" and "action" keys 

3997 id_ = d.get("id") 

3998 if id_ is None: # pragma: no cover 

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

4000 action = d.get("action") 

4001 if action is None: # pragma: no cover 

4002 raise ServerError(f"{tag}: no action") 

4003 

4004 # TODO : make/force always an object from the client connected. 

4005 param = d.get('param', {}) # Can be none or a string 

4006 # Set log flag. 

4007 if param: 

4008 self.log_flag = param.get("log") 

4009 pass 

4010 else: 

4011 param = {} 

4012 

4013 # Handle traces. 

4014 if trace and verbose: # pragma: no cover 

4015 g.printObj(d, tag=f"request {id_}") 

4016 print('', flush=True) 

4017 elif trace: # pragma: no cover 

4018 keys = sorted(param.keys()) 

4019 if action == '!set_config': 

4020 keys_s = f"({len(keys)} keys)" 

4021 elif len(keys) > 5: 

4022 keys_s = '\n ' + '\n '.join(keys) 

4023 else: 

4024 keys_s = ', '.join(keys) 

4025 print(f" request {id_:<4} {action:<30} {keys_s}", flush=True) 

4026 

4027 # Set the current_id and action ivars for _make_response. 

4028 self.current_id = id_ 

4029 self.action = action 

4030 

4031 # Execute the requested action. 

4032 if action[0] == "!": 

4033 action = action[1:] # Remove exclamation point "!" 

4034 func = self._do_server_command # Server has this method. 

4035 elif action[0] == '-': 

4036 action = action[1:] # Remove dash "-" 

4037 func = self._do_leo_command_by_name # It's a command name. 

4038 else: 

4039 func = self._do_leo_function_by_name # It's the name of a method in some commander. 

4040 result = func(action, param) 

4041 if result is None: # pragma: no cover 

4042 raise ServerError(f"{tag}: no response: {action!r}") 

4043 return result 

4044 #@+node:felix.20210621233316.86: *4* server._do_server_command 

4045 def _do_server_command(self, action, param): 

4046 tag = '_do_server_command' 

4047 # Disallow hidden methods. 

4048 if action.startswith('_'): # pragma: no cover 

4049 raise ServerError(f"{tag}: action starts with '_': {action!r}") 

4050 # Find and execute the server method. 

4051 func = getattr(self, action, None) 

4052 if not func: 

4053 raise ServerError(f"{tag}: action not found: {action!r}") # pragma: no cover 

4054 if not callable(func): 

4055 raise ServerError(f"{tag}: not callable: {func!r}") # pragma: no cover 

4056 return func(param) 

4057 #@+node:felix.20210621233316.87: *4* server._dump_* 

4058 def _dump_outline(self, c): # pragma: no cover 

4059 """Dump the outline.""" 

4060 tag = '_dump_outline' 

4061 print(f"{tag}: {c.shortFileName()}...\n", flush=True) 

4062 for p in c.all_positions(): 

4063 self._dump_position(p) 

4064 print('', flush=True) 

4065 

4066 def _dump_position(self, p): # pragma: no cover 

4067 level_s = ' ' * 2 * p.level() 

4068 print(f"{level_s}{p.childIndex():2} {p.v.gnx} {p.h}", flush=True) 

4069 #@+node:felix.20210624160812.1: *4* server._emit_signon 

4070 def _emit_signon(self): 

4071 """Simulate the Initial Leo Log Entry""" 

4072 tag = 'emit_signon' 

4073 if self.loop: 

4074 g.app.computeSignon() 

4075 signon = [] 

4076 for z in (g.app.signon, g.app.signon1): 

4077 for z2 in z.split('\n'): 

4078 signon.append(z2.strip()) 

4079 g.es("\n".join(signon)) 

4080 else: 

4081 raise ServerError(f"{tag}: no loop ready for emit_signon") 

4082 #@+node:felix.20210625230236.1: *4* server._get_commander_method 

4083 def _get_commander_method(self, command): 

4084 """ Return the given method (p_command) in the Commands class or subcommanders.""" 

4085 # First, try the commands class. 

4086 c = self._check_c() 

4087 func = getattr(c, command, None) 

4088 if func: 

4089 return func 

4090 # Otherwise, search all subcommanders for the method. 

4091 table = ( # This table comes from c.initObjectIvars. 

4092 'abbrevCommands', 

4093 'bufferCommands', 

4094 'chapterCommands', 

4095 'controlCommands', 

4096 'convertCommands', 

4097 'debugCommands', 

4098 'editCommands', 

4099 'editFileCommands', 

4100 'evalController', 

4101 'gotoCommands', 

4102 'helpCommands', 

4103 'keyHandler', 

4104 'keyHandlerCommands', 

4105 'killBufferCommands', 

4106 'leoCommands', 

4107 'leoTestManager', 

4108 'macroCommands', 

4109 'miniBufferWidget', 

4110 'printingController', 

4111 'queryReplaceCommands', 

4112 'rectangleCommands', 

4113 'searchCommands', 

4114 'spellCommands', 

4115 'vimCommands', # Not likely to be useful. 

4116 ) 

4117 for ivar in table: 

4118 subcommander = getattr(c, ivar, None) 

4119 if subcommander: 

4120 func = getattr(subcommander, command, None) 

4121 if func: 

4122 return func 

4123 return None 

4124 #@+node:felix.20210621233316.91: *4* server._get_focus 

4125 def _get_focus(self): 

4126 """Server helper method to get the focused panel name string""" 

4127 tag = '_get_focus' 

4128 try: 

4129 w = g.app.gui.get_focus() 

4130 focus = g.app.gui.widget_name(w) 

4131 except Exception as e: 

4132 raise ServerError(f"{tag}: exception trying to get the focused widget: {e}") 

4133 return focus 

4134 #@+node:felix.20210621233316.90: *4* server._get_p 

4135 def _get_p(self, param, strict=False): 

4136 """ 

4137 Return _ap_to_p(param["ap"]) or c.p., 

4138 or False if the strict flag is set 

4139 """ 

4140 tag = '_get_ap' 

4141 c = self.c 

4142 if not c: # pragma: no cover 

4143 raise ServerError(f"{tag}: no c") 

4144 

4145 ap = param.get("ap") 

4146 if ap: 

4147 p = self._ap_to_p(ap) # Convertion 

4148 if p: 

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

4150 raise ServerError(f"{tag}: position does not exist. ap: {ap!r}") 

4151 return p # Return the position 

4152 if strict: 

4153 return False 

4154 # Fallback to c.p 

4155 if not c.p: # pragma: no cover 

4156 raise ServerError(f"{tag}: no c.p") 

4157 

4158 return c.p 

4159 #@+node:felix.20210621233316.92: *4* server._get_position_d 

4160 def _get_position_d(self, p): 

4161 """ 

4162 Return a python dict that is adding 

4163 graphical representation data and flags 

4164 to the base 'ap' dict from _p_to_ap. 

4165 (To be used by the connected client GUI.) 

4166 """ 

4167 d = self._p_to_ap(p) 

4168 d['headline'] = p.h 

4169 d['level'] = p.level() 

4170 if p.v.u: 

4171 if g.leoServer.leoServerConfig and g.leoServer.leoServerConfig.get("uAsBoolean", False): 

4172 # uAsBoolean is 'thruthy' 

4173 d['u'] = True 

4174 else: 

4175 # Normal output if no options set 

4176 d['u'] = p.v.u 

4177 if bool(p.b): 

4178 d['hasBody'] = True 

4179 if p.hasChildren(): 

4180 d['hasChildren'] = True 

4181 if p.isCloned(): 

4182 d['cloned'] = True 

4183 if p.isDirty(): 

4184 d['dirty'] = True 

4185 if p.isExpanded(): 

4186 d['expanded'] = True 

4187 if p.isMarked(): 

4188 d['marked'] = True 

4189 if p.isAnyAtFileNode(): 

4190 d['atFile'] = True 

4191 if p == self.c.p: 

4192 d['selected'] = True 

4193 return d 

4194 #@+node:felix.20210705211625.1: *4* server._is_jsonable 

4195 def _is_jsonable(self, x): 

4196 try: 

4197 json.dumps(x, cls=SetEncoder) 

4198 return True 

4199 except(TypeError, OverflowError): 

4200 return False 

4201 #@+node:felix.20210621233316.94: *4* server._make_minimal_response 

4202 def _make_minimal_response(self, package=None): 

4203 """ 

4204 Return a json string representing a response dict. 

4205 

4206 The 'package' kwarg, if present, must be a python dict describing a 

4207 response. package may be an empty dict or None. 

4208 

4209 The 'p' kwarg, if present, must be a position. 

4210 

4211 First, this method creates a response (a python dict) containing all 

4212 the keys in the 'package' dict. 

4213 

4214 Then it adds 'id' to the package. 

4215 

4216 Finally, this method returns the json string corresponding to the 

4217 response. 

4218 """ 

4219 if package is None: 

4220 package = {} 

4221 

4222 # Always add id. 

4223 package["id"] = self.current_id 

4224 

4225 return json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4226 #@+node:felix.20210621233316.93: *4* server._make_response 

4227 def _make_response(self, package=None): 

4228 """ 

4229 Return a json string representing a response dict. 

4230 

4231 The 'package' kwarg, if present, must be a python dict describing a 

4232 response. package may be an empty dict or None. 

4233 

4234 The 'p' kwarg, if present, must be a position. 

4235 

4236 First, this method creates a response (a python dict) containing all 

4237 the keys in the 'package' dict, with the following added keys: 

4238 

4239 - "id": The incoming id. 

4240 - "commander": A dict describing self.c. 

4241 - "node": None, or an archived position describing self.c.p. 

4242 

4243 Finally, this method returns the json string corresponding to the 

4244 response. 

4245 """ 

4246 global traces 

4247 tag = '_make_response' 

4248 trace = self.log_flag or 'response' in traces 

4249 verbose = 'verbose' in traces 

4250 c = self.c # It is valid for c to be None. 

4251 if package is None: 

4252 package = {} 

4253 p = package.get("p") 

4254 if p: 

4255 del package["p"] 

4256 # Raise an *internal* error if checks fail. 

4257 if isinstance(package, str): # pragma: no cover 

4258 raise InternalServerError(f"{tag}: bad package kwarg: {package!r}") 

4259 if p and not isinstance(p, Position): # pragma: no cover 

4260 raise InternalServerError(f"{tag}: bad p kwarg: {p!r}") 

4261 if p and not c: # pragma: no cover 

4262 raise InternalServerError(f"{tag}: p but not c") 

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

4264 raise InternalServerError(f"{tag}: p does not exist: {p!r}") 

4265 if c and not c.p: # pragma: no cover 

4266 raise InternalServerError(f"{tag}: empty c.p") 

4267 

4268 # Always add id 

4269 package["id"] = self.current_id 

4270 

4271 # The following keys are relevant only if there is an open commander. 

4272 if c: 

4273 # Allow commands, especially _get_redraw_d, to specify p! 

4274 p = p or c.p 

4275 package["commander"] = { 

4276 "changed": c.isChanged(), 

4277 "fileName": c.fileName(), # Can be None for new files. 

4278 } 

4279 # Add all the node data, including: 

4280 # - "node": self._p_to_ap(p) # Contains p.gnx, p.childIndex and p.stack. 

4281 # - All the *cheap* redraw data for p. 

4282 redraw_d = self._get_position_d(p) 

4283 package["node"] = redraw_d 

4284 

4285 # Handle traces. 

4286 if trace and verbose: # pragma: no cover 

4287 g.printObj(package, tag=f"response {self.current_id}") 

4288 print('', flush=True) 

4289 elif trace: # pragma: no cover 

4290 keys = sorted(package.keys()) 

4291 keys_s = ', '.join(keys) 

4292 print(f"response {self.current_id:<4} {keys_s}", flush=True) 

4293 

4294 return json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4295 #@+node:felix.20210621233316.95: *4* server._p_to_ap 

4296 def _p_to_ap(self, p): 

4297 """ 

4298 * From Leo plugin leoflexx.py * 

4299 

4300 Convert Leo position p to a serializable archived position. 

4301 

4302 This returns only position-related data. 

4303 get_position_data returns all data needed to redraw the screen. 

4304 """ 

4305 self._check_c() 

4306 stack = [{'gnx': v.gnx, 'childIndex': childIndex} 

4307 for (v, childIndex) in p.stack] 

4308 return { 

4309 'childIndex': p._childIndex, 

4310 'gnx': p.v.gnx, 

4311 'stack': stack, 

4312 } 

4313 #@+node:felix.20210621233316.96: *4* server._positionFromGnx 

4314 def _positionFromGnx(self, gnx): 

4315 """Return first p node with this gnx or false""" 

4316 c = self._check_c() 

4317 for p in c.all_unique_positions(): 

4318 if p.v.gnx == gnx: 

4319 return p 

4320 return False 

4321 #@+node:felix.20210622232409.1: *4* server._send_async_output & helper 

4322 def _send_async_output(self, package, toAll=False): 

4323 """ 

4324 Send data asynchronously to the client 

4325 """ 

4326 tag = "send async output" 

4327 jsonPackage = json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4328 if "async" not in package: 

4329 InternalServerError(f"\n{tag}: async member missing in package {jsonPackage} \n") 

4330 if self.loop: 

4331 self.loop.create_task(self._async_output(jsonPackage, toAll)) 

4332 else: 

4333 InternalServerError(f"\n{tag}: loop not ready {jsonPackage} \n") 

4334 #@+node:felix.20210621233316.89: *5* server._async_output 

4335 async def _async_output(self, json, toAll=False): # pragma: no cover (tested in server) 

4336 """Output json string to the web_socket""" 

4337 global connectionsTotal 

4338 tag = '_async_output' 

4339 outputBytes = bytes(json, 'utf-8') 

4340 if toAll: 

4341 if connectionsPool: # asyncio.wait doesn't accept an empty list 

4342 await asyncio.wait([asyncio.create_task(client.send(outputBytes)) for client in connectionsPool]) 

4343 else: 

4344 g.trace(f"{tag}: no web socket. json: {json!r}") 

4345 else: 

4346 if self.web_socket: 

4347 await self.web_socket.send(outputBytes) 

4348 else: 

4349 g.trace(f"{tag}: no web socket. json: {json!r}") 

4350 #@+node:felix.20210621233316.97: *4* server._test_round_trip_positions 

4351 def _test_round_trip_positions(self, c): # pragma: no cover (tested in client). 

4352 """Test the round tripping of p_to_ap and ap_to_p.""" 

4353 tag = '_test_round_trip_positions' 

4354 for p in c.all_unique_positions(): 

4355 ap = self._p_to_ap(p) 

4356 p2 = self._ap_to_p(ap) 

4357 if p != p2: 

4358 self._dump_outline(c) 

4359 raise ServerError(f"{tag}: round-trip failed: ap: {ap!r}, p: {p!r}, p2: {p2!r}") 

4360 #@+node:felix.20210625002950.1: *4* server._yieldAllRootChildren 

4361 def _yieldAllRootChildren(self): 

4362 """Return all root children P nodes""" 

4363 c = self._check_c() 

4364 p = c.rootPosition() 

4365 while p: 

4366 yield p 

4367 p.moveToNext() 

4368 

4369 #@-others 

4370#@+node:felix.20210621233316.105: ** function: main & helpers 

4371def main(): # pragma: no cover (tested in client) 

4372 """python script for leo integration via leoBridge""" 

4373 # pylint: disable=used-before-assignment 

4374 global websockets 

4375 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile 

4376 if not websockets: 

4377 print('websockets not found') 

4378 print('pip install websockets') 

4379 return 

4380 

4381 #@+others 

4382 #@+node:felix.20210807214524.1: *3* function: cancel_tasks 

4383 def cancel_tasks(to_cancel, loop): 

4384 if not to_cancel: 

4385 return 

4386 

4387 for task in to_cancel: 

4388 task.cancel() 

4389 

4390 loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) 

4391 

4392 for task in to_cancel: 

4393 if task.cancelled(): 

4394 continue 

4395 if task.exception() is not None: 

4396 loop.call_exception_handler( 

4397 { 

4398 "message": "unhandled exception during asyncio.run() shutdown", 

4399 "exception": task.exception(), 

4400 "task": task, 

4401 } 

4402 ) 

4403 #@+node:ekr.20210825115746.1: *3* function: center_tk_frame 

4404 def center_tk_frame(top): 

4405 """Center the top-level Frame.""" 

4406 # https://stackoverflow.com/questions/3352918 

4407 top.update_idletasks() 

4408 screen_width = top.winfo_screenwidth() 

4409 screen_height = top.winfo_screenheight() 

4410 size = tuple(int(_) for _ in top.geometry().split('+')[0].split('x')) 

4411 x = screen_width / 2 - size[0] / 2 

4412 y = screen_height / 2 - size[1] / 2 

4413 top.geometry("+%d+%d" % (x, y)) 

4414 #@+node:felix.20210804130751.1: *3* function: close_server 

4415 def close_Server(): 

4416 """ 

4417 Close the server by stopping the loop 

4418 """ 

4419 print('Closing Leo Server', flush=True) 

4420 if loop.is_running(): 

4421 loop.stop() 

4422 else: 

4423 print('Loop was not running', flush=True) 

4424 #@+node:ekr.20210825172913.1: *3* function: general_yes_no_dialog & helpers 

4425 def general_yes_no_dialog( 

4426 c, 

4427 title, # Not used. 

4428 message=None, # Must exist. 

4429 yesMessage="&Yes", # Not used. 

4430 noMessage="&No", # Not used. 

4431 yesToAllMessage=None, # Not used. 

4432 defaultButton="Yes", # Not used 

4433 cancelMessage=None, # Not used. 

4434 ): 

4435 """ 

4436 Monkey-patched implementation of LeoQtGui.runAskYesNoCancelDialog 

4437 offering *only* Yes/No buttons. 

4438 

4439 This will fallback to a tk implementation if the qt library is unavailable. 

4440 

4441 This raises a dialog and return either 'yes' or 'no'. 

4442 """ 

4443 #@+others # define all helper functions. 

4444 #@+node:ekr.20210801175921.1: *4* function: tk_runAskYesNoCancelDialog & helpers 

4445 def tk_runAskYesNoCancelDialog(c): 

4446 """ 

4447 Tk version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons. 

4448 """ 

4449 if g.unitTesting: 

4450 return None 

4451 root = top = val = None # Non-locals 

4452 #@+others # define helper functions 

4453 #@+node:ekr.20210801180311.4: *5* function: create_yes_no_frame 

4454 def create_yes_no_frame(message, top): 

4455 """Create the dialog's frame.""" 

4456 frame = Tk.Frame(top) 

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

4458 label = Tk.Label(frame, text=message, bg="white") 

4459 label.pack(pady=10) 

4460 # Create buttons. 

4461 f = Tk.Frame(top) 

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

4463 b = Tk.Button(f, width=6, text="Yes", bd=4, underline=0, command=yesButton) 

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

4465 b = Tk.Button(f, width=6, text="No", bd=2, underline=0, command=noButton) 

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

4467 #@+node:ekr.20210801180311.5: *5* function: callbacks 

4468 def noButton(event=None): 

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

4470 nonlocal val 

4471 print(f"Not saved: {c.fileName()}") 

4472 val = "no" 

4473 top.destroy() 

4474 

4475 def yesButton(event=None): 

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

4477 nonlocal val 

4478 print(f"Saved: {c.fileName()}") 

4479 val = "yes" 

4480 top.destroy() 

4481 #@-others 

4482 root = Tk.Tk() 

4483 root.withdraw() 

4484 root.update() 

4485 

4486 top = Tk.Toplevel(root) 

4487 top.title("Saved changed outline?") 

4488 create_yes_no_frame(message, top) 

4489 top.bind("<Return>", yesButton) 

4490 top.bind("y", yesButton) 

4491 top.bind("Y", yesButton) 

4492 top.bind("n", noButton) 

4493 top.bind("N", noButton) 

4494 top.lift() 

4495 

4496 center_tk_frame(top) 

4497 

4498 top.grab_set() # Make the dialog a modal dialog. 

4499 

4500 root.update() 

4501 root.wait_window(top) 

4502 

4503 top.destroy() 

4504 root.destroy() 

4505 return val 

4506 #@+node:ekr.20210825170952.1: *4* function: qt_runAskYesNoCancelDialog 

4507 def qt_runAskYesNoCancelDialog(c): 

4508 """ 

4509 Qt version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons. 

4510 """ 

4511 if g.unitTesting: 

4512 return None 

4513 dialog = QtWidgets.QMessageBox(None) 

4514 dialog.setIcon(Information.Warning) 

4515 dialog.setWindowTitle("Saved changed outline?") 

4516 if message: 

4517 dialog.setText(message) 

4518 # Creation order determines returned value. 

4519 yes = dialog.addButton(yesMessage, ButtonRole.YesRole) 

4520 dialog.addButton(noMessage, ButtonRole.NoRole) 

4521 dialog.setDefaultButton(yes) 

4522 # Set the Leo icon. 

4523 core_dir = os.path.dirname(__file__) 

4524 icon_path = os.path.join(core_dir, "..", "Icons", "leoApp.ico") 

4525 if os.path.exists(icon_path): 

4526 pixmap = QtGui.QPixmap() 

4527 pixmap.load(icon_path) 

4528 icon = QtGui.QIcon(pixmap) 

4529 dialog.setWindowIcon(icon) 

4530 # None of these grabs focus from the console window. 

4531 dialog.raise_() 

4532 dialog.setFocus() 

4533 app.processEvents() # type:ignore 

4534 # val is the same as the creation order. 

4535 # Tested with both Qt6 and Qt5. 

4536 val = dialog.exec() if isQt6 else dialog.exec_() 

4537 if val == 0: 

4538 print(f"Saved: {c.fileName()}") 

4539 return 'yes' 

4540 print(f"Not saved: {c.fileName()}") 

4541 return 'no' 

4542 #@-others 

4543 try: 

4544 # Careful: raise the Tk dialog if there are errors in the Qt code. 

4545 from leo.core.leoQt import isQt6, QtGui, QtWidgets 

4546 from leo.core.leoQt import ButtonRole, Information 

4547 if QtGui and QtWidgets: 

4548 app = QtWidgets.QApplication([]) 

4549 assert app 

4550 val = qt_runAskYesNoCancelDialog(c) 

4551 assert val in ('yes', 'no') 

4552 return val 

4553 except Exception: 

4554 pass 

4555 if Tk: 

4556 return tk_runAskYesNoCancelDialog(c) 

4557 # #2512: There is no way to raise a dialog. 

4558 return 'yes' # Just save the file! 

4559 

4560 #@+node:felix.20210621233316.107: *3* function: get_args 

4561 def get_args(): # pragma: no cover 

4562 """ 

4563 Get arguments from the command line and sets them globally. 

4564 """ 

4565 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile, traces 

4566 

4567 def leo_file(s): 

4568 if os.path.exists(s): 

4569 return s 

4570 print(f"\nNot a .leo file: {s!r}") 

4571 sys.exit(1) 

4572 

4573 description = ''.join([ 

4574 " leoserver.py\n", 

4575 " ------------\n", 

4576 " Offers single or multiple concurrent websockets\n", 

4577 " for JSON based remote-procedure-calls\n", 

4578 " to a shared instance of leo.core.leoBridge\n", 

4579 " \n", 

4580 " Clients may be written in any language:\n", 

4581 " - leo.core.leoclient is an example client written in python.\n", 

4582 " - leoInteg (https://github.com/boltex/leointeg) is written in typescript.\n" 

4583 ]) 

4584 # Usage: 

4585 # leoserver.py [-a <address>] [-p <port>] [-l <limit>] [-f <file>] [--dirty] [--persist] 

4586 usage = 'python leo.core.leoserver [options...]' 

4587 trace_s = 'request,response,verbose' 

4588 valid_traces = [z.strip() for z in trace_s.split(',')] 

4589 parser = argparse.ArgumentParser(description=description, usage=usage, 

4590 formatter_class=argparse.RawTextHelpFormatter) 

4591 add = parser.add_argument 

4592 add('-a', '--address', dest='wsHost', type=str, default=wsHost, metavar='STR', 

4593 help='server address. Defaults to ' + str(wsHost)) 

4594 add('-p', '--port', dest='wsPort', type=int, default=wsPort, metavar='N', 

4595 help='port number. Defaults to ' + str(wsPort)) 

4596 add('-l', '--limit', dest='wsLimit', type=int, default=wsLimit, metavar='N', 

4597 help='maximum number of clients. Defaults to ' + str(wsLimit)) 

4598 add('-f', '--file', dest='argFile', type=leo_file, metavar='PATH', 

4599 help='open a .leo file at startup') 

4600 add('--persist', dest='wsPersist', action='store_true', 

4601 help='do not quit when last client disconnects') 

4602 add('-d', '--dirty', dest='wsSkipDirty', action='store_true', 

4603 help='do not warn about dirty files when quitting') 

4604 add('--trace', dest='traces', type=str, metavar='STRINGS', 

4605 help=f"comma-separated list of {trace_s}") 

4606 add('-v', '--version', dest='v', action='store_true', 

4607 help='show version and exit') 

4608 # Parse 

4609 args = parser.parse_args() 

4610 # Handle the args and set them up globally 

4611 wsHost = args.wsHost 

4612 wsPort = args.wsPort 

4613 wsLimit = args.wsLimit 

4614 wsPersist = bool(args.wsPersist) 

4615 wsSkipDirty = bool(args.wsSkipDirty) 

4616 argFile = args.argFile 

4617 if args.traces: 

4618 ok = True 

4619 for z in args.traces.split(','): 

4620 if z in valid_traces: 

4621 traces.append(z) 

4622 else: 

4623 ok = False 

4624 print(f"Ignoring invalid --trace value: {z!r}", flush=True) 

4625 if not ok: 

4626 print(f"Valid traces are: {','.join(valid_traces)}", flush=True) 

4627 print(f"--trace={','.join(traces)}", flush=True) 

4628 if args.v: 

4629 print(__version__) 

4630 sys.exit(0) 

4631 # Sanitize limit. 

4632 if wsLimit < 1: 

4633 wsLimit = 1 

4634 #@+node:felix.20210803174312.1: *3* function: notify_clients 

4635 async def notify_clients(action, excludedConn=None): 

4636 global connectionsTotal 

4637 if connectionsPool: # asyncio.wait doesn't accept an empty list 

4638 opened = bool(controller.c) # c can be none if no files opened 

4639 m = json.dumps({ 

4640 "async": "refresh", 

4641 "action": action, 

4642 "opened": opened, 

4643 }, separators=(',', ':'), cls=SetEncoder) 

4644 clientSetCopy = connectionsPool.copy() 

4645 if excludedConn: 

4646 clientSetCopy.discard(excludedConn) 

4647 if clientSetCopy: 

4648 # if still at least one to notify 

4649 await asyncio.wait([asyncio.create_task(client.send(m)) for client in clientSetCopy]) 

4650 

4651 #@+node:felix.20210803174312.2: *3* function: register_client 

4652 async def register_client(websocket): 

4653 global connectionsTotal 

4654 connectionsPool.add(websocket) 

4655 await notify_clients("unregister", websocket) 

4656 #@+node:felix.20210807160828.1: *3* function: save_dirty 

4657 def save_dirty(): 

4658 """ 

4659 Ask the user about dirty files if any remained opened. 

4660 """ 

4661 # Monkey-patch the dialog method first. 

4662 g.app.gui.runAskYesNoCancelDialog = general_yes_no_dialog 

4663 # Loop all commanders and 'close' them for dirty check 

4664 commanders = g.app.commanders() 

4665 for commander in commanders: 

4666 if commander.isChanged() and commander.fileName(): 

4667 commander.close() # Patched 'ask' methods will open dialog 

4668 #@+node:felix.20210803174312.3: *3* function: unregister_client 

4669 async def unregister_client(websocket): 

4670 global connectionsTotal 

4671 connectionsPool.remove(websocket) 

4672 await notify_clients("unregister") 

4673 #@+node:felix.20210621233316.106: *3* function: ws_handler (server) 

4674 async def ws_handler(websocket, path): 

4675 """ 

4676 The web socket handler: server.ws_server. 

4677 

4678 It must be a coroutine accepting two arguments: a WebSocketServerProtocol and the request URI. 

4679 """ 

4680 global connectionsTotal, wsLimit 

4681 tag = 'server' 

4682 trace = False 

4683 verbose = False 

4684 connected = False 

4685 

4686 try: 

4687 # Websocket connection startup 

4688 if connectionsTotal >= wsLimit: 

4689 print(f"{tag}: User Refused, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True) 

4690 await websocket.close(1001) 

4691 return 

4692 connected = True # local variable 

4693 connectionsTotal += 1 # global variable 

4694 print(f"{tag}: User Connected, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True) 

4695 # If first connection set it as the main client connection 

4696 controller._init_connection(websocket) 

4697 await register_client(websocket) 

4698 # Start by sending empty as 'ok'. 

4699 n = 0 

4700 await websocket.send(controller._make_response({"leoID": g.app.leoID})) 

4701 controller._emit_signon() 

4702 

4703 # Websocket connection message handling loop 

4704 async for json_message in websocket: 

4705 try: 

4706 n += 1 

4707 d = None 

4708 d = json.loads(json_message) 

4709 if trace and verbose: 

4710 print(f"{tag}: got: {d}", flush=True) 

4711 elif trace: 

4712 print(f"{tag}: got: {d}", flush=True) 

4713 answer = controller._do_message(d) 

4714 except TerminateServer as e: 

4715 # pylint: disable=no-value-for-parameter,unexpected-keyword-arg 

4716 raise websockets.exceptions.ConnectionClosed(code=1000, reason=e) 

4717 except ServerError as e: 

4718 data = f"{d}" if d else f"json syntax error: {json_message!r}" 

4719 error = f"{tag}: ServerError: {e}...\n{tag}: {data}" 

4720 print("", flush=True) 

4721 print(error, flush=True) 

4722 print("", flush=True) 

4723 package = { 

4724 "id": controller.current_id, 

4725 "action": controller.action, 

4726 "request": data, 

4727 "ServerError": f"{e}", 

4728 } 

4729 answer = json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4730 except InternalServerError as e: # pragma: no cover 

4731 print(f"{tag}: InternalServerError {e}", flush=True) 

4732 break 

4733 except Exception as e: # pragma: no cover 

4734 print(f"{tag}: Unexpected Exception! {e}", flush=True) 

4735 g.print_exception() 

4736 print('', flush=True) 

4737 break 

4738 await websocket.send(answer) 

4739 

4740 # If not a 'getter' send refresh signal to other clients 

4741 if controller.action[0:5] != "!get_" and controller.action != "!do_nothing": 

4742 await notify_clients(controller.action, websocket) 

4743 

4744 except websockets.exceptions.ConnectionClosedError as e: # pragma: no cover 

4745 print(f"{tag}: connection closed error: {e}") 

4746 except websockets.exceptions.ConnectionClosed as e: 

4747 print(f"{tag}: connection closed: {e}") 

4748 finally: 

4749 if connected: 

4750 connectionsTotal -= 1 

4751 await unregister_client(websocket) 

4752 print(f"{tag} connection finished. Total: {connectionsTotal}, Limit: {wsLimit}") 

4753 # Check for persistence flag if all connections are closed 

4754 if connectionsTotal == 0 and not wsPersist: 

4755 print("Shutting down leoserver") 

4756 # Preemptive closing of tasks 

4757 for task in asyncio.all_tasks(): 

4758 task.cancel() 

4759 close_Server() # Stops the run_forever loop 

4760 #@-others 

4761 

4762 # Make the first real line of output more visible. 

4763 print("", flush=True) 

4764 

4765 # Sets sHost, wsPort, wsLimit, wsPersist, wsSkipDirty fileArg and traces 

4766 get_args() # Set global values from the command line arguments 

4767 print("Starting LeoBridge... (Launch with -h for help)", flush=True) 

4768 

4769 # Open leoBridge. 

4770 controller = LeoServer() # Single instance of LeoServer, i.e., an instance of leoBridge 

4771 if argFile: 

4772 # Open specified file argument 

4773 try: 

4774 print(f"Opening file: {argFile}", flush=True) 

4775 controller.open_file({"filename": argFile}) 

4776 except Exception: 

4777 print("Opening file failed", flush=True) 

4778 

4779 # Start the server. 

4780 loop = asyncio.get_event_loop() 

4781 

4782 try: 

4783 try: 

4784 server = websockets.serve(ws_handler, wsHost, wsPort, max_size=None) # pylint: disable=no-member 

4785 realtime_server = loop.run_until_complete(server) 

4786 except OSError as e: 

4787 print(e) 

4788 print("Trying with IPv4 Family", flush=True) 

4789 server = websockets.serve(ws_handler, wsHost, wsPort, family=socket.AF_INET, max_size=None) # pylint: disable=no-member 

4790 realtime_server = loop.run_until_complete(server) 

4791 

4792 signon = SERVER_STARTED_TOKEN + f" at {wsHost} on port: {wsPort}.\n" 

4793 if wsPersist: 

4794 signon = signon + "Persistent server\n" 

4795 if wsSkipDirty: 

4796 signon = signon + "No prompt about dirty file(s) when closing server\n" 

4797 if wsLimit > 1: 

4798 signon = signon + f"Total client limit is {wsLimit}.\n" 

4799 signon = signon + "Ctrl+c to break" 

4800 print(signon, flush=True) 

4801 loop.run_forever() 

4802 

4803 except KeyboardInterrupt: 

4804 print("Process interrupted", flush=True) 

4805 

4806 finally: 

4807 # Execution continues here after server is interupted (e.g. with ctrl+c) 

4808 realtime_server.close() 

4809 if not wsSkipDirty: 

4810 print("Checking for changed commanders...", flush=True) 

4811 save_dirty() 

4812 cancel_tasks(asyncio.all_tasks(loop), loop) 

4813 loop.run_until_complete(loop.shutdown_asyncgens()) 

4814 loop.close() 

4815 asyncio.set_event_loop(None) 

4816 print("Stopped leobridge server", flush=True) 

4817#@-others 

4818if __name__ == '__main__': 

4819 # pytest will *not* execute this code. 

4820 main() 

4821#@-leo