Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#@+leo-ver=5-thin 

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

3#@@language python 

4#@@tabwidth -4 

5""" 

6Leo's internet server. 

7 

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

9""" 

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

11#@+<< imports >> 

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

13import argparse 

14import asyncio 

15import 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 ExternalFiles 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.20210818012827.1: *5* server.do_nothing 

2535 def do_nothing(self, param): 

2536 """Simply return states from _make_response""" 

2537 return self._make_response() 

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

2539 def set_ask_result(self, param): 

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

2541 tag = "set_ask_result" 

2542 result = param.get("result") 

2543 if not result: 

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

2545 g.app.externalFilesController.clientResult(result) 

2546 return self._make_response() 

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

2548 def set_config(self, param): 

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

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

2551 return self._make_response() 

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

2553 def error(self, param): 

2554 """For unit testing. Raise ServerError""" 

2555 raise ServerError("error called") 

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

2557 def get_all_leo_commands(self, param): 

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

2559 tag = 'get_all_leo_commands' 

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

2561 c = self.c 

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

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

2564 good_names = self._good_commands() 

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

2566 if duplicates: # pragma: no cover 

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

2568 for z in sorted(duplicates): 

2569 print(z, flush=True) 

2570 result = [] 

2571 for command_name in sorted(d): 

2572 func = d.get(command_name) 

2573 if not func: # pragma: no cover 

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

2575 continue 

2576 if command_name in bad_names: # #92. 

2577 continue 

2578 doc = func.__doc__ or '' 

2579 result.append({ 

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

2581 "detail": doc, 

2582 }) 

2583 if self.log_flag: # pragma: no cover 

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

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

2586 print('', flush=True) 

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

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

2589 def _bad_commands(self, c): 

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

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

2592 bad = [] 

2593 # 

2594 # leoInteg #173: Remove only vim commands. 

2595 for command_name in sorted(d): 

2596 if command_name.startswith(':'): 

2597 bad.append(command_name) 

2598 # 

2599 # Remove other commands. 

2600 # This is a hand-curated list. 

2601 bad_list = [ 

2602 'demangle-recent-files', 

2603 'clean-main-spell-dict', 

2604 'clean-persistence', 

2605 'clean-recent-files', 

2606 'clean-spellpyx', 

2607 'clean-user-spell-dict', 

2608 'clear-recent-files', 

2609 'delete-first-icon', 

2610 'delete-last-icon', 

2611 'delete-node-icons', 

2612 'insert-icon', 

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

2614 'export-headlines', # export TODO 

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

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

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

2618 'remove-sentinels', # import TODO 

2619 

2620 'save-all', 

2621 'save-file-as-zipped', 

2622 'write-file-from-node', 

2623 'edit-setting', 

2624 'edit-shortcut', 

2625 'goto-line', 

2626 'pdb', 

2627 'xdb', 

2628 'compare-two-leo-files', 

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

2630 'edit-recent-files', 

2631 'exit-leo', 

2632 'help', # To do. 

2633 'help-for-abbreviations', 

2634 'help-for-autocompletion', 

2635 'help-for-bindings', 

2636 'help-for-command', 

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

2638 'help-for-debugging-commands', 

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

2640 'help-for-dynamic-abbreviations', 

2641 'help-for-find-commands', 

2642 'help-for-keystroke', 

2643 'help-for-minibuffer', 

2644 'help-for-python', 

2645 'help-for-regular-expressions', 

2646 'help-for-scripting', 

2647 'help-for-settings', 

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

2649 

2650 'print-body', 

2651 'print-cmd-docstrings', 

2652 'print-expanded-body', 

2653 'print-expanded-html', 

2654 'print-html', 

2655 'print-marked-bodies', 

2656 'print-marked-html', 

2657 'print-marked-nodes', 

2658 'print-node', 

2659 'print-sep', 

2660 'print-tree-bodies', 

2661 'print-tree-html', 

2662 'print-tree-nodes', 

2663 'print-window-state', 

2664 'quit-leo', 

2665 'reload-style-sheets', 

2666 'save-buffers-kill-leo', 

2667 'screen-capture-5sec', 

2668 'screen-capture-now', 

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

2670 'show-style-sheet', 

2671 'sort-recent-files', 

2672 'view-lossage', 

2673 

2674 # Buffers commands (Usage?) 

2675 'buffer-append-to', 

2676 'buffer-copy', 

2677 'buffer-insert', 

2678 'buffer-kill', 

2679 'buffer-prepend-to', 

2680 'buffer-switch-to', 

2681 'buffers-list', 

2682 'buffers-list-alphabetically', 

2683 

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

2685 # 'ekr-projects', 

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

2687 'leo-dist-leo', 

2688 'leo-docs-leo', 

2689 'leo-plugins-leo', 

2690 'leo-py-leo', 

2691 'leo-quickstart-leo', 

2692 'leo-scripts-leo', 

2693 'leo-unittest-leo', 

2694 

2695 # 'scripts', 

2696 'settings', 

2697 

2698 'open-cheat-sheet-leo', 

2699 'cheat-sheet-leo', 

2700 'cheat-sheet', 

2701 'open-desktop-integration-leo', 

2702 'desktop-integration-leo', 

2703 'open-leo-dist-leo', 

2704 'leo-dist-leo', 

2705 'open-leo-docs-leo', 

2706 'leo-docs-leo', 

2707 'open-leo-plugins-leo', 

2708 'leo-plugins-leo', 

2709 'open-leo-py-leo', 

2710 'leo-py-leo', 

2711 'open-leo-py-ref-leo', 

2712 'leo-py-ref-leo', 

2713 'open-leo-py', 

2714 'open-leo-settings', 

2715 'open-leo-settings-leo', 

2716 'open-local-settings', 

2717 'my-leo-settings', 

2718 'open-my-leo-settings', 

2719 'open-my-leo-settings-leo', 

2720 'leo-settings' 

2721 'open-quickstart-leo', 

2722 'leo-quickstart-leo' 

2723 'open-scripts-leo', 

2724 'leo-scripts-leo' 

2725 'open-unittest-leo', 

2726 'leo-unittest-leo', 

2727 

2728 # Open other places... 

2729 'desktop-integration-leo', 

2730 

2731 'open-offline-tutorial', 

2732 'open-online-home', 

2733 'open-online-toc', 

2734 'open-online-tutorials', 

2735 'open-online-videos', 

2736 'open-recent-file', 

2737 'open-theme-file', 

2738 'open-url', 

2739 'open-url-under-cursor', 

2740 'open-users-guide', 

2741 

2742 # Diffs - needs open file dialog 

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

2744 'diff-leo-files', 

2745 

2746 # --- ORIGINAL BAD COMMANDS START HERE --- 

2747 # Abbreviations... 

2748 'abbrev-kill-all', 

2749 'abbrev-list', 

2750 'dabbrev-completion', 

2751 'dabbrev-expands', 

2752 

2753 # Autocompletion... 

2754 'auto-complete', 

2755 'auto-complete-force', 

2756 'disable-autocompleter', 

2757 'disable-calltips', 

2758 'enable-autocompleter', 

2759 'enable-calltips', 

2760 

2761 # Debugger... 

2762 'debug', 

2763 'db-again', 

2764 'db-b', 

2765 'db-c', 

2766 'db-h', 

2767 'db-input', 

2768 'db-l', 

2769 'db-n', 

2770 'db-q', 

2771 'db-r', 

2772 'db-s', 

2773 'db-status', 

2774 'db-w', 

2775 

2776 # File operations... 

2777 'directory-make', 

2778 'directory-remove', 

2779 'file-delete', 

2780 'file-diff-files', 

2781 'file-insert', 

2782 #'file-new', 

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

2784 

2785 # All others... 

2786 'shell-command', 

2787 'shell-command-on-region', 

2788 'cheat-sheet', 

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

2790 #'find-clone-all', 

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

2792 #'find-clone-tag', 

2793 #'find-all', 

2794 'find-all-unique-regex', 

2795 'find-character', 

2796 'find-character-extend-selection', 

2797 #'find-next', 

2798 #'find-prev', 

2799 'find-word', 

2800 'find-word-in-line', 

2801 

2802 'global-search', 

2803 

2804 'isearch-backward', 

2805 'isearch-backward-regexp', 

2806 'isearch-forward', 

2807 'isearch-forward-regexp', 

2808 'isearch-with-present-options', 

2809 

2810 #'replace', 

2811 #'replace-all', 

2812 'replace-current-character', 

2813 #'replace-then-find', 

2814 

2815 're-search-backward', 

2816 're-search-forward', 

2817 

2818 #'search-backward', 

2819 #'search-forward', 

2820 'search-return-to-origin', 

2821 

2822 'set-find-everywhere', 

2823 'set-find-node-only', 

2824 'set-find-suboutline-only', 

2825 'set-replace-string', 

2826 'set-search-string', 

2827 

2828 #'show-find-options', 

2829 

2830 #'start-search', 

2831 

2832 'toggle-find-collapses-nodes', 

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

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

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

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

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

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

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

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

2841 

2842 'word-search-backward', 

2843 'word-search-forward', 

2844 

2845 # Buttons... 

2846 'delete-script-button-button', 

2847 

2848 # Clicks... 

2849 'click-click-box', 

2850 'click-icon-box', 

2851 'ctrl-click-at-cursor', 

2852 'ctrl-click-icon', 

2853 'double-click-icon-box', 

2854 'right-click-icon', 

2855 

2856 # Editors... 

2857 'add-editor', 'editor-add', 

2858 'delete-editor', 'editor-delete', 

2859 'detach-editor-toggle', 

2860 'detach-editor-toggle-max', 

2861 

2862 # Focus... 

2863 'cycle-editor-focus', 'editor-cycle-focus', 

2864 'focus-to-body', 

2865 'focus-to-find', 

2866 'focus-to-log', 

2867 'focus-to-minibuffer', 

2868 'focus-to-nav', 

2869 'focus-to-spell-tab', 

2870 'focus-to-tree', 

2871 

2872 'tab-cycle-next', 

2873 'tab-cycle-previous', 

2874 'tab-detach', 

2875 

2876 # Headlines.. 

2877 'abort-edit-headline', 

2878 'edit-headline', 

2879 'end-edit-headline', 

2880 

2881 # Layout and panes... 

2882 'adoc', 

2883 'adoc-with-preview', 

2884 

2885 'contract-body-pane', 

2886 'contract-log-pane', 

2887 'contract-outline-pane', 

2888 

2889 'edit-pane-csv', 

2890 'edit-pane-test-open', 

2891 'equal-sized-panes', 

2892 'expand-log-pane', 

2893 'expand-body-pane', 

2894 'expand-outline-pane', 

2895 

2896 'free-layout-context-menu', 

2897 'free-layout-load', 

2898 'free-layout-restore', 

2899 'free-layout-zoom', 

2900 

2901 'zoom-in', 

2902 'zoom-out', 

2903 

2904 # Log 

2905 'clear-log', 

2906 

2907 # Menus... 

2908 'activate-cmds-menu', 

2909 'activate-edit-menu', 

2910 'activate-file-menu', 

2911 'activate-help-menu', 

2912 'activate-outline-menu', 

2913 'activate-plugins-menu', 

2914 'activate-window-menu', 

2915 'context-menu-open', 

2916 'menu-shortcut', 

2917 

2918 # Modes... 

2919 'clear-extend-mode', 

2920 

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

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

2923 #'contract-node', 

2924 #'contract-parent', 

2925 

2926 # Scrolling... 

2927 'scroll-down-half-page', 

2928 'scroll-down-line', 

2929 'scroll-down-page', 

2930 'scroll-outline-down-line', 

2931 'scroll-outline-down-page', 

2932 'scroll-outline-left', 

2933 'scroll-outline-right', 

2934 'scroll-outline-up-line', 

2935 'scroll-outline-up-page', 

2936 'scroll-up-half-page', 

2937 'scroll-up-line', 

2938 'scroll-up-page', 

2939 

2940 # Windows... 

2941 'about-leo', 

2942 

2943 'cascade-windows', 

2944 'close-others', 

2945 'close-window', 

2946 

2947 'iconify-frame', 

2948 

2949 'find-tab-hide', 

2950 #'find-tab-open', 

2951 

2952 'hide-body-dock', 

2953 'hide-body-pane', 

2954 'hide-invisibles', 

2955 'hide-log-pane', 

2956 'hide-outline-dock', 

2957 'hide-outline-pane', 

2958 'hide-tabs-dock', 

2959 

2960 'minimize-all', 

2961 

2962 'resize-to-screen', 

2963 

2964 'show-body-dock', 

2965 'show-hide-body-dock', 

2966 'show-hide-outline-dock', 

2967 'show-hide-render-dock', 

2968 'show-hide-tabs-dock', 

2969 'show-tabs-dock', 

2970 'clean-diff', 

2971 'cm-external-editor', 

2972 

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

2974 'delete-trace-statements', 

2975 

2976 'disable-idle-time-events', 

2977 

2978 'enable-idle-time-events', 

2979 'enter-quick-command-mode', 

2980 'exit-named-mode', 

2981 

2982 'F6-open-console', 

2983 

2984 'flush-lines', 

2985 'full-command', 

2986 

2987 'get-child-headlines', 

2988 

2989 'history', 

2990 

2991 'insert-file-name', 

2992 

2993 'justify-toggle-auto', 

2994 

2995 'keep-lines', 

2996 'keyboard-quit', 

2997 

2998 'line-number', 

2999 'line-numbering-toggle', 

3000 'line-to-headline', 

3001 

3002 'marked-list', 

3003 

3004 'mode-help', 

3005 

3006 'open-python-window', 

3007 

3008 'open-with-idle', 

3009 'open-with-open-office', 

3010 'open-with-scite', 

3011 'open-with-word', 

3012 

3013 'recolor', 

3014 'redraw', 

3015 

3016 'repeat-complex-command', 

3017 

3018 'session-clear', 

3019 'session-create', 

3020 'session-refresh', 

3021 'session-restore', 

3022 'session-snapshot-load', 

3023 'session-snapshot-save', 

3024 

3025 'set-colors', 

3026 'set-command-state', 

3027 'set-comment-column', 

3028 'set-extend-mode', 

3029 'set-fill-column', 

3030 'set-fill-prefix', 

3031 'set-font', 

3032 'set-insert-state', 

3033 'set-overwrite-state', 

3034 'set-silent-mode', 

3035 

3036 'show-buttons', 

3037 'show-calltips', 

3038 'show-calltips-force', 

3039 'show-color-names', 

3040 'show-color-wheel', 

3041 'show-commands', 

3042 'show-file-line', 

3043 

3044 'show-focus', 

3045 'show-fonts', 

3046 

3047 'show-invisibles', 

3048 'show-node-uas', 

3049 'show-outline-dock', 

3050 'show-plugin-handlers', 

3051 'show-plugins-info', 

3052 'show-settings', 

3053 'show-settings-outline', 

3054 'show-spell-info', 

3055 'show-stats', 

3056 'show-tips', 

3057 

3058 'style-set-selected', 

3059 

3060 'suspend', 

3061 

3062 'toggle-abbrev-mode', 

3063 'toggle-active-pane', 

3064 'toggle-angle-brackets', 

3065 'toggle-at-auto-at-edit', 

3066 'toggle-autocompleter', 

3067 'toggle-calltips', 

3068 'toggle-case-region', 

3069 'toggle-extend-mode', 

3070 'toggle-idle-time-events', 

3071 'toggle-input-state', 

3072 'toggle-invisibles', 

3073 'toggle-line-numbering-root', 

3074 'toggle-sparse-move', 

3075 'toggle-split-direction', 

3076 

3077 'what-line', 

3078 'eval', 

3079 'eval-block', 

3080 'eval-last', 

3081 'eval-last-pretty', 

3082 'eval-replace', 

3083 

3084 'find-quick', 

3085 'find-quick-changed', 

3086 'find-quick-selected', 

3087 'find-quick-test-failures', 

3088 'find-quick-timeline', 

3089 

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

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

3092 

3093 'preview', 

3094 'preview-body', 

3095 'preview-expanded-body', 

3096 'preview-expanded-html', 

3097 'preview-html', 

3098 'preview-marked-bodies', 

3099 'preview-marked-html', 

3100 'preview-marked-nodes', 

3101 'preview-node', 

3102 'preview-tree-bodies', 

3103 'preview-tree-html', 

3104 'preview-tree-nodes', 

3105 

3106 'spell-add', 

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

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

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

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

3111 'spell-change', 

3112 'spell-change-then-find', 

3113 'spell-find', 

3114 'spell-ignore', 

3115 'spell-tab-hide', 

3116 'spell-tab-open', 

3117 

3118 #'tag-children', 

3119 

3120 'todo-children-todo', 

3121 'todo-dec-pri', 

3122 'todo-find-todo', 

3123 'todo-fix-datetime', 

3124 'todo-inc-pri', 

3125 

3126 'vr', 

3127 'vr-contract', 

3128 'vr-expand', 

3129 'vr-hide', 

3130 'vr-lock', 

3131 'vr-pause-play-movie', 

3132 'vr-show', 

3133 'vr-toggle', 

3134 'vr-unlock', 

3135 'vr-update', 

3136 'vr-zoom', 

3137 

3138 'vs-create-tree', 

3139 'vs-dump', 

3140 'vs-reset', 

3141 'vs-update', 

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

3143 'add-comments', 

3144 'add-space-to-lines', 

3145 'add-tab-to-lines', 

3146 'align-eq-signs', 

3147 

3148 'back-char', 

3149 'back-char-extend-selection', 

3150 'back-page', 

3151 'back-page-extend-selection', 

3152 'back-paragraph', 

3153 'back-paragraph-extend-selection', 

3154 'back-sentence', 

3155 'back-sentence-extend-selection', 

3156 'back-to-home', 

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

3158 'back-to-indentation', 

3159 'back-word', 

3160 'back-word-extend-selection', 

3161 'back-word-smart', 

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

3163 'backward-delete-char', 

3164 'backward-delete-word', 

3165 'backward-delete-word-smart', 

3166 'backward-find-character', 

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

3168 'backward-kill-paragraph', 

3169 'backward-kill-sentence', 

3170 'backward-kill-word', 

3171 'beginning-of-buffer', 

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

3173 'beginning-of-line', 

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

3175 

3176 'capitalize-word', 

3177 'center-line', 

3178 'center-region', 

3179 'clean-all-blank-lines', 

3180 'clean-all-lines', 

3181 'clean-body', 

3182 'clean-lines', 

3183 'clear-kill-ring', 

3184 'clear-selected-text', 

3185 'convert-blanks', 

3186 'convert-tabs', 

3187 'copy-text', 

3188 'cut-text', 

3189 

3190 'delete-char', 

3191 'delete-comments', 

3192 'delete-indentation', 

3193 'delete-spaces', 

3194 'delete-word', 

3195 'delete-word-smart', 

3196 'downcase-region', 

3197 'downcase-word', 

3198 

3199 'end-of-buffer', 

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

3201 'end-of-line', 

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

3203 

3204 'exchange-point-mark', 

3205 

3206 'extend-to-line', 

3207 'extend-to-paragraph', 

3208 'extend-to-sentence', 

3209 'extend-to-word', 

3210 

3211 'fill-paragraph', 

3212 'fill-region', 

3213 'fill-region-as-paragraph', 

3214 

3215 'finish-of-line', 

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

3217 

3218 'forward-char', 

3219 'forward-char-extend-selection', 

3220 'forward-end-word', 

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

3222 'forward-page', 

3223 'forward-page-extend-selection', 

3224 'forward-paragraph', 

3225 'forward-paragraph-extend-selection', 

3226 'forward-sentence', 

3227 'forward-sentence-extend-selection', 

3228 'forward-word', 

3229 'forward-word-extend-selection', 

3230 'forward-word-smart', 

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

3232 

3233 'go-anywhere', 

3234 'go-back', 

3235 'go-forward', 

3236 'goto-char', 

3237 

3238 'indent-region', 

3239 'indent-relative', 

3240 'indent-rigidly', 

3241 'indent-to-comment-column', 

3242 

3243 'insert-hard-tab', 

3244 'insert-newline', 

3245 'insert-parentheses', 

3246 'insert-soft-tab', 

3247 

3248 'kill-line', 

3249 'kill-paragraph', 

3250 'kill-pylint', 

3251 'kill-region', 

3252 'kill-region-save', 

3253 'kill-sentence', 

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

3255 'kill-word', 

3256 'kill-ws', 

3257 

3258 'match-brackets', 

3259 

3260 'move-lines-down', 

3261 'move-lines-up', 

3262 'move-past-close', 

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

3264 

3265 'newline-and-indent', 

3266 'next-line', 

3267 'next-line-extend-selection', 

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

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

3270 

3271 'previous-line', 

3272 'previous-line-extend-selection', 

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

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

3275 

3276 'rectangle-clear', 

3277 'rectangle-close', 

3278 'rectangle-delete', 

3279 'rectangle-kill', 

3280 'rectangle-open', 

3281 'rectangle-string', 

3282 'rectangle-yank', 

3283 

3284 'remove-blank-lines', 

3285 'remove-newlines', 

3286 'remove-space-from-lines', 

3287 'remove-tab-from-lines', 

3288 

3289 'reverse-region', 

3290 'reverse-sort-lines', 

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

3292 

3293 'paste-text', 

3294 'pop-cursor', 

3295 'push-cursor', 

3296 

3297 'select-all', 

3298 'select-next-trace-statement', 

3299 'select-to-matching-bracket', 

3300 

3301 'sort-columns', 

3302 'sort-fields', 

3303 'sort-lines', 

3304 'sort-lines-ignoring-case', 

3305 

3306 'split-defs', 

3307 'split-line', 

3308 

3309 'start-of-line', 

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

3311 

3312 'tabify', 

3313 'transpose-chars', 

3314 'transpose-lines', 

3315 'transpose-words', 

3316 

3317 'unformat-paragraph', 

3318 'unindent-region', 

3319 

3320 'untabify', 

3321 

3322 'upcase-region', 

3323 'upcase-word', 

3324 'update-ref-file', 

3325 

3326 'yank', 

3327 'yank-pop', 

3328 

3329 'zap-to-character', 

3330 

3331 ] 

3332 bad.extend(bad_list) 

3333 result = list(sorted(bad)) 

3334 return result 

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

3336 def _good_commands(self): 

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

3338 good_list = [ 

3339 

3340 'contract-all', 

3341 'contract-all-other-nodes', 

3342 'clone-node', 

3343 'copy-node', 

3344 'copy-marked-nodes', 

3345 'cut-node', 

3346 

3347 'de-hoist', 

3348 'delete-marked-nodes', 

3349 'delete-node', 

3350 # 'demangle-recent-files', 

3351 'demote', 

3352 'do-nothing', 

3353 'expand-and-go-right', 

3354 'expand-next-level', 

3355 'expand-node', 

3356 'expand-or-go-right', 

3357 'expand-prev-level', 

3358 'expand-to-level-1', 

3359 'expand-to-level-2', 

3360 'expand-to-level-3', 

3361 'expand-to-level-4', 

3362 'expand-to-level-5', 

3363 'expand-to-level-6', 

3364 'expand-to-level-7', 

3365 'expand-to-level-8', 

3366 'expand-to-level-9', 

3367 'expand-all', 

3368 'expand-all-subheads', 

3369 'expand-ancestors-only', 

3370 

3371 'find-next-clone', 

3372 

3373 'goto-first-node', 

3374 'goto-first-sibling', 

3375 'goto-first-visible-node', 

3376 'goto-last-node', 

3377 'goto-last-sibling', 

3378 'goto-last-visible-node', 

3379 'goto-next-changed', 

3380 'goto-next-clone', 

3381 'goto-next-marked', 

3382 'goto-next-node', 

3383 'goto-next-sibling', 

3384 'goto-next-visible', 

3385 'goto-parent', 

3386 'goto-prev-marked', 

3387 'goto-prev-node', 

3388 'goto-prev-sibling', 

3389 'goto-prev-visible', 

3390 

3391 'hoist', 

3392 

3393 'insert-node', 

3394 'insert-node-before', 

3395 'insert-as-first-child', 

3396 'insert-as-last-child', 

3397 'insert-child', 

3398 

3399 'mark', 

3400 'mark-changed-items', 

3401 'mark-first-parents', 

3402 'mark-subheads', 

3403 

3404 'move-marked-nodes', 

3405 'move-outline-down', 

3406 'move-outline-left', 

3407 'move-outline-right', 

3408 'move-outline-up', 

3409 

3410 'paste-node', 

3411 'paste-retaining-clones', 

3412 'promote', 

3413 'promote-bodies', 

3414 'promote-headlines', 

3415 

3416 'sort-children', 

3417 'sort-siblings', 

3418 

3419 'tangle', 

3420 'tangle-all', 

3421 'tangle-marked', 

3422 

3423 'unmark-all', 

3424 'unmark-first-parents', 

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

3426 #'clean-persistence', 

3427 #'clean-recent-files', 

3428 #'clean-spellpyx', 

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

3430 

3431 'clear-all-caches', 

3432 'clear-all-hoists', 

3433 'clear-all-uas', 

3434 'clear-cache', 

3435 'clear-node-uas', 

3436 #'clear-recent-files', 

3437 

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

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

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

3441 

3442 'dump-caches', 

3443 'dump-clone-parents', 

3444 'dump-expanded', 

3445 'dump-node', 

3446 'dump-outline', 

3447 

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

3449 

3450 #'set-ua', 

3451 

3452 'show-all-uas', 

3453 'show-bindings', 

3454 'show-clone-ancestors', 

3455 'show-clone-parents', 

3456 

3457 # Export files... 

3458 #'export-headlines', # export 

3459 #'export-jupyter-notebook', # export 

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

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

3462 #'remove-sentinels', # import 

3463 'typescript-to-py', 

3464 

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

3466 'import-MORE-files', 

3467 'import-file', 

3468 'import-free-mind-files', 

3469 'import-jupyter-notebook', 

3470 'import-legacy-external-files', 

3471 'import-mind-jet-files', 

3472 'import-tabbed-files', 

3473 'import-todo-text-files', 

3474 'import-zim-folder', 

3475 

3476 # Read outlines... 

3477 'read-at-auto-nodes', 

3478 'read-at-file-nodes', 

3479 'read-at-shadow-nodes', 

3480 'read-file-into-node', 

3481 'read-outline-only', 

3482 'read-ref-file', 

3483 

3484 # Save Files. 

3485 'file-save', 

3486 'file-save-as', 

3487 'file-save-by-name', 

3488 'file-save-to', 

3489 'save', 

3490 'save-as', 

3491 'save-file', 

3492 'save-file-as', 

3493 'save-file-by-name', 

3494 'save-file-to', 

3495 'save-to', 

3496 

3497 # Write parts of outlines... 

3498 'write-at-auto-nodes', 

3499 'write-at-file-nodes', 

3500 'write-at-shadow-nodes', 

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

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

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

3504 'write-edited-recent-files', 

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

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

3507 'write-outline-only', 

3508 

3509 'clone-find-all', 

3510 'clone-find-all-flattened', 

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

3512 'clone-find-all-marked', 

3513 'clone-find-parents', 

3514 'clone-find-tag', 

3515 'clone-marked-nodes', 

3516 'clone-node-to-last-node', 

3517 

3518 'clone-to-at-spot', 

3519 

3520 #'edit-setting', 

3521 #'edit-shortcut', 

3522 

3523 'execute-pytest', 

3524 'execute-script', 

3525 'extract', 

3526 'extract-names', 

3527 

3528 'goto-any-clone', 

3529 'goto-global-line', 

3530 #'goto-line', 

3531 'git-diff', 'gd', 

3532 

3533 'log-kill-listener', 'kill-log-listener', 

3534 'log-listen', 'listen-to-log', 

3535 

3536 'make-stub-files', 

3537 

3538 #'pdb', 

3539 

3540 'redo', 

3541 'rst3', 

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

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

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

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

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

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

3548 'run-tests', 

3549 

3550 'undo', 

3551 

3552 #'xdb', 

3553 

3554 # Beautify, blacken, fstringify... 

3555 'beautify-files', 

3556 'beautify-files-diff', 

3557 'blacken-files', 

3558 'blacken-files-diff', 

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

3560 'diff-beautify-files', 

3561 'diff-fstringify-files', 

3562 #'diff-leo-files', 

3563 'diff-marked-nodes', 

3564 'fstringify-files', 

3565 'fstringify-files-diff', 

3566 'fstringify-files-silent', 

3567 'pretty-print-c', 

3568 'silent-fstringify-files', 

3569 

3570 # All other commands... 

3571 'at-file-to-at-auto', 

3572 

3573 'beautify-c', 

3574 

3575 'cls', 

3576 'c-to-python', 

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

3578 'check-derived-file', 

3579 'check-outline', 

3580 'code-to-rst', 

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

3582 'convert-all-blanks', 

3583 'convert-all-tabs', 

3584 'count-children', 

3585 'count-pages', 

3586 'count-region', 

3587 

3588 #'desktop-integration-leo', 

3589 

3590 #'edit-recent-files', 

3591 #'exit-leo', 

3592 

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

3594 'find-def', 

3595 'find-long-lines', 

3596 'find-missing-docstrings', 

3597 'flake8-files', 

3598 'flatten-outline', 

3599 'flatten-outline-to-node', 

3600 'flatten-script', 

3601 

3602 'gc-collect-garbage', 

3603 'gc-dump-all-objects', 

3604 'gc-dump-new-objects', 

3605 'gc-dump-objects-verbose', 

3606 'gc-show-summary', 

3607 

3608 #'help', # To do. 

3609 #'help-for-abbreviations', 

3610 #'help-for-autocompletion', 

3611 #'help-for-bindings', 

3612 #'help-for-command', 

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

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

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

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

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

3618 #'help-for-keystroke', 

3619 #'help-for-minibuffer', 

3620 #'help-for-python', 

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

3622 #'help-for-scripting', 

3623 #'help-for-settings', 

3624 

3625 'insert-body-time', # ? 

3626 'insert-headline-time', 

3627 'insert-jupyter-toc', 

3628 'insert-markdown-toc', 

3629 

3630 'find-var', 

3631 

3632 #'join-leo-irc', 

3633 'join-node-above', 

3634 'join-node-below', 

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

3636 

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

3638 

3639 'new', 

3640 

3641 'open-outline', 

3642 

3643 'parse-body', 

3644 'parse-json', 

3645 'pandoc', 

3646 'pandoc-with-preview', 

3647 'paste-as-template', 

3648 

3649 #'print-body', 

3650 #'print-cmd-docstrings', 

3651 #'print-expanded-body', 

3652 #'print-expanded-html', 

3653 #'print-html', 

3654 #'print-marked-bodies', 

3655 #'print-marked-html', 

3656 #'print-marked-nodes', 

3657 #'print-node', 

3658 #'print-sep', 

3659 #'print-tree-bodies', 

3660 #'print-tree-html', 

3661 #'print-tree-nodes', 

3662 #'print-window-state', 

3663 

3664 'pyflakes', 

3665 'pylint', 

3666 'pylint-kill', 

3667 'python-to-coffeescript', 

3668 

3669 #'quit-leo', 

3670 

3671 'reformat-body', 

3672 'reformat-paragraph', 

3673 'refresh-from-disk', 

3674 'reload-settings', 

3675 #'reload-style-sheets', 

3676 'revert', 

3677 

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

3679 #'screen-capture-5sec', 

3680 #'screen-capture-now', 

3681 'script-button', # ? 

3682 #'set-reference-file', 

3683 #'show-style-sheet', 

3684 #'sort-recent-files', 

3685 'sphinx', 

3686 'sphinx-with-preview', 

3687 'style-reload', # ? 

3688 

3689 'untangle', 

3690 'untangle-all', 

3691 'untangle-marked', 

3692 

3693 #'view-lossage', # ? 

3694 

3695 'weave', 

3696 

3697 # Dubious commands (to do)... 

3698 'act-on-node', 

3699 

3700 'cfa', 

3701 'cfam', 

3702 'cff', 

3703 'cffm', 

3704 'cft', 

3705 

3706 #'buffer-append-to', 

3707 #'buffer-copy', 

3708 #'buffer-insert', 

3709 #'buffer-kill', 

3710 #'buffer-prepend-to', 

3711 #'buffer-switch-to', 

3712 #'buffers-list', 

3713 #'buffers-list-alphabetically', 

3714 

3715 'chapter-back', 

3716 'chapter-next', 

3717 'chapter-select', 

3718 'chapter-select-main', 

3719 'create-def-list', # ? 

3720 ] 

3721 return good_list 

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

3723 def get_all_server_commands(self, param): 

3724 """ 

3725 Public server method: 

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

3727 """ 

3728 tag = 'get_all_server_commands' 

3729 names = self._get_all_server_commands() 

3730 if self.log_flag: # pragma: no cover 

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

3732 g.printObj(names, tag=tag) 

3733 print('', flush=True) 

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

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

3736 def _get_all_server_commands(self): 

3737 """ 

3738 Private server method: 

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

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

3741 """ 

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

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

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

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

3746 """Begin the connection.""" 

3747 global connectionsTotal 

3748 if connectionsTotal == 1: 

3749 # First connection, so "Master client" setup 

3750 self.web_socket = web_socket 

3751 self.loop = asyncio.get_event_loop() 

3752 else: 

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

3754 pass # nothing for now 

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

3756 def shut_down(self, param): 

3757 """Shut down the server.""" 

3758 tag = 'shut_down' 

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

3760 if n: # pragma: no cover 

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

3762 raise TerminateServer("client requested shut down") 

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

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

3765 def _ap_to_p(self, ap): 

3766 """ 

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

3768 

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

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

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

3772 now inaccessible leo document commander.) 

3773 """ 

3774 tag = '_ap_to_p' 

3775 c = self._check_c() 

3776 gnx_d = c.fileCommands.gnxDict 

3777 try: 

3778 outer_stack = ap.get('stack') 

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

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

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

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

3783 # 

3784 def d_to_childIndex_v(d): 

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

3786 childIndex = d.get('childIndex') 

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

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

3789 try: 

3790 childIndex = int(childIndex) 

3791 except Exception: # pragma: no cover. 

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

3793 gnx = d.get('gnx') 

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

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

3796 v = gnx_d.get(gnx) 

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

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

3799 return childIndex, v 

3800 # 

3801 # Compute p.childIndex and p.v. 

3802 childIndex, v = d_to_childIndex_v(ap) 

3803 # 

3804 # Create p.stack. 

3805 stack = [] 

3806 for stack_d in outer_stack: 

3807 stack_childIndex, stack_v = d_to_childIndex_v(stack_d) 

3808 stack.append((stack_v, stack_childIndex)) 

3809 # 

3810 # Make p and check p. 

3811 p = Position(v, childIndex, stack) 

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

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

3814 except Exception: 

3815 if self.log_flag or traces: 

3816 print( 

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

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

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

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

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

3822 return p 

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

3824 def _check_c(self): 

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

3826 tag = '_check_c' 

3827 c = self.c 

3828 if not c: # pragma: no cover 

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

3830 return c 

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

3832 def _check_outline(self, c): 

3833 """Check self.c for consistency.""" 

3834 # Check that all positions exist. 

3835 self._check_outline_positions(c) 

3836 # Test round-tripping. 

3837 self._test_round_trip_positions(c) 

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

3839 def _check_outline_positions(self, c): 

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

3841 tag = '_check_outline_positions' 

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

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

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

3845 print(message, flush=True) 

3846 self._dump_position(p) 

3847 raise ServerError(message) 

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

3849 def _do_leo_command_by_name(self, command_name, param): 

3850 """ 

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

3852 

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

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

3855 should be re-selected afterward. 

3856 

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

3858 

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

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

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

3862 

3863 """ 

3864 tag = '_do_leo_command_by_name' 

3865 c = self._check_c() 

3866 

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

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

3869 

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

3871 if "keep" in param: 

3872 keepSelection = param["keep"] 

3873 

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

3875 if not func: # pragma: no cover 

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

3877 

3878 p = self._get_p(param) 

3879 try: 

3880 if p == c.p: 

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

3882 else: 

3883 old_p = c.p # preserve old position 

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

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

3886 if keepSelection and c.positionExists(old_p): 

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

3888 c.selectPosition(old_p) 

3889 except Exception as e: 

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

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

3892 # 

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

3894 if self._is_jsonable(value): 

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

3896 return self._make_response() 

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

3898 def _do_leo_function_by_name(self, function_name, param): 

3899 """ 

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

3901 

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

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

3904 should be re-selected afterward. 

3905 

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

3907 

3908 command: the name of a method 

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

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

3911 

3912 """ 

3913 tag = '_do_leo_function_by_name' 

3914 c = self._check_c() 

3915 

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

3917 if "keep" in param: 

3918 keepSelection = param["keep"] 

3919 

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

3921 if not func: # pragma: no cover 

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

3923 

3924 p = self._get_p(param) 

3925 try: 

3926 if p == c.p: 

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

3928 else: 

3929 old_p = c.p # preserve old position 

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

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

3932 if keepSelection and c.positionExists(old_p): 

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

3934 c.selectPosition(old_p) 

3935 except Exception as e: 

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

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

3938 # 

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

3940 if self._is_jsonable(value): 

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

3942 return self._make_response() 

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

3944 def _do_message(self, d): 

3945 """ 

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

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

3948 

3949 "id": A positive integer. 

3950 

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

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

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

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

3955 

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

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

3958 

3959 Return a dict, created by _make_response or _make_minimal_response 

3960 that contains at least an 'id' key. 

3961 

3962 """ 

3963 global traces 

3964 tag = '_do_message' 

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

3966 

3967 # Require "id" and "action" keys 

3968 id_ = d.get("id") 

3969 if id_ is None: # pragma: no cover 

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

3971 action = d.get("action") 

3972 if action is None: # pragma: no cover 

3973 raise ServerError(f"{tag}: no action") 

3974 

3975 # TODO : make/force always an object from the client connected. 

3976 param = d.get('param', {}) # Can be none or a string 

3977 # Set log flag. 

3978 if param: 

3979 self.log_flag = param.get("log") 

3980 pass 

3981 else: 

3982 param = {} 

3983 

3984 # Handle traces. 

3985 if trace and verbose: # pragma: no cover 

3986 g.printObj(d, tag=f"request {id_}") 

3987 print('', flush=True) 

3988 elif trace: # pragma: no cover 

3989 keys = sorted(param.keys()) 

3990 if action == '!set_config': 

3991 keys_s = f"({len(keys)} keys)" 

3992 elif len(keys) > 5: 

3993 keys_s = '\n ' + '\n '.join(keys) 

3994 else: 

3995 keys_s = ', '.join(keys) 

3996 print(f" request {id_:<4} {action:<30} {keys_s}", flush=True) 

3997 

3998 # Set the current_id and action ivars for _make_response. 

3999 self.current_id = id_ 

4000 self.action = action 

4001 

4002 # Execute the requested action. 

4003 if action[0] == "!": 

4004 action = action[1:] # Remove exclamation point "!" 

4005 func = self._do_server_command # Server has this method. 

4006 elif action[0] == '-': 

4007 action = action[1:] # Remove dash "-" 

4008 func = self._do_leo_command_by_name # It's a command name. 

4009 else: 

4010 func = self._do_leo_function_by_name # It's the name of a method in some commander. 

4011 result = func(action, param) 

4012 if result is None: # pragma: no cover 

4013 raise ServerError(f"{tag}: no response: {action!r}") 

4014 return result 

4015 #@+node:felix.20210621233316.86: *4* server._do_server_command 

4016 def _do_server_command(self, action, param): 

4017 tag = '_do_server_command' 

4018 # Disallow hidden methods. 

4019 if action.startswith('_'): # pragma: no cover 

4020 raise ServerError(f"{tag}: action starts with '_': {action!r}") 

4021 # Find and execute the server method. 

4022 func = getattr(self, action, None) 

4023 if not func: 

4024 raise ServerError(f"{tag}: action not found: {action!r}") # pragma: no cover 

4025 if not callable(func): 

4026 raise ServerError(f"{tag}: not callable: {func!r}") # pragma: no cover 

4027 return func(param) 

4028 #@+node:felix.20210621233316.87: *4* server._dump_* 

4029 def _dump_outline(self, c): # pragma: no cover 

4030 """Dump the outline.""" 

4031 tag = '_dump_outline' 

4032 print(f"{tag}: {c.shortFileName()}...\n", flush=True) 

4033 for p in c.all_positions(): 

4034 self._dump_position(p) 

4035 print('', flush=True) 

4036 

4037 def _dump_position(self, p): # pragma: no cover 

4038 level_s = ' ' * 2 * p.level() 

4039 print(f"{level_s}{p.childIndex():2} {p.v.gnx} {p.h}", flush=True) 

4040 #@+node:felix.20210624160812.1: *4* server._emit_signon 

4041 def _emit_signon(self): 

4042 """Simulate the Initial Leo Log Entry""" 

4043 tag = 'emit_signon' 

4044 if self.loop: 

4045 g.app.computeSignon() 

4046 signon = [] 

4047 for z in (g.app.signon, g.app.signon1): 

4048 for z2 in z.split('\n'): 

4049 signon.append(z2.strip()) 

4050 g.es("\n".join(signon)) 

4051 else: 

4052 raise ServerError(f"{tag}: no loop ready for emit_signon") 

4053 #@+node:felix.20210625230236.1: *4* server._get_commander_method 

4054 def _get_commander_method(self, command): 

4055 """ Return the given method (p_command) in the Commands class or subcommanders.""" 

4056 # First, try the commands class. 

4057 c = self._check_c() 

4058 func = getattr(c, command, None) 

4059 if func: 

4060 return func 

4061 # Otherwise, search all subcommanders for the method. 

4062 table = ( # This table comes from c.initObjectIvars. 

4063 'abbrevCommands', 

4064 'bufferCommands', 

4065 'chapterCommands', 

4066 'controlCommands', 

4067 'convertCommands', 

4068 'debugCommands', 

4069 'editCommands', 

4070 'editFileCommands', 

4071 'evalController', 

4072 'gotoCommands', 

4073 'helpCommands', 

4074 'keyHandler', 

4075 'keyHandlerCommands', 

4076 'killBufferCommands', 

4077 'leoCommands', 

4078 'leoTestManager', 

4079 'macroCommands', 

4080 'miniBufferWidget', 

4081 'printingController', 

4082 'queryReplaceCommands', 

4083 'rectangleCommands', 

4084 'searchCommands', 

4085 'spellCommands', 

4086 'vimCommands', # Not likely to be useful. 

4087 ) 

4088 for ivar in table: 

4089 subcommander = getattr(c, ivar, None) 

4090 if subcommander: 

4091 func = getattr(subcommander, command, None) 

4092 if func: 

4093 return func 

4094 return None 

4095 #@+node:felix.20210621233316.91: *4* server._get_focus 

4096 def _get_focus(self): 

4097 """Server helper method to get the focused panel name string""" 

4098 tag = '_get_focus' 

4099 try: 

4100 w = g.app.gui.get_focus() 

4101 focus = g.app.gui.widget_name(w) 

4102 except Exception as e: 

4103 raise ServerError(f"{tag}: exception trying to get the focused widget: {e}") 

4104 return focus 

4105 #@+node:felix.20210621233316.90: *4* server._get_p 

4106 def _get_p(self, param, strict=False): 

4107 """ 

4108 Return _ap_to_p(param["ap"]) or c.p., 

4109 or False if the strict flag is set 

4110 """ 

4111 tag = '_get_ap' 

4112 c = self.c 

4113 if not c: # pragma: no cover 

4114 raise ServerError(f"{tag}: no c") 

4115 

4116 ap = param.get("ap") 

4117 if ap: 

4118 p = self._ap_to_p(ap) # Convertion 

4119 if p: 

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

4121 raise ServerError(f"{tag}: position does not exist. ap: {ap!r}") 

4122 return p # Return the position 

4123 if strict: 

4124 return False 

4125 # Fallback to c.p 

4126 if not c.p: # pragma: no cover 

4127 raise ServerError(f"{tag}: no c.p") 

4128 

4129 return c.p 

4130 #@+node:felix.20210621233316.92: *4* server._get_position_d 

4131 def _get_position_d(self, p): 

4132 """ 

4133 Return a python dict that is adding 

4134 graphical representation data and flags 

4135 to the base 'ap' dict from _p_to_ap. 

4136 (To be used by the connected client GUI.) 

4137 """ 

4138 d = self._p_to_ap(p) 

4139 d['headline'] = p.h 

4140 d['level'] = p.level() 

4141 if p.v.u: 

4142 if g.leoServer.leoServerConfig and g.leoServer.leoServerConfig.get("uAsBoolean", False): 

4143 # uAsBoolean is 'thruthy' 

4144 d['u'] = True 

4145 else: 

4146 # Normal output if no options set 

4147 d['u'] = p.v.u 

4148 if bool(p.b): 

4149 d['hasBody'] = True 

4150 if p.hasChildren(): 

4151 d['hasChildren'] = True 

4152 if p.isCloned(): 

4153 d['cloned'] = True 

4154 if p.isDirty(): 

4155 d['dirty'] = True 

4156 if p.isExpanded(): 

4157 d['expanded'] = True 

4158 if p.isMarked(): 

4159 d['marked'] = True 

4160 if p.isAnyAtFileNode(): 

4161 d['atFile'] = True 

4162 if p == self.c.p: 

4163 d['selected'] = True 

4164 return d 

4165 #@+node:felix.20210705211625.1: *4* server._is_jsonable 

4166 def _is_jsonable(self, x): 

4167 try: 

4168 json.dumps(x, cls=SetEncoder) 

4169 return True 

4170 except(TypeError, OverflowError): 

4171 return False 

4172 #@+node:felix.20210621233316.94: *4* server._make_minimal_response 

4173 def _make_minimal_response(self, package=None): 

4174 """ 

4175 Return a json string representing a response dict. 

4176 

4177 The 'package' kwarg, if present, must be a python dict describing a 

4178 response. package may be an empty dict or None. 

4179 

4180 The 'p' kwarg, if present, must be a position. 

4181 

4182 First, this method creates a response (a python dict) containing all 

4183 the keys in the 'package' dict. 

4184 

4185 Then it adds 'id' to the package. 

4186 

4187 Finally, this method returns the json string corresponding to the 

4188 response. 

4189 """ 

4190 if package is None: 

4191 package = {} 

4192 

4193 # Always add id. 

4194 package["id"] = self.current_id 

4195 

4196 return json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4197 #@+node:felix.20210621233316.93: *4* server._make_response 

4198 def _make_response(self, package=None): 

4199 """ 

4200 Return a json string representing a response dict. 

4201 

4202 The 'package' kwarg, if present, must be a python dict describing a 

4203 response. package may be an empty dict or None. 

4204 

4205 The 'p' kwarg, if present, must be a position. 

4206 

4207 First, this method creates a response (a python dict) containing all 

4208 the keys in the 'package' dict, with the following added keys: 

4209 

4210 - "id": The incoming id. 

4211 - "commander": A dict describing self.c. 

4212 - "node": None, or an archived position describing self.c.p. 

4213 

4214 Finally, this method returns the json string corresponding to the 

4215 response. 

4216 """ 

4217 global traces 

4218 tag = '_make_response' 

4219 trace = self.log_flag or 'response' in traces 

4220 verbose = 'verbose' in traces 

4221 c = self.c # It is valid for c to be None. 

4222 if package is None: 

4223 package = {} 

4224 p = package.get("p") 

4225 if p: 

4226 del package["p"] 

4227 # Raise an *internal* error if checks fail. 

4228 if isinstance(package, str): # pragma: no cover 

4229 raise InternalServerError(f"{tag}: bad package kwarg: {package!r}") 

4230 if p and not isinstance(p, Position): # pragma: no cover 

4231 raise InternalServerError(f"{tag}: bad p kwarg: {p!r}") 

4232 if p and not c: # pragma: no cover 

4233 raise InternalServerError(f"{tag}: p but not c") 

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

4235 raise InternalServerError(f"{tag}: p does not exist: {p!r}") 

4236 if c and not c.p: # pragma: no cover 

4237 raise InternalServerError(f"{tag}: empty c.p") 

4238 

4239 # Always add id 

4240 package["id"] = self.current_id 

4241 

4242 # The following keys are relevant only if there is an open commander. 

4243 if c: 

4244 # Allow commands, especially _get_redraw_d, to specify p! 

4245 p = p or c.p 

4246 package["commander"] = { 

4247 "changed": c.isChanged(), 

4248 "fileName": c.fileName(), # Can be None for new files. 

4249 } 

4250 # Add all the node data, including: 

4251 # - "node": self._p_to_ap(p) # Contains p.gnx, p.childIndex and p.stack. 

4252 # - All the *cheap* redraw data for p. 

4253 redraw_d = self._get_position_d(p) 

4254 package["node"] = redraw_d 

4255 

4256 # Handle traces. 

4257 if trace and verbose: # pragma: no cover 

4258 g.printObj(package, tag=f"response {self.current_id}") 

4259 print('', flush=True) 

4260 elif trace: # pragma: no cover 

4261 keys = sorted(package.keys()) 

4262 keys_s = ', '.join(keys) 

4263 print(f"response {self.current_id:<4} {keys_s}", flush=True) 

4264 

4265 return json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4266 #@+node:felix.20210621233316.95: *4* server._p_to_ap 

4267 def _p_to_ap(self, p): 

4268 """ 

4269 * From Leo plugin leoflexx.py * 

4270 

4271 Convert Leo position p to a serializable archived position. 

4272 

4273 This returns only position-related data. 

4274 get_position_data returns all data needed to redraw the screen. 

4275 """ 

4276 self._check_c() 

4277 stack = [{'gnx': v.gnx, 'childIndex': childIndex} 

4278 for (v, childIndex) in p.stack] 

4279 return { 

4280 'childIndex': p._childIndex, 

4281 'gnx': p.v.gnx, 

4282 'stack': stack, 

4283 } 

4284 #@+node:felix.20210621233316.96: *4* server._positionFromGnx 

4285 def _positionFromGnx(self, gnx): 

4286 """Return first p node with this gnx or false""" 

4287 c = self._check_c() 

4288 for p in c.all_unique_positions(): 

4289 if p.v.gnx == gnx: 

4290 return p 

4291 return False 

4292 #@+node:felix.20210622232409.1: *4* server._send_async_output & helper 

4293 def _send_async_output(self, package, toAll=False): 

4294 """ 

4295 Send data asynchronously to the client 

4296 """ 

4297 tag = "send async output" 

4298 jsonPackage = json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4299 if "async" not in package: 

4300 InternalServerError(f"\n{tag}: async member missing in package {jsonPackage} \n") 

4301 if self.loop: 

4302 self.loop.create_task(self._async_output(jsonPackage, toAll)) 

4303 else: 

4304 InternalServerError(f"\n{tag}: loop not ready {jsonPackage} \n") 

4305 #@+node:felix.20210621233316.89: *5* server._async_output 

4306 async def _async_output(self, json, toAll=False): # pragma: no cover (tested in server) 

4307 """Output json string to the web_socket""" 

4308 global connectionsTotal 

4309 tag = '_async_output' 

4310 outputBytes = bytes(json, 'utf-8') 

4311 if toAll: 

4312 if connectionsPool: # asyncio.wait doesn't accept an empty list 

4313 await asyncio.wait([asyncio.create_task(client.send(outputBytes)) for client in connectionsPool]) 

4314 else: 

4315 g.trace(f"{tag}: no web socket. json: {json!r}") 

4316 else: 

4317 if self.web_socket: 

4318 await self.web_socket.send(outputBytes) 

4319 else: 

4320 g.trace(f"{tag}: no web socket. json: {json!r}") 

4321 #@+node:felix.20210621233316.97: *4* server._test_round_trip_positions 

4322 def _test_round_trip_positions(self, c): # pragma: no cover (tested in client). 

4323 """Test the round tripping of p_to_ap and ap_to_p.""" 

4324 tag = '_test_round_trip_positions' 

4325 for p in c.all_unique_positions(): 

4326 ap = self._p_to_ap(p) 

4327 p2 = self._ap_to_p(ap) 

4328 if p != p2: 

4329 self._dump_outline(c) 

4330 raise ServerError(f"{tag}: round-trip failed: ap: {ap!r}, p: {p!r}, p2: {p2!r}") 

4331 #@+node:felix.20210625002950.1: *4* server._yieldAllRootChildren 

4332 def _yieldAllRootChildren(self): 

4333 """Return all root children P nodes""" 

4334 c = self._check_c() 

4335 p = c.rootPosition() 

4336 while p: 

4337 yield p 

4338 p.moveToNext() 

4339 

4340 #@-others 

4341#@+node:felix.20210621233316.105: ** function: main & helpers 

4342def main(): # pragma: no cover (tested in client) 

4343 """python script for leo integration via leoBridge""" 

4344 global websockets 

4345 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile 

4346 if not websockets: 

4347 print('websockets not found') 

4348 print('pip install websockets') 

4349 return 

4350 

4351 #@+others 

4352 #@+node:felix.20210807214524.1: *3* function: cancel_tasks 

4353 def cancel_tasks(to_cancel, loop): 

4354 if not to_cancel: 

4355 return 

4356 

4357 for task in to_cancel: 

4358 task.cancel() 

4359 

4360 loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) 

4361 

4362 for task in to_cancel: 

4363 if task.cancelled(): 

4364 continue 

4365 if task.exception() is not None: 

4366 loop.call_exception_handler( 

4367 { 

4368 "message": "unhandled exception during asyncio.run() shutdown", 

4369 "exception": task.exception(), 

4370 "task": task, 

4371 } 

4372 ) 

4373 #@+node:ekr.20210825115746.1: *3* function: center_tk_frame 

4374 def center_tk_frame(top): 

4375 """Center the top-level Frame.""" 

4376 # https://stackoverflow.com/questions/3352918 

4377 top.update_idletasks() 

4378 screen_width = top.winfo_screenwidth() 

4379 screen_height = top.winfo_screenheight() 

4380 size = tuple(int(_) for _ in top.geometry().split('+')[0].split('x')) 

4381 x = screen_width / 2 - size[0] / 2 

4382 y = screen_height / 2 - size[1] / 2 

4383 top.geometry("+%d+%d" % (x, y)) 

4384 #@+node:felix.20210804130751.1: *3* function: close_server 

4385 def close_Server(): 

4386 """ 

4387 Close the server by stopping the loop 

4388 """ 

4389 print('Closing Leo Server', flush=True) 

4390 if loop.is_running(): 

4391 loop.stop() 

4392 else: 

4393 print('Loop was not running', flush=True) 

4394 #@+node:ekr.20210825172913.1: *3* function: general_yes_no_dialog & helpers 

4395 def general_yes_no_dialog( 

4396 c, 

4397 title, # Not used. 

4398 message=None, # Must exist. 

4399 yesMessage="&Yes", # Not used. 

4400 noMessage="&No", # Not used. 

4401 yesToAllMessage=None, # Not used. 

4402 defaultButton="Yes", # Not used 

4403 cancelMessage=None, # Not used. 

4404 ): 

4405 """ 

4406 Monkey-patched implementation of LeoQtGui.runAskYesNoCancelDialog 

4407 offering *only* Yes/No buttons. 

4408 

4409 This will fallback to a tk implementation if the qt library is unavailable. 

4410 

4411 This raises a dialog and return either 'yes' or 'no'. 

4412 """ 

4413 #@+others # define all helper functions. 

4414 #@+node:ekr.20210801175921.1: *4* function: tk_runAskYesNoCancelDialog & helpers 

4415 def tk_runAskYesNoCancelDialog(c): 

4416 """ 

4417 Tk version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons. 

4418 """ 

4419 if g.unitTesting: 

4420 return None 

4421 root = top = val = None # Non-locals 

4422 #@+others # define helper functions 

4423 #@+node:ekr.20210801180311.4: *5* function: create_yes_no_frame 

4424 def create_yes_no_frame(message, top): 

4425 """Create the dialog's frame.""" 

4426 frame = Tk.Frame(top) 

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

4428 label = Tk.Label(frame, text=message, bg="white") 

4429 label.pack(pady=10) 

4430 # Create buttons. 

4431 f = Tk.Frame(top) 

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

4433 b = Tk.Button(f, width=6, text="Yes", bd=4, underline=0, command=yesButton) 

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

4435 b = Tk.Button(f, width=6, text="No", bd=2, underline=0, command=noButton) 

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

4437 #@+node:ekr.20210801180311.5: *5* function: callbacks 

4438 def noButton(event=None): 

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

4440 nonlocal val 

4441 print(f"Not saved: {c.fileName()}") 

4442 val = "no" 

4443 top.destroy() 

4444 

4445 def yesButton(event=None): 

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

4447 nonlocal val 

4448 print(f"Saved: {c.fileName()}") 

4449 val = "yes" 

4450 top.destroy() 

4451 #@-others 

4452 root = Tk.Tk() 

4453 root.withdraw() 

4454 root.update() 

4455 

4456 top = Tk.Toplevel(root) 

4457 top.title("Saved changed outline?") 

4458 create_yes_no_frame(message, top) 

4459 top.bind("<Return>", yesButton) 

4460 top.bind("y", yesButton) 

4461 top.bind("Y", yesButton) 

4462 top.bind("n", noButton) 

4463 top.bind("N", noButton) 

4464 top.lift() 

4465 

4466 center_tk_frame(top) 

4467 

4468 top.grab_set() # Make the dialog a modal dialog. 

4469 

4470 root.update() 

4471 root.wait_window(top) 

4472 

4473 top.destroy() 

4474 root.destroy() 

4475 return val 

4476 #@+node:ekr.20210825170952.1: *4* function: qt_runAskYesNoCancelDialog 

4477 def qt_runAskYesNoCancelDialog(c): 

4478 """ 

4479 Qt version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons. 

4480 """ 

4481 if g.unitTesting: 

4482 return None 

4483 dialog = QtWidgets.QMessageBox(None) 

4484 dialog.setIcon(Information.Warning) 

4485 dialog.setWindowTitle("Saved changed outline?") 

4486 if message: 

4487 dialog.setText(message) 

4488 # Creation order determines returned value. 

4489 yes = dialog.addButton(yesMessage, ButtonRole.YesRole) 

4490 dialog.addButton(noMessage, ButtonRole.NoRole) 

4491 dialog.setDefaultButton(yes) 

4492 # Set the Leo icon. 

4493 core_dir = os.path.dirname(__file__) 

4494 icon_path = os.path.join(core_dir, "..", "Icons", "leoApp.ico") 

4495 if os.path.exists(icon_path): 

4496 pixmap = QtGui.QPixmap() 

4497 pixmap.load(icon_path) 

4498 icon = QtGui.QIcon(pixmap) 

4499 dialog.setWindowIcon(icon) 

4500 # None of these grabs focus from the console window. 

4501 dialog.raise_() 

4502 dialog.setFocus() 

4503 app.processEvents() # type:ignore 

4504 # val is the same as the creation order. 

4505 # Tested with both Qt6 and Qt5. 

4506 val = dialog.exec() if isQt6 else dialog.exec_() 

4507 if val == 0: 

4508 print(f"Saved: {c.fileName()}") 

4509 return 'yes' 

4510 print(f"Not saved: {c.fileName()}") 

4511 return 'no' 

4512 #@-others 

4513 try: 

4514 # Careful: raise the Tk dialog if there are errors in the Qt code. 

4515 from leo.core.leoQt import isQt6, QtGui, QtWidgets 

4516 from leo.core.leoQt import ButtonRole, Information 

4517 if QtGui and QtWidgets: 

4518 app = QtWidgets.QApplication([]) 

4519 assert app 

4520 val = qt_runAskYesNoCancelDialog(c) 

4521 assert val in ('yes', 'no') 

4522 return val 

4523 except Exception: 

4524 pass 

4525 if Tk: 

4526 return tk_runAskYesNoCancelDialog(c) 

4527 # #2512: There is no way to raise a dialog. 

4528 return 'yes' # Just save the file! 

4529 

4530 #@+node:felix.20210621233316.107: *3* function: get_args 

4531 def get_args(): # pragma: no cover 

4532 """ 

4533 Get arguments from the command line and sets them globally. 

4534 """ 

4535 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile, traces 

4536 

4537 def leo_file(s): 

4538 if os.path.exists(s): 

4539 return s 

4540 print(f"\nNot a .leo file: {s!r}") 

4541 sys.exit(1) 

4542 

4543 description = ''.join([ 

4544 " leoserver.py\n", 

4545 " ------------\n", 

4546 " Offers single or multiple concurrent websockets\n", 

4547 " for JSON based remote-procedure-calls\n", 

4548 " to a shared instance of leo.core.leoBridge\n", 

4549 " \n", 

4550 " Clients may be written in any language:\n", 

4551 " - leo.core.leoclient is an example client written in python.\n", 

4552 " - leoInteg (https://github.com/boltex/leointeg) is written in typescript.\n" 

4553 ]) 

4554 # Usage: 

4555 # leoserver.py [-a <address>] [-p <port>] [-l <limit>] [-f <file>] [--dirty] [--persist] 

4556 usage = 'python leo.core.leoserver [options...]' 

4557 trace_s = 'request,response,verbose' 

4558 valid_traces = [z.strip() for z in trace_s.split(',')] 

4559 parser = argparse.ArgumentParser(description=description, usage=usage, 

4560 formatter_class=argparse.RawTextHelpFormatter) 

4561 add = parser.add_argument 

4562 add('-a', '--address', dest='wsHost', type=str, default=wsHost, metavar='STR', 

4563 help='server address. Defaults to ' + str(wsHost)) 

4564 add('-p', '--port', dest='wsPort', type=int, default=wsPort, metavar='N', 

4565 help='port number. Defaults to ' + str(wsPort)) 

4566 add('-l', '--limit', dest='wsLimit', type=int, default=wsLimit, metavar='N', 

4567 help='maximum number of clients. Defaults to ' + str(wsLimit)) 

4568 add('-f', '--file', dest='argFile', type=leo_file, metavar='PATH', 

4569 help='open a .leo file at startup') 

4570 add('--persist', dest='wsPersist', action='store_true', 

4571 help='do not quit when last client disconnects') 

4572 add('-d', '--dirty', dest='wsSkipDirty', action='store_true', 

4573 help='do not warn about dirty files when quitting') 

4574 add('--trace', dest='traces', type=str, metavar='STRINGS', 

4575 help=f"comma-separated list of {trace_s}") 

4576 add('-v', '--version', dest='v', action='store_true', 

4577 help='show version and exit') 

4578 # Parse 

4579 args = parser.parse_args() 

4580 # Handle the args and set them up globally 

4581 wsHost = args.wsHost 

4582 wsPort = args.wsPort 

4583 wsLimit = args.wsLimit 

4584 wsPersist = bool(args.wsPersist) 

4585 wsSkipDirty = bool(args.wsSkipDirty) 

4586 argFile = args.argFile 

4587 if args.traces: 

4588 ok = True 

4589 for z in args.traces.split(','): 

4590 if z in valid_traces: 

4591 traces.append(z) 

4592 else: 

4593 ok = False 

4594 print(f"Ignoring invalid --trace value: {z!r}", flush=True) 

4595 if not ok: 

4596 print(f"Valid traces are: {','.join(valid_traces)}", flush=True) 

4597 print(f"--trace={','.join(traces)}", flush=True) 

4598 if args.v: 

4599 print(__version__) 

4600 sys.exit(0) 

4601 # Sanitize limit. 

4602 if wsLimit < 1: 

4603 wsLimit = 1 

4604 #@+node:felix.20210803174312.1: *3* function: notify_clients 

4605 async def notify_clients(action, excludedConn=None): 

4606 global connectionsTotal 

4607 if connectionsPool: # asyncio.wait doesn't accept an empty list 

4608 opened = bool(controller.c) # c can be none if no files opened 

4609 m = json.dumps({ 

4610 "async": "refresh", 

4611 "action": action, 

4612 "opened": opened, 

4613 }, separators=(',', ':'), cls=SetEncoder) 

4614 clientSetCopy = connectionsPool.copy() 

4615 if excludedConn: 

4616 clientSetCopy.discard(excludedConn) 

4617 if clientSetCopy: 

4618 # if still at least one to notify 

4619 await asyncio.wait([asyncio.create_task(client.send(m)) for client in clientSetCopy]) 

4620 

4621 #@+node:felix.20210803174312.2: *3* function: register_client 

4622 async def register_client(websocket): 

4623 global connectionsTotal 

4624 connectionsPool.add(websocket) 

4625 await notify_clients("unregister", websocket) 

4626 #@+node:felix.20210807160828.1: *3* function: save_dirty 

4627 def save_dirty(): 

4628 """ 

4629 Ask the user about dirty files if any remained opened. 

4630 """ 

4631 # Monkey-patch the dialog method first. 

4632 g.app.gui.runAskYesNoCancelDialog = general_yes_no_dialog 

4633 # Loop all commanders and 'close' them for dirty check 

4634 commanders = g.app.commanders() 

4635 for commander in commanders: 

4636 if commander.isChanged() and commander.fileName(): 

4637 commander.close() # Patched 'ask' methods will open dialog 

4638 #@+node:felix.20210803174312.3: *3* function: unregister_client 

4639 async def unregister_client(websocket): 

4640 global connectionsTotal 

4641 connectionsPool.remove(websocket) 

4642 await notify_clients("unregister") 

4643 #@+node:felix.20210621233316.106: *3* function: ws_handler (server) 

4644 async def ws_handler(websocket, path): 

4645 """ 

4646 The web socket handler: server.ws_server. 

4647 

4648 It must be a coroutine accepting two arguments: a WebSocketServerProtocol and the request URI. 

4649 """ 

4650 global connectionsTotal, wsLimit 

4651 tag = 'server' 

4652 trace = False 

4653 verbose = False 

4654 connected = False 

4655 

4656 try: 

4657 # Websocket connection startup 

4658 if connectionsTotal >= wsLimit: 

4659 print(f"{tag}: User Refused, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True) 

4660 await websocket.close(1001) 

4661 return 

4662 connected = True # local variable 

4663 connectionsTotal += 1 # global variable 

4664 print(f"{tag}: User Connected, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True) 

4665 # If first connection set it as the main client connection 

4666 controller._init_connection(websocket) 

4667 await register_client(websocket) 

4668 # Start by sending empty as 'ok'. 

4669 n = 0 

4670 await websocket.send(controller._make_response()) 

4671 controller._emit_signon() 

4672 

4673 # Websocket connection message handling loop 

4674 async for json_message in websocket: 

4675 try: 

4676 n += 1 

4677 d = None 

4678 d = json.loads(json_message) 

4679 if trace and verbose: 

4680 print(f"{tag}: got: {d}", flush=True) 

4681 elif trace: 

4682 print(f"{tag}: got: {d}", flush=True) 

4683 answer = controller._do_message(d) 

4684 except TerminateServer as e: 

4685 # pylint: disable=no-value-for-parameter,unexpected-keyword-arg 

4686 raise websockets.exceptions.ConnectionClosed(code=1000, reason=e) 

4687 except ServerError as e: 

4688 data = f"{d}" if d else f"json syntax error: {json_message!r}" 

4689 error = f"{tag}: ServerError: {e}...\n{tag}: {data}" 

4690 print("", flush=True) 

4691 print(error, flush=True) 

4692 print("", flush=True) 

4693 package = { 

4694 "id": controller.current_id, 

4695 "action": controller.action, 

4696 "request": data, 

4697 "ServerError": f"{e}", 

4698 } 

4699 answer = json.dumps(package, separators=(',', ':'), cls=SetEncoder) 

4700 except InternalServerError as e: # pragma: no cover 

4701 print(f"{tag}: InternalServerError {e}", flush=True) 

4702 break 

4703 except Exception as e: # pragma: no cover 

4704 print(f"{tag}: Unexpected Exception! {e}", flush=True) 

4705 g.print_exception() 

4706 print('', flush=True) 

4707 break 

4708 await websocket.send(answer) 

4709 

4710 # If not a 'getter' send refresh signal to other clients 

4711 if controller.action[0:5] != "!get_" and controller.action != "!do_nothing": 

4712 await notify_clients(controller.action, websocket) 

4713 

4714 except websockets.exceptions.ConnectionClosedError as e: # pragma: no cover 

4715 print(f"{tag}: connection closed error: {e}") 

4716 except websockets.exceptions.ConnectionClosed as e: 

4717 print(f"{tag}: connection closed: {e}") 

4718 finally: 

4719 if connected: 

4720 connectionsTotal -= 1 

4721 await unregister_client(websocket) 

4722 print(f"{tag} connection finished. Total: {connectionsTotal}, Limit: {wsLimit}") 

4723 # Check for persistence flag if all connections are closed 

4724 if connectionsTotal == 0 and not wsPersist: 

4725 print("Shutting down leoserver") 

4726 # Preemptive closing of tasks 

4727 for task in asyncio.all_tasks(): 

4728 task.cancel() 

4729 close_Server() # Stops the run_forever loop 

4730 #@-others 

4731 

4732 # Make the first real line of output more visible. 

4733 print("", flush=True) 

4734 

4735 # Sets sHost, wsPort, wsLimit, wsPersist, wsSkipDirty fileArg and traces 

4736 get_args() # Set global values from the command line arguments 

4737 print("Starting LeoBridge... (Launch with -h for help)", flush=True) 

4738 

4739 # Open leoBridge. 

4740 controller = LeoServer() # Single instance of LeoServer, i.e., an instance of leoBridge 

4741 if argFile: 

4742 # Open specified file argument 

4743 try: 

4744 print(f"Opening file: {argFile}", flush=True) 

4745 controller.open_file({"filename": argFile}) 

4746 except Exception: 

4747 print("Opening file failed", flush=True) 

4748 

4749 # Start the server. 

4750 loop = asyncio.get_event_loop() 

4751 

4752 try: 

4753 try: 

4754 server = websockets.serve(ws_handler, wsHost, wsPort, max_size=None) # pylint: disable=no-member 

4755 realtime_server = loop.run_until_complete(server) 

4756 except OSError as e: 

4757 print(e) 

4758 print("Trying with IPv4 Family", flush=True) 

4759 server = websockets.serve(ws_handler, wsHost, wsPort, family=socket.AF_INET, max_size=None) # pylint: disable=no-member 

4760 realtime_server = loop.run_until_complete(server) 

4761 

4762 signon = SERVER_STARTED_TOKEN + f" at {wsHost} on port: {wsPort}.\n" 

4763 if wsPersist: 

4764 signon = signon + "Persistent server\n" 

4765 if wsSkipDirty: 

4766 signon = signon + "No prompt about dirty file(s) when closing server\n" 

4767 if wsLimit > 1: 

4768 signon = signon + f"Total client limit is {wsLimit}.\n" 

4769 signon = signon + "Ctrl+c to break" 

4770 print(signon, flush=True) 

4771 loop.run_forever() 

4772 

4773 except KeyboardInterrupt: 

4774 print("Process interrupted", flush=True) 

4775 

4776 finally: 

4777 # Execution continues here after server is interupted (e.g. with ctrl+c) 

4778 realtime_server.close() 

4779 if not wsSkipDirty: 

4780 print("Checking for changed commanders...", flush=True) 

4781 save_dirty() 

4782 cancel_tasks(asyncio.all_tasks(loop), loop) 

4783 loop.run_until_complete(loop.shutdown_asyncgens()) 

4784 loop.close() 

4785 asyncio.set_event_loop(None) 

4786 print("Stopped leobridge server", flush=True) 

4787#@-others 

4788if __name__ == '__main__': 

4789 # pytest will *not* execute this code. 

4790 main() 

4791#@-leo