Coverage for C:\leo.repo\leo-editor\leo\core\leoserver.py: 58%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#@+leo-ver=5-thin
2#@+node:felix.20210621233316.1: * @file leoserver.py
3#@@language python
4#@@tabwidth -4
5"""
6Leo's internet server.
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.
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
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
79class ServerError(Exception): # pragma: no cover
80 """The server received an erroneous package."""
81 pass
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
91 #@+others
92 #@+node:felix.20210626222905.2: *3* sefc.ctor
93 def __init__(self):
94 """Ctor for ServerExternalFiles class."""
95 super().__init__()
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'
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"
108 g.app.idleTimeManager.add_callback(self.on_idle)
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
121 # check if p_result was from a warn (ok) or an ask ('yes','yes-all','no','no-all')
122 # act accordingly
124 # 1- if ok, unblock 'warn'
125 # 2- if no, unblock 'ask'
126 # ------------------------------------------ Nothing special to do
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
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
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)
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
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()
176 if not g.app or g.app.killed:
177 return
178 if self.waitingForAnswer:
179 return
181 self.on_idle_count += 1
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"
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)
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)
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
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
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
296 _is_leo = path.endswith(('.leo', '.db'))
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 ])
309 package = {"async": "ask", "ask": 'Overwrite the version in Leo?',
310 "message": s, "yes_all": not _is_leo, "no_all": not _is_leo}
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.
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()
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
344 if g.unitTesting or c not in g.app.commanders():
345 return
346 if not p:
347 g.trace('NO P')
348 return
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 ])
358 package = {"async": "warn",
359 "warn": 'External file changed', "message": s}
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)
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
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:
392 #@+others
393 #@+node:felix.20220225003906.2: *3* __init__
394 def __init__(self, c):
395 self.c = c
396 self.lw = [] # empty list
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 = {}
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"]
408 self._search_patterns = []
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"]
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
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
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
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
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()
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)
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))
534 def pushSearchHistory(self, pat):
535 if pat in self._search_patterns:
536 return
537 self._search_patterns = ([pat] + self._search_patterns)[:30]
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()
615 else:
616 hNodes = [self.c.p]
617 bNodes = [self.c.p]
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)
638 hits = numOfHm + lineMatchHits
639 self.lw.insert(0, "{} hits".format(hits))
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)
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
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
724 res = PosList()
726 tc = getattr(c, 'theTagController', None)
727 gnxDict = c.fileCommands.gnxDict
728 key = pat.strip()
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()
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
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.
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
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")
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):
845 import leo.core.leoApp as leoApp
846 import leo.core.leoBridge as leoBridge
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 = []
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})
1036 return rclickList
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
1053 if not button:
1054 raise ServerError(f"{tag}: button {index!r} does not exist")
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, "")
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.
1081 Typescript RClick recursive interface:
1082 RClick: {name: string, children: RClick[]}
1084 Typescript return interface:
1085 {
1086 name: string;
1087 index: string;
1088 rclicks: RClick[];
1089 }[]
1090 """
1091 d = self._check_button_command('get_buttons')
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)
1103 entry = {"name": d[key], "index": str(key), "rclicks": rclickList}
1104 buttons.append(entry)
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")
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")
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)
1195 result = {"total": len(g.app.commanders()), "filename": self.c.fileName()}
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)
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()
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()
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)
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()
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()
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()
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()
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)
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')
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.
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
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}
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}
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.
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()
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})
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)
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.
2401 Set the selection in the wrapper if p == c.p
2403 Package has these keys:
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.
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)
2502 #@+node:felix.20210621233316.66: *5* server.unmark_node
2503 def unmark_node(self, param):
2504 """
2505 Unmark a node.
2506 Try to keep selection, then return the selected node that remains.
2507 """
2508 # pylint: disable=no-else-return
2509 self._check_c()
2510 p = self._get_p(param)
2511 if not p.isMarked():
2512 return self._make_response()
2513 else:
2514 return self.toggle_mark(param)
2515 #@+node:felix.20210621233316.67: *5* server.undo
2516 def undo(self, param):
2517 """Undo last un-doable operation"""
2518 c = self._check_c()
2519 u = c.undoer
2520 if u.canUndo():
2521 u.undo()
2522 # Félix: Caller can get focus using other calls.
2523 return self._make_response()
2524 #@+node:felix.20210621233316.68: *4* server:server commands
2525 #@+node:felix.20210914230846.1: *5* server.get_version
2526 def get_version(self, param):
2527 """
2528 Return this server program name and version as a string representation
2529 along with the three version members as numbers 'major', 'minor' and 'patch'.
2530 """
2531 # uses the __version__ global constant and the v1, v2, v3 global version numbers
2532 result = {"version": __version__, "major": v1, "minor": v2, "patch": v3}
2533 return self._make_minimal_response(result)
2534 #@+node:felix.20220326190000.1: *5* server.get_leoid
2535 def get_leoid(self, param):
2536 """
2537 returns g.app.leoID
2538 """
2539 # uses the __version__ global constant and the v1, v2, v3 global version numbers
2540 result = {"leoID": g.app.leoID}
2541 return self._make_minimal_response(result)
2542 #@+node:felix.20220326190008.1: *5* server.set_leoid
2543 def set_leoid(self, param):
2544 """
2545 sets g.app.leoID
2546 """
2547 # uses the __version__ global constant and the v1, v2, v3 global version numbers
2548 leoID = param.get('leoID', '')
2549 # Same test/fix as in Leo
2550 if leoID:
2551 try:
2552 leoID = leoID.replace('.', '').replace(',', '').replace('"', '').replace("'", '')
2553 # Remove *all* whitespace: https://stackoverflow.com/questions/3739909
2554 leoID = ''.join(leoID.split())
2555 except Exception:
2556 g.es_exception()
2557 leoID = 'None'
2558 if len(leoID) > 2:
2559 g.app.leoID = leoID
2560 g.app.nodeIndices.defaultId = leoID
2561 g.app.nodeIndices.userId = leoID
2562 return self._make_response()
2563 #@+node:felix.20210818012827.1: *5* server.do_nothing
2564 def do_nothing(self, param):
2565 """Simply return states from _make_response"""
2566 return self._make_response()
2567 #@+node:felix.20210621233316.69: *5* server.set_ask_result
2568 def set_ask_result(self, param):
2569 """Got the result to an asked question/warning from client"""
2570 tag = "set_ask_result"
2571 result = param.get("result")
2572 if not result:
2573 raise ServerError(f"{tag}: no param result")
2574 g.app.externalFilesController.clientResult(result)
2575 return self._make_response()
2576 #@+node:felix.20210621233316.70: *5* server.set_config
2577 def set_config(self, param):
2578 """Got auto-reload's config from client"""
2579 self.leoServerConfig = param # PARAM IS THE CONFIG-DICT
2580 return self._make_response()
2581 #@+node:felix.20210621233316.71: *5* server.error
2582 def error(self, param):
2583 """For unit testing. Raise ServerError"""
2584 raise ServerError("error called")
2585 #@+node:felix.20210621233316.72: *5* server.get_all_leo_commands & helper
2586 def get_all_leo_commands(self, param):
2587 """Return a list of all commands that make sense for connected clients."""
2588 tag = 'get_all_leo_commands'
2589 # #173: Use the present commander to get commands created by @button and @command.
2590 c = self.c
2591 d = c.commandsDict if c else {} # keys are command names, values are functions.
2592 bad_names = self._bad_commands(c) # #92.
2593 good_names = self._good_commands()
2594 duplicates = set(bad_names).intersection(set(good_names))
2595 if duplicates: # pragma: no cover
2596 print(f"{tag}: duplicate command names...", flush=True)
2597 for z in sorted(duplicates):
2598 print(z, flush=True)
2599 result = []
2600 for command_name in sorted(d):
2601 func = d.get(command_name)
2602 if not func: # pragma: no cover
2603 print(f"{tag}: no func: {command_name!r}", flush=True)
2604 continue
2605 if command_name in bad_names: # #92.
2606 continue
2607 doc = func.__doc__ or ''
2608 result.append({
2609 "label": command_name, # Kebab-cased Command name to be called
2610 "detail": doc,
2611 })
2612 if self.log_flag: # pragma: no cover
2613 print(f"\n{tag}: {len(result)} leo commands\n", flush=True)
2614 g.printObj([z.get("label") for z in result], tag=tag)
2615 print('', flush=True)
2616 return self._make_minimal_response({"commands": result})
2617 #@+node:felix.20210621233316.73: *6* server._bad_commands
2618 def _bad_commands(self, c):
2619 """Return the list of command names that connected clients should ignore."""
2620 d = c.commandsDict if c else {} # keys are command names, values are functions.
2621 bad = []
2622 #
2623 # leoInteg #173: Remove only vim commands.
2624 for command_name in sorted(d):
2625 if command_name.startswith(':'):
2626 bad.append(command_name)
2627 #
2628 # Remove other commands.
2629 # This is a hand-curated list.
2630 bad_list = [
2631 'demangle-recent-files',
2632 'clean-main-spell-dict',
2633 'clean-persistence',
2634 'clean-recent-files',
2635 'clean-spellpyx',
2636 'clean-user-spell-dict',
2637 'clear-recent-files',
2638 'delete-first-icon',
2639 'delete-last-icon',
2640 'delete-node-icons',
2641 'insert-icon',
2642 'set-ua', # TODO : Should be easy to implement
2643 'export-headlines', # export TODO
2644 'export-jupyter-notebook', # export TODO
2645 'outline-to-cweb', # export TODO
2646 'outline-to-noweb', # export TODO
2647 'remove-sentinels', # import TODO
2649 'save-all',
2650 'save-file-as-zipped',
2651 'write-file-from-node',
2652 'edit-setting',
2653 'edit-shortcut',
2654 'goto-line',
2655 'pdb',
2656 'xdb',
2657 'compare-two-leo-files',
2658 'file-compare-two-leo-files',
2659 'edit-recent-files',
2660 'exit-leo',
2661 'help', # To do.
2662 'help-for-abbreviations',
2663 'help-for-autocompletion',
2664 'help-for-bindings',
2665 'help-for-command',
2666 'help-for-creating-external-files',
2667 'help-for-debugging-commands',
2668 'help-for-drag-and-drop',
2669 'help-for-dynamic-abbreviations',
2670 'help-for-find-commands',
2671 'help-for-keystroke',
2672 'help-for-minibuffer',
2673 'help-for-python',
2674 'help-for-regular-expressions',
2675 'help-for-scripting',
2676 'help-for-settings',
2677 'join-leo-irc', # Some online irc - parameters not working anymore
2679 'print-body',
2680 'print-cmd-docstrings',
2681 'print-expanded-body',
2682 'print-expanded-html',
2683 'print-html',
2684 'print-marked-bodies',
2685 'print-marked-html',
2686 'print-marked-nodes',
2687 'print-node',
2688 'print-sep',
2689 'print-tree-bodies',
2690 'print-tree-html',
2691 'print-tree-nodes',
2692 'print-window-state',
2693 'quit-leo',
2694 'reload-style-sheets',
2695 'save-buffers-kill-leo',
2696 'screen-capture-5sec',
2697 'screen-capture-now',
2698 'set-reference-file', # TODO : maybe offer this
2699 'show-style-sheet',
2700 'sort-recent-files',
2701 'view-lossage',
2703 # Buffers commands (Usage?)
2704 'buffer-append-to',
2705 'buffer-copy',
2706 'buffer-insert',
2707 'buffer-kill',
2708 'buffer-prepend-to',
2709 'buffer-switch-to',
2710 'buffers-list',
2711 'buffers-list-alphabetically',
2713 # Open specific files... (MAYBE MAKE AVAILABLE?)
2714 # 'ekr-projects',
2715 'leo-cheat-sheet', # These duplicates are useful.
2716 'leo-dist-leo',
2717 'leo-docs-leo',
2718 'leo-plugins-leo',
2719 'leo-py-leo',
2720 'leo-quickstart-leo',
2721 'leo-scripts-leo',
2722 'leo-unittest-leo',
2724 # 'scripts',
2725 'settings',
2727 'open-cheat-sheet-leo',
2728 'cheat-sheet-leo',
2729 'cheat-sheet',
2730 'open-desktop-integration-leo',
2731 'desktop-integration-leo',
2732 'open-leo-dist-leo',
2733 'leo-dist-leo',
2734 'open-leo-docs-leo',
2735 'leo-docs-leo',
2736 'open-leo-plugins-leo',
2737 'leo-plugins-leo',
2738 'open-leo-py-leo',
2739 'leo-py-leo',
2740 'open-leo-py-ref-leo',
2741 'leo-py-ref-leo',
2742 'open-leo-py',
2743 'open-leo-settings',
2744 'open-leo-settings-leo',
2745 'open-local-settings',
2746 'my-leo-settings',
2747 'open-my-leo-settings',
2748 'open-my-leo-settings-leo',
2749 'leo-settings'
2750 'open-quickstart-leo',
2751 'leo-quickstart-leo'
2752 'open-scripts-leo',
2753 'leo-scripts-leo'
2754 'open-unittest-leo',
2755 'leo-unittest-leo',
2757 # Open other places...
2758 'desktop-integration-leo',
2760 'open-offline-tutorial',
2761 'open-online-home',
2762 'open-online-toc',
2763 'open-online-tutorials',
2764 'open-online-videos',
2765 'open-recent-file',
2766 'open-theme-file',
2767 'open-url',
2768 'open-url-under-cursor',
2769 'open-users-guide',
2771 # Diffs - needs open file dialog
2772 'diff-and-open-leo-files',
2773 'diff-leo-files',
2775 # --- ORIGINAL BAD COMMANDS START HERE ---
2776 # Abbreviations...
2777 'abbrev-kill-all',
2778 'abbrev-list',
2779 'dabbrev-completion',
2780 'dabbrev-expands',
2782 # Autocompletion...
2783 'auto-complete',
2784 'auto-complete-force',
2785 'disable-autocompleter',
2786 'disable-calltips',
2787 'enable-autocompleter',
2788 'enable-calltips',
2790 # Debugger...
2791 'debug',
2792 'db-again',
2793 'db-b',
2794 'db-c',
2795 'db-h',
2796 'db-input',
2797 'db-l',
2798 'db-n',
2799 'db-q',
2800 'db-r',
2801 'db-s',
2802 'db-status',
2803 'db-w',
2805 # File operations...
2806 'directory-make',
2807 'directory-remove',
2808 'file-delete',
2809 'file-diff-files',
2810 'file-insert',
2811 #'file-new',
2812 #'file-open-by-name',
2814 # All others...
2815 'shell-command',
2816 'shell-command-on-region',
2817 'cheat-sheet',
2818 'dehoist', # Duplicates of de-hoist.
2819 #'find-clone-all',
2820 #'find-clone-all-flattened',
2821 #'find-clone-tag',
2822 #'find-all',
2823 'find-all-unique-regex',
2824 'find-character',
2825 'find-character-extend-selection',
2826 #'find-next',
2827 #'find-prev',
2828 'find-word',
2829 'find-word-in-line',
2831 'global-search',
2833 'isearch-backward',
2834 'isearch-backward-regexp',
2835 'isearch-forward',
2836 'isearch-forward-regexp',
2837 'isearch-with-present-options',
2839 #'replace',
2840 #'replace-all',
2841 'replace-current-character',
2842 #'replace-then-find',
2844 're-search-backward',
2845 're-search-forward',
2847 #'search-backward',
2848 #'search-forward',
2849 'search-return-to-origin',
2851 'set-find-everywhere',
2852 'set-find-node-only',
2853 'set-find-suboutline-only',
2854 'set-replace-string',
2855 'set-search-string',
2857 #'show-find-options',
2859 #'start-search',
2861 'toggle-find-collapses-nodes',
2862 #'toggle-find-ignore-case-option',
2863 #'toggle-find-in-body-option',
2864 #'toggle-find-in-headline-option',
2865 #'toggle-find-mark-changes-option',
2866 #'toggle-find-mark-finds-option',
2867 #'toggle-find-regex-option',
2868 #'toggle-find-word-option',
2869 'toggle-find-wrap-around-option',
2871 'word-search-backward',
2872 'word-search-forward',
2874 # Buttons...
2875 'delete-script-button-button',
2877 # Clicks...
2878 'click-click-box',
2879 'click-icon-box',
2880 'ctrl-click-at-cursor',
2881 'ctrl-click-icon',
2882 'double-click-icon-box',
2883 'right-click-icon',
2885 # Editors...
2886 'add-editor', 'editor-add',
2887 'delete-editor', 'editor-delete',
2888 'detach-editor-toggle',
2889 'detach-editor-toggle-max',
2891 # Focus...
2892 'cycle-editor-focus', 'editor-cycle-focus',
2893 'focus-to-body',
2894 'focus-to-find',
2895 'focus-to-log',
2896 'focus-to-minibuffer',
2897 'focus-to-nav',
2898 'focus-to-spell-tab',
2899 'focus-to-tree',
2901 'tab-cycle-next',
2902 'tab-cycle-previous',
2903 'tab-detach',
2905 # Headlines..
2906 'abort-edit-headline',
2907 'edit-headline',
2908 'end-edit-headline',
2910 # Layout and panes...
2911 'adoc',
2912 'adoc-with-preview',
2914 'contract-body-pane',
2915 'contract-log-pane',
2916 'contract-outline-pane',
2918 'edit-pane-csv',
2919 'edit-pane-test-open',
2920 'equal-sized-panes',
2921 'expand-log-pane',
2922 'expand-body-pane',
2923 'expand-outline-pane',
2925 'free-layout-context-menu',
2926 'free-layout-load',
2927 'free-layout-restore',
2928 'free-layout-zoom',
2930 'zoom-in',
2931 'zoom-out',
2933 # Log
2934 'clear-log',
2936 # Menus...
2937 'activate-cmds-menu',
2938 'activate-edit-menu',
2939 'activate-file-menu',
2940 'activate-help-menu',
2941 'activate-outline-menu',
2942 'activate-plugins-menu',
2943 'activate-window-menu',
2944 'context-menu-open',
2945 'menu-shortcut',
2947 # Modes...
2948 'clear-extend-mode',
2950 # Outline... (Commented off by Félix, Should work)
2951 #'contract-or-go-left',
2952 #'contract-node',
2953 #'contract-parent',
2955 # Scrolling...
2956 'scroll-down-half-page',
2957 'scroll-down-line',
2958 'scroll-down-page',
2959 'scroll-outline-down-line',
2960 'scroll-outline-down-page',
2961 'scroll-outline-left',
2962 'scroll-outline-right',
2963 'scroll-outline-up-line',
2964 'scroll-outline-up-page',
2965 'scroll-up-half-page',
2966 'scroll-up-line',
2967 'scroll-up-page',
2969 # Windows...
2970 'about-leo',
2972 'cascade-windows',
2973 'close-others',
2974 'close-window',
2976 'iconify-frame',
2978 'find-tab-hide',
2979 #'find-tab-open',
2981 'hide-body-dock',
2982 'hide-body-pane',
2983 'hide-invisibles',
2984 'hide-log-pane',
2985 'hide-outline-dock',
2986 'hide-outline-pane',
2987 'hide-tabs-dock',
2989 'minimize-all',
2991 'resize-to-screen',
2993 'show-body-dock',
2994 'show-hide-body-dock',
2995 'show-hide-outline-dock',
2996 'show-hide-render-dock',
2997 'show-hide-tabs-dock',
2998 'show-tabs-dock',
2999 'clean-diff',
3000 'cm-external-editor',
3002 'delete-@button-parse-json-button',
3003 'delete-trace-statements',
3005 'disable-idle-time-events',
3007 'enable-idle-time-events',
3008 'enter-quick-command-mode',
3009 'exit-named-mode',
3011 'F6-open-console',
3013 'flush-lines',
3014 'full-command',
3016 'get-child-headlines',
3018 'history',
3020 'insert-file-name',
3022 'justify-toggle-auto',
3024 'keep-lines',
3025 'keyboard-quit',
3027 'line-number',
3028 'line-numbering-toggle',
3029 'line-to-headline',
3031 'marked-list',
3033 'mode-help',
3035 'open-python-window',
3037 'open-with-idle',
3038 'open-with-open-office',
3039 'open-with-scite',
3040 'open-with-word',
3042 'recolor',
3043 'redraw',
3045 'repeat-complex-command',
3047 'session-clear',
3048 'session-create',
3049 'session-refresh',
3050 'session-restore',
3051 'session-snapshot-load',
3052 'session-snapshot-save',
3054 'set-colors',
3055 'set-command-state',
3056 'set-comment-column',
3057 'set-extend-mode',
3058 'set-fill-column',
3059 'set-fill-prefix',
3060 'set-font',
3061 'set-insert-state',
3062 'set-overwrite-state',
3063 'set-silent-mode',
3065 'show-buttons',
3066 'show-calltips',
3067 'show-calltips-force',
3068 'show-color-names',
3069 'show-color-wheel',
3070 'show-commands',
3071 'show-file-line',
3073 'show-focus',
3074 'show-fonts',
3076 'show-invisibles',
3077 'show-node-uas',
3078 'show-outline-dock',
3079 'show-plugin-handlers',
3080 'show-plugins-info',
3081 'show-settings',
3082 'show-settings-outline',
3083 'show-spell-info',
3084 'show-stats',
3085 'show-tips',
3087 'style-set-selected',
3089 'suspend',
3091 'toggle-abbrev-mode',
3092 'toggle-active-pane',
3093 'toggle-angle-brackets',
3094 'toggle-at-auto-at-edit',
3095 'toggle-autocompleter',
3096 'toggle-calltips',
3097 'toggle-case-region',
3098 'toggle-extend-mode',
3099 'toggle-idle-time-events',
3100 'toggle-input-state',
3101 'toggle-invisibles',
3102 'toggle-line-numbering-root',
3103 'toggle-sparse-move',
3104 'toggle-split-direction',
3106 'what-line',
3107 'eval',
3108 'eval-block',
3109 'eval-last',
3110 'eval-last-pretty',
3111 'eval-replace',
3113 'find-quick',
3114 'find-quick-changed',
3115 'find-quick-selected',
3116 'find-quick-test-failures',
3117 'find-quick-timeline',
3119 #'goto-next-history-node',
3120 #'goto-prev-history-node',
3122 'preview',
3123 'preview-body',
3124 'preview-expanded-body',
3125 'preview-expanded-html',
3126 'preview-html',
3127 'preview-marked-bodies',
3128 'preview-marked-html',
3129 'preview-marked-nodes',
3130 'preview-node',
3131 'preview-tree-bodies',
3132 'preview-tree-html',
3133 'preview-tree-nodes',
3135 'spell-add',
3136 'spell-as-you-type-next',
3137 'spell-as-you-type-toggle',
3138 'spell-as-you-type-undo',
3139 'spell-as-you-type-wrap',
3140 'spell-change',
3141 'spell-change-then-find',
3142 'spell-find',
3143 'spell-ignore',
3144 'spell-tab-hide',
3145 'spell-tab-open',
3147 #'tag-children',
3149 'todo-children-todo',
3150 'todo-dec-pri',
3151 'todo-find-todo',
3152 'todo-fix-datetime',
3153 'todo-inc-pri',
3155 'vr',
3156 'vr-contract',
3157 'vr-expand',
3158 'vr-hide',
3159 'vr-lock',
3160 'vr-pause-play-movie',
3161 'vr-show',
3162 'vr-toggle',
3163 'vr-unlock',
3164 'vr-update',
3165 'vr-zoom',
3167 'vs-create-tree',
3168 'vs-dump',
3169 'vs-reset',
3170 'vs-update',
3171 # Connected client's text editing commands should cover all of these...
3172 'add-comments',
3173 'add-space-to-lines',
3174 'add-tab-to-lines',
3175 'align-eq-signs',
3177 'back-char',
3178 'back-char-extend-selection',
3179 'back-page',
3180 'back-page-extend-selection',
3181 'back-paragraph',
3182 'back-paragraph-extend-selection',
3183 'back-sentence',
3184 'back-sentence-extend-selection',
3185 'back-to-home',
3186 'back-to-home-extend-selection',
3187 'back-to-indentation',
3188 'back-word',
3189 'back-word-extend-selection',
3190 'back-word-smart',
3191 'back-word-smart-extend-selection',
3192 'backward-delete-char',
3193 'backward-delete-word',
3194 'backward-delete-word-smart',
3195 'backward-find-character',
3196 'backward-find-character-extend-selection',
3197 'backward-kill-paragraph',
3198 'backward-kill-sentence',
3199 'backward-kill-word',
3200 'beginning-of-buffer',
3201 'beginning-of-buffer-extend-selection',
3202 'beginning-of-line',
3203 'beginning-of-line-extend-selection',
3205 'capitalize-word',
3206 'center-line',
3207 'center-region',
3208 'clean-all-blank-lines',
3209 'clean-all-lines',
3210 'clean-body',
3211 'clean-lines',
3212 'clear-kill-ring',
3213 'clear-selected-text',
3214 'convert-blanks',
3215 'convert-tabs',
3216 'copy-text',
3217 'cut-text',
3219 'delete-char',
3220 'delete-comments',
3221 'delete-indentation',
3222 'delete-spaces',
3223 'delete-word',
3224 'delete-word-smart',
3225 'downcase-region',
3226 'downcase-word',
3228 'end-of-buffer',
3229 'end-of-buffer-extend-selection',
3230 'end-of-line',
3231 'end-of-line-extend-selection',
3233 'exchange-point-mark',
3235 'extend-to-line',
3236 'extend-to-paragraph',
3237 'extend-to-sentence',
3238 'extend-to-word',
3240 'fill-paragraph',
3241 'fill-region',
3242 'fill-region-as-paragraph',
3244 'finish-of-line',
3245 'finish-of-line-extend-selection',
3247 'forward-char',
3248 'forward-char-extend-selection',
3249 'forward-end-word',
3250 'forward-end-word-extend-selection',
3251 'forward-page',
3252 'forward-page-extend-selection',
3253 'forward-paragraph',
3254 'forward-paragraph-extend-selection',
3255 'forward-sentence',
3256 'forward-sentence-extend-selection',
3257 'forward-word',
3258 'forward-word-extend-selection',
3259 'forward-word-smart',
3260 'forward-word-smart-extend-selection',
3262 'go-anywhere',
3263 'go-back',
3264 'go-forward',
3265 'goto-char',
3267 'indent-region',
3268 'indent-relative',
3269 'indent-rigidly',
3270 'indent-to-comment-column',
3272 'insert-hard-tab',
3273 'insert-newline',
3274 'insert-parentheses',
3275 'insert-soft-tab',
3277 'kill-line',
3278 'kill-paragraph',
3279 'kill-pylint',
3280 'kill-region',
3281 'kill-region-save',
3282 'kill-sentence',
3283 'kill-to-end-of-line',
3284 'kill-word',
3285 'kill-ws',
3287 'match-brackets',
3289 'move-lines-down',
3290 'move-lines-up',
3291 'move-past-close',
3292 'move-past-close-extend-selection',
3294 'newline-and-indent',
3295 'next-line',
3296 'next-line-extend-selection',
3297 'next-or-end-of-line',
3298 'next-or-end-of-line-extend-selection',
3300 'previous-line',
3301 'previous-line-extend-selection',
3302 'previous-or-beginning-of-line',
3303 'previous-or-beginning-of-line-extend-selection',
3305 'rectangle-clear',
3306 'rectangle-close',
3307 'rectangle-delete',
3308 'rectangle-kill',
3309 'rectangle-open',
3310 'rectangle-string',
3311 'rectangle-yank',
3313 'remove-blank-lines',
3314 'remove-newlines',
3315 'remove-space-from-lines',
3316 'remove-tab-from-lines',
3318 'reverse-region',
3319 'reverse-sort-lines',
3320 'reverse-sort-lines-ignoring-case',
3322 'paste-text',
3323 'pop-cursor',
3324 'push-cursor',
3326 'select-all',
3327 'select-next-trace-statement',
3328 'select-to-matching-bracket',
3330 'sort-columns',
3331 'sort-fields',
3332 'sort-lines',
3333 'sort-lines-ignoring-case',
3335 'split-defs',
3336 'split-line',
3338 'start-of-line',
3339 'start-of-line-extend-selection',
3341 'tabify',
3342 'transpose-chars',
3343 'transpose-lines',
3344 'transpose-words',
3346 'unformat-paragraph',
3347 'unindent-region',
3349 'untabify',
3351 'upcase-region',
3352 'upcase-word',
3353 'update-ref-file',
3355 'yank',
3356 'yank-pop',
3358 'zap-to-character',
3360 ]
3361 bad.extend(bad_list)
3362 result = list(sorted(bad))
3363 return result
3364 #@+node:felix.20210621233316.74: *6* server._good_commands
3365 def _good_commands(self):
3366 """Defined commands that should be available in a connected client"""
3367 good_list = [
3369 'contract-all',
3370 'contract-all-other-nodes',
3371 'clone-node',
3372 'copy-node',
3373 'copy-marked-nodes',
3374 'cut-node',
3376 'de-hoist',
3377 'delete-marked-nodes',
3378 'delete-node',
3379 # 'demangle-recent-files',
3380 'demote',
3381 'do-nothing',
3382 'expand-and-go-right',
3383 'expand-next-level',
3384 'expand-node',
3385 'expand-or-go-right',
3386 'expand-prev-level',
3387 'expand-to-level-1',
3388 'expand-to-level-2',
3389 'expand-to-level-3',
3390 'expand-to-level-4',
3391 'expand-to-level-5',
3392 'expand-to-level-6',
3393 'expand-to-level-7',
3394 'expand-to-level-8',
3395 'expand-to-level-9',
3396 'expand-all',
3397 'expand-all-subheads',
3398 'expand-ancestors-only',
3400 'find-next-clone',
3402 'goto-first-node',
3403 'goto-first-sibling',
3404 'goto-first-visible-node',
3405 'goto-last-node',
3406 'goto-last-sibling',
3407 'goto-last-visible-node',
3408 'goto-next-changed',
3409 'goto-next-clone',
3410 'goto-next-marked',
3411 'goto-next-node',
3412 'goto-next-sibling',
3413 'goto-next-visible',
3414 'goto-parent',
3415 'goto-prev-marked',
3416 'goto-prev-node',
3417 'goto-prev-sibling',
3418 'goto-prev-visible',
3420 'hoist',
3422 'insert-node',
3423 'insert-node-before',
3424 'insert-as-first-child',
3425 'insert-as-last-child',
3426 'insert-child',
3428 'mark',
3429 'mark-changed-items',
3430 'mark-first-parents',
3431 'mark-subheads',
3433 'move-marked-nodes',
3434 'move-outline-down',
3435 'move-outline-left',
3436 'move-outline-right',
3437 'move-outline-up',
3439 'paste-node',
3440 'paste-retaining-clones',
3441 'promote',
3442 'promote-bodies',
3443 'promote-headlines',
3445 'sort-children',
3446 'sort-siblings',
3448 'tangle',
3449 'tangle-all',
3450 'tangle-marked',
3452 'unmark-all',
3453 'unmark-first-parents',
3454 #'clean-main-spell-dict',
3455 #'clean-persistence',
3456 #'clean-recent-files',
3457 #'clean-spellpyx',
3458 #'clean-user-spell-dict',
3460 'clear-all-caches',
3461 'clear-all-hoists',
3462 'clear-all-uas',
3463 'clear-cache',
3464 'clear-node-uas',
3465 #'clear-recent-files',
3467 #'delete-first-icon', # ? maybe move to bad commands?
3468 #'delete-last-icon', # ? maybe move to bad commands?
3469 #'delete-node-icons', # ? maybe move to bad commands?
3471 'dump-caches',
3472 'dump-clone-parents',
3473 'dump-expanded',
3474 'dump-node',
3475 'dump-outline',
3477 #'insert-icon', # ? maybe move to bad commands?
3479 #'set-ua',
3481 'show-all-uas',
3482 'show-bindings',
3483 'show-clone-ancestors',
3484 'show-clone-parents',
3486 # Export files...
3487 #'export-headlines', # export
3488 #'export-jupyter-notebook', # export
3489 #'outline-to-cweb', # export
3490 #'outline-to-noweb', # export
3491 #'remove-sentinels', # import
3492 'typescript-to-py',
3494 # Import files... # done through import all
3495 'import-MORE-files',
3496 'import-file',
3497 'import-free-mind-files',
3498 'import-jupyter-notebook',
3499 'import-legacy-external-files',
3500 'import-mind-jet-files',
3501 'import-tabbed-files',
3502 'import-todo-text-files',
3503 'import-zim-folder',
3505 # Read outlines...
3506 'read-at-auto-nodes',
3507 'read-at-file-nodes',
3508 'read-at-shadow-nodes',
3509 'read-file-into-node',
3510 'read-outline-only',
3511 'read-ref-file',
3513 # Save Files.
3514 'file-save',
3515 'file-save-as',
3516 'file-save-by-name',
3517 'file-save-to',
3518 'save',
3519 'save-as',
3520 'save-file',
3521 'save-file-as',
3522 'save-file-by-name',
3523 'save-file-to',
3524 'save-to',
3526 # Write parts of outlines...
3527 'write-at-auto-nodes',
3528 'write-at-file-nodes',
3529 'write-at-shadow-nodes',
3530 'write-dirty-at-auto-nodes',
3531 'write-dirty-at-file-nodes',
3532 'write-dirty-at-shadow-nodes',
3533 'write-edited-recent-files',
3534 #'write-file-from-node',
3535 'write-missing-at-file-nodes',
3536 'write-outline-only',
3538 'clone-find-all',
3539 'clone-find-all-flattened',
3540 'clone-find-all-flattened-marked',
3541 'clone-find-all-marked',
3542 'clone-find-parents',
3543 'clone-find-tag',
3544 'clone-marked-nodes',
3545 'clone-node-to-last-node',
3547 'clone-to-at-spot',
3549 #'edit-setting',
3550 #'edit-shortcut',
3552 'execute-pytest',
3553 'execute-script',
3554 'extract',
3555 'extract-names',
3557 'goto-any-clone',
3558 'goto-global-line',
3559 #'goto-line',
3560 'git-diff', 'gd',
3562 'log-kill-listener', 'kill-log-listener',
3563 'log-listen', 'listen-to-log',
3565 'make-stub-files',
3567 #'pdb',
3569 'redo',
3570 'rst3',
3571 'run-all-unit-tests-externally',
3572 'run-all-unit-tests-locally',
3573 'run-marked-unit-tests-externally',
3574 'run-marked-unit-tests-locally',
3575 'run-selected-unit-tests-externally',
3576 'run-selected-unit-tests-locally',
3577 'run-tests',
3579 'undo',
3581 #'xdb',
3583 # Beautify, blacken, fstringify...
3584 'beautify-files',
3585 'beautify-files-diff',
3586 'blacken-files',
3587 'blacken-files-diff',
3588 #'diff-and-open-leo-files',
3589 'diff-beautify-files',
3590 'diff-fstringify-files',
3591 #'diff-leo-files',
3592 'diff-marked-nodes',
3593 'fstringify-files',
3594 'fstringify-files-diff',
3595 'fstringify-files-silent',
3596 'pretty-print-c',
3597 'silent-fstringify-files',
3599 # All other commands...
3600 'at-file-to-at-auto',
3602 'beautify-c',
3604 'cls',
3605 'c-to-python',
3606 'c-to-python-clean-docs',
3607 'check-derived-file',
3608 'check-outline',
3609 'code-to-rst',
3610 #'compare-two-leo-files',
3611 'convert-all-blanks',
3612 'convert-all-tabs',
3613 'count-children',
3614 'count-pages',
3615 'count-region',
3617 #'desktop-integration-leo',
3619 #'edit-recent-files',
3620 #'exit-leo',
3622 #'file-compare-two-leo-files',
3623 'find-def',
3624 'find-long-lines',
3625 'find-missing-docstrings',
3626 'flake8-files',
3627 'flatten-outline',
3628 'flatten-outline-to-node',
3629 'flatten-script',
3631 'gc-collect-garbage',
3632 'gc-dump-all-objects',
3633 'gc-dump-new-objects',
3634 'gc-dump-objects-verbose',
3635 'gc-show-summary',
3637 #'help', # To do.
3638 #'help-for-abbreviations',
3639 #'help-for-autocompletion',
3640 #'help-for-bindings',
3641 #'help-for-command',
3642 #'help-for-creating-external-files',
3643 #'help-for-debugging-commands',
3644 #'help-for-drag-and-drop',
3645 #'help-for-dynamic-abbreviations',
3646 #'help-for-find-commands',
3647 #'help-for-keystroke',
3648 #'help-for-minibuffer',
3649 #'help-for-python',
3650 #'help-for-regular-expressions',
3651 #'help-for-scripting',
3652 #'help-for-settings',
3654 'insert-body-time', # ?
3655 'insert-headline-time',
3656 'insert-jupyter-toc',
3657 'insert-markdown-toc',
3659 'find-var',
3661 #'join-leo-irc',
3662 'join-node-above',
3663 'join-node-below',
3664 'join-selection-to-node-below',
3666 'move-lines-to-next-node',
3668 'new',
3670 'open-outline',
3672 'parse-body',
3673 'parse-json',
3674 'pandoc',
3675 'pandoc-with-preview',
3676 'paste-as-template',
3678 #'print-body',
3679 #'print-cmd-docstrings',
3680 #'print-expanded-body',
3681 #'print-expanded-html',
3682 #'print-html',
3683 #'print-marked-bodies',
3684 #'print-marked-html',
3685 #'print-marked-nodes',
3686 #'print-node',
3687 #'print-sep',
3688 #'print-tree-bodies',
3689 #'print-tree-html',
3690 #'print-tree-nodes',
3691 #'print-window-state',
3693 'pyflakes',
3694 'pylint',
3695 'pylint-kill',
3696 'python-to-coffeescript',
3698 #'quit-leo',
3700 'reformat-body',
3701 'reformat-paragraph',
3702 'refresh-from-disk',
3703 'reload-settings',
3704 #'reload-style-sheets',
3705 'revert',
3707 #'save-buffers-kill-leo',
3708 #'screen-capture-5sec',
3709 #'screen-capture-now',
3710 'script-button', # ?
3711 #'set-reference-file',
3712 #'show-style-sheet',
3713 #'sort-recent-files',
3714 'sphinx',
3715 'sphinx-with-preview',
3716 'style-reload', # ?
3718 'untangle',
3719 'untangle-all',
3720 'untangle-marked',
3722 #'view-lossage', # ?
3724 'weave',
3726 # Dubious commands (to do)...
3727 'act-on-node',
3729 'cfa',
3730 'cfam',
3731 'cff',
3732 'cffm',
3733 'cft',
3735 #'buffer-append-to',
3736 #'buffer-copy',
3737 #'buffer-insert',
3738 #'buffer-kill',
3739 #'buffer-prepend-to',
3740 #'buffer-switch-to',
3741 #'buffers-list',
3742 #'buffers-list-alphabetically',
3744 'chapter-back',
3745 'chapter-next',
3746 'chapter-select',
3747 'chapter-select-main',
3748 'create-def-list', # ?
3749 ]
3750 return good_list
3751 #@+node:felix.20210621233316.75: *5* server.get_all_server_commands & helpers
3752 def get_all_server_commands(self, param):
3753 """
3754 Public server method:
3755 Return the names of all callable public methods of the server.
3756 """
3757 tag = 'get_all_server_commands'
3758 names = self._get_all_server_commands()
3759 if self.log_flag: # pragma: no cover
3760 print(f"\n{tag}: {len(names)} server commands\n", flush=True)
3761 g.printObj(names, tag=tag)
3762 print('', flush=True)
3763 return self._make_response({"server-commands": names})
3764 #@+node:felix.20210914231602.1: *6* _get_all_server_commands
3765 def _get_all_server_commands(self):
3766 """
3767 Private server method:
3768 Return the names of all callable public methods of the server.
3769 (Methods that do not start with an underscore '_')
3770 """
3771 members = inspect.getmembers(self, inspect.ismethod)
3772 return sorted([name for (name, value) in members if not name.startswith('_')])
3773 #@+node:felix.20210621233316.76: *5* server.init_connection
3774 def _init_connection(self, web_socket): # pragma: no cover (tested in client).
3775 """Begin the connection."""
3776 global connectionsTotal
3777 if connectionsTotal == 1:
3778 # First connection, so "Master client" setup
3779 self.web_socket = web_socket
3780 self.loop = asyncio.get_event_loop()
3781 else:
3782 # already exist, so "spectator-clients" setup
3783 pass # nothing for now
3784 #@+node:felix.20210621233316.77: *5* server.shut_down
3785 def shut_down(self, param):
3786 """Shut down the server."""
3787 tag = 'shut_down'
3788 n = len(g.app.commanders())
3789 if n: # pragma: no cover
3790 raise ServerError(f"{tag}: {n} open outlines")
3791 raise TerminateServer("client requested shut down")
3792 #@+node:felix.20210621233316.78: *3* server:server utils
3793 #@+node:felix.20210621233316.79: *4* server._ap_to_p
3794 def _ap_to_p(self, ap):
3795 """
3796 Convert ap (archived position, a dict) to a valid Leo position.
3798 Return False on any kind of error to support calls to invalid positions
3799 after a document has been closed of switched and interface interaction
3800 in the client generated incoming calls to 'getters' already sent. (for the
3801 now inaccessible leo document commander.)
3802 """
3803 tag = '_ap_to_p'
3804 c = self._check_c()
3805 gnx_d = c.fileCommands.gnxDict
3806 try:
3807 outer_stack = ap.get('stack')
3808 if outer_stack is None: # pragma: no cover.
3809 raise ServerError(f"{tag}: no stack in ap: {ap!r}")
3810 if not isinstance(outer_stack, (list, tuple)): # pragma: no cover.
3811 raise ServerError(f"{tag}: stack must be tuple or list: {outer_stack}")
3812 #
3813 def d_to_childIndex_v(d):
3814 """Helper: return childIndex and v from d ["childIndex"] and d["gnx"]."""
3815 childIndex = d.get('childIndex')
3816 if childIndex is None: # pragma: no cover.
3817 raise ServerError(f"{tag}: no childIndex in {d}")
3818 try:
3819 childIndex = int(childIndex)
3820 except Exception: # pragma: no cover.
3821 raise ServerError(f"{tag}: bad childIndex: {childIndex!r}")
3822 gnx = d.get('gnx')
3823 if gnx is None: # pragma: no cover.
3824 raise ServerError(f"{tag}: no gnx in {d}.")
3825 v = gnx_d.get(gnx)
3826 if v is None: # pragma: no cover.
3827 raise ServerError(f"{tag}: gnx not found: {gnx!r}")
3828 return childIndex, v
3829 #
3830 # Compute p.childIndex and p.v.
3831 childIndex, v = d_to_childIndex_v(ap)
3832 #
3833 # Create p.stack.
3834 stack = []
3835 for stack_d in outer_stack:
3836 stack_childIndex, stack_v = d_to_childIndex_v(stack_d)
3837 stack.append((stack_v, stack_childIndex))
3838 #
3839 # Make p and check p.
3840 p = Position(v, childIndex, stack)
3841 if not c.positionExists(p): # pragma: no cover.
3842 raise ServerError(f"{tag}: p does not exist in {c.shortFileName()}")
3843 except Exception:
3844 if self.log_flag or traces:
3845 print(
3846 f"{tag}: Bad ap: {ap!r}\n"
3847 # f"{tag}: position: {p!r}\n"
3848 f"{tag}: v {v!r} childIndex: {childIndex!r}\n"
3849 f"{tag}: stack: {stack!r}", flush=True)
3850 return False # Return false on any error so caller can react
3851 return p
3852 #@+node:felix.20210621233316.80: *4* server._check_c
3853 def _check_c(self):
3854 """Return self.c or raise ServerError if self.c is None."""
3855 tag = '_check_c'
3856 c = self.c
3857 if not c: # pragma: no cover
3858 raise ServerError(f"{tag}: no open commander")
3859 return c
3860 #@+node:felix.20210621233316.81: *4* server._check_outline
3861 def _check_outline(self, c):
3862 """Check self.c for consistency."""
3863 # Check that all positions exist.
3864 self._check_outline_positions(c)
3865 # Test round-tripping.
3866 self._test_round_trip_positions(c)
3867 #@+node:felix.20210621233316.82: *4* server._check_outline_positions
3868 def _check_outline_positions(self, c):
3869 """Verify that all positions in c exist."""
3870 tag = '_check_outline_positions'
3871 for p in c.all_positions(copy=False):
3872 if not c.positionExists(p): # pragma: no cover
3873 message = f"{tag}: position {p!r} does not exist in {c.shortFileName()}"
3874 print(message, flush=True)
3875 self._dump_position(p)
3876 raise ServerError(message)
3877 #@+node:felix.20210621233316.84: *4* server._do_leo_command_by_name
3878 def _do_leo_command_by_name(self, command_name, param):
3879 """
3880 Generic call to a command in Leo's Commands class or any subcommander class.
3882 The param["ap"] position is to be selected before having the command run,
3883 while the param["keep"] parameter specifies wether the original position
3884 should be re-selected afterward.
3886 TODO: The whole of those operations is to be undoable as one undo step.
3888 command_name: the name of a Leo command (a kebab-cased string).
3889 param["ap"]: an archived position.
3890 param["keep"]: preserve the current selection, if possible.
3892 """
3893 tag = '_do_leo_command_by_name'
3894 c = self._check_c()
3896 if command_name in self.bad_commands_list: # pragma: no cover
3897 raise ServerError(f"{tag}: disallowed command: {command_name!r}")
3899 keepSelection = False # Set default, optional component of param
3900 if "keep" in param:
3901 keepSelection = param["keep"]
3903 func = c.commandsDict.get(command_name) # Getting from kebab-cased 'Command Name'
3904 if not func: # pragma: no cover
3905 raise ServerError(f"{tag}: Leo command not found: {command_name!r}")
3907 p = self._get_p(param)
3908 try:
3909 if p == c.p:
3910 value = func(event={"c": c}) # no need for re-selection
3911 else:
3912 old_p = c.p # preserve old position
3913 c.selectPosition(p) # set position upon which to perform the command
3914 value = func(event={"c": c})
3915 if keepSelection and c.positionExists(old_p):
3916 # Only if 'keep' old position was set, and old_p still exists
3917 c.selectPosition(old_p)
3918 except Exception as e:
3919 print(f"_do_leo_command Recovered from Error {e!s}", flush=True)
3920 return self._make_response() # Return empty on error
3921 #
3922 # Tag along a possible return value with info sent back by _make_response
3923 if self._is_jsonable(value):
3924 return self._make_response({"return-value": value})
3925 return self._make_response()
3926 #@+node:ekr.20210722184932.1: *4* server._do_leo_function_by_name
3927 def _do_leo_function_by_name(self, function_name, param):
3928 """
3929 Generic call to a method in Leo's Commands class or any subcommander class.
3931 The param["ap"] position is to be selected before having the command run,
3932 while the param["keep"] parameter specifies wether the original position
3933 should be re-selected afterward.
3935 TODO: The whole of those operations is to be undoable as one undo step.
3937 command: the name of a method
3938 param["ap"]: an archived position.
3939 param["keep"]: preserve the current selection, if possible.
3941 """
3942 tag = '_do_leo_function_by_name'
3943 c = self._check_c()
3945 keepSelection = False # Set default, optional component of param
3946 if "keep" in param:
3947 keepSelection = param["keep"]
3949 func = self._get_commander_method(function_name) # GET FUNC
3950 if not func: # pragma: no cover
3951 raise ServerError(f"{tag}: Leo command not found: {function_name!r}")
3953 p = self._get_p(param)
3954 try:
3955 if p == c.p:
3956 value = func(event={"c": c}) # no need for re-selection
3957 else:
3958 old_p = c.p # preserve old position
3959 c.selectPosition(p) # set position upon which to perform the command
3960 value = func(event={"c": c})
3961 if keepSelection and c.positionExists(old_p):
3962 # Only if 'keep' old position was set, and old_p still exists
3963 c.selectPosition(old_p)
3964 except Exception as e:
3965 print(f"_do_leo_command Recovered from Error {e!s}", flush=True)
3966 return self._make_response() # Return empty on error
3967 #
3968 # Tag along a possible return value with info sent back by _make_response
3969 if self._is_jsonable(value):
3970 return self._make_response({"return-value": value})
3971 return self._make_response()
3972 #@+node:felix.20210621233316.85: *4* server._do_message
3973 def _do_message(self, d):
3974 """
3975 Handle d, a python dict representing the incoming request.
3976 The d dict must have the three (3) following keys:
3978 "id": A positive integer.
3980 "action": A string, which is either:
3981 - The name of public method of this class, prefixed with '!'.
3982 - The name of a Leo command, prefixed with '-'
3983 - The name of a method of a Leo class, without prefix.
3985 "param": A dict to be passed to the called "action" method.
3986 (Passed to the public method, or the _do_leo_command. Often contains ap, text & keep)
3988 Return a dict, created by _make_response or _make_minimal_response
3989 that contains at least an 'id' key.
3991 """
3992 global traces
3993 tag = '_do_message'
3994 trace, verbose = 'request' in traces, 'verbose' in traces
3996 # Require "id" and "action" keys
3997 id_ = d.get("id")
3998 if id_ is None: # pragma: no cover
3999 raise ServerError(f"{tag}: no id")
4000 action = d.get("action")
4001 if action is None: # pragma: no cover
4002 raise ServerError(f"{tag}: no action")
4004 # TODO : make/force always an object from the client connected.
4005 param = d.get('param', {}) # Can be none or a string
4006 # Set log flag.
4007 if param:
4008 self.log_flag = param.get("log")
4009 pass
4010 else:
4011 param = {}
4013 # Handle traces.
4014 if trace and verbose: # pragma: no cover
4015 g.printObj(d, tag=f"request {id_}")
4016 print('', flush=True)
4017 elif trace: # pragma: no cover
4018 keys = sorted(param.keys())
4019 if action == '!set_config':
4020 keys_s = f"({len(keys)} keys)"
4021 elif len(keys) > 5:
4022 keys_s = '\n ' + '\n '.join(keys)
4023 else:
4024 keys_s = ', '.join(keys)
4025 print(f" request {id_:<4} {action:<30} {keys_s}", flush=True)
4027 # Set the current_id and action ivars for _make_response.
4028 self.current_id = id_
4029 self.action = action
4031 # Execute the requested action.
4032 if action[0] == "!":
4033 action = action[1:] # Remove exclamation point "!"
4034 func = self._do_server_command # Server has this method.
4035 elif action[0] == '-':
4036 action = action[1:] # Remove dash "-"
4037 func = self._do_leo_command_by_name # It's a command name.
4038 else:
4039 func = self._do_leo_function_by_name # It's the name of a method in some commander.
4040 result = func(action, param)
4041 if result is None: # pragma: no cover
4042 raise ServerError(f"{tag}: no response: {action!r}")
4043 return result
4044 #@+node:felix.20210621233316.86: *4* server._do_server_command
4045 def _do_server_command(self, action, param):
4046 tag = '_do_server_command'
4047 # Disallow hidden methods.
4048 if action.startswith('_'): # pragma: no cover
4049 raise ServerError(f"{tag}: action starts with '_': {action!r}")
4050 # Find and execute the server method.
4051 func = getattr(self, action, None)
4052 if not func:
4053 raise ServerError(f"{tag}: action not found: {action!r}") # pragma: no cover
4054 if not callable(func):
4055 raise ServerError(f"{tag}: not callable: {func!r}") # pragma: no cover
4056 return func(param)
4057 #@+node:felix.20210621233316.87: *4* server._dump_*
4058 def _dump_outline(self, c): # pragma: no cover
4059 """Dump the outline."""
4060 tag = '_dump_outline'
4061 print(f"{tag}: {c.shortFileName()}...\n", flush=True)
4062 for p in c.all_positions():
4063 self._dump_position(p)
4064 print('', flush=True)
4066 def _dump_position(self, p): # pragma: no cover
4067 level_s = ' ' * 2 * p.level()
4068 print(f"{level_s}{p.childIndex():2} {p.v.gnx} {p.h}", flush=True)
4069 #@+node:felix.20210624160812.1: *4* server._emit_signon
4070 def _emit_signon(self):
4071 """Simulate the Initial Leo Log Entry"""
4072 tag = 'emit_signon'
4073 if self.loop:
4074 g.app.computeSignon()
4075 signon = []
4076 for z in (g.app.signon, g.app.signon1):
4077 for z2 in z.split('\n'):
4078 signon.append(z2.strip())
4079 g.es("\n".join(signon))
4080 else:
4081 raise ServerError(f"{tag}: no loop ready for emit_signon")
4082 #@+node:felix.20210625230236.1: *4* server._get_commander_method
4083 def _get_commander_method(self, command):
4084 """ Return the given method (p_command) in the Commands class or subcommanders."""
4085 # First, try the commands class.
4086 c = self._check_c()
4087 func = getattr(c, command, None)
4088 if func:
4089 return func
4090 # Otherwise, search all subcommanders for the method.
4091 table = ( # This table comes from c.initObjectIvars.
4092 'abbrevCommands',
4093 'bufferCommands',
4094 'chapterCommands',
4095 'controlCommands',
4096 'convertCommands',
4097 'debugCommands',
4098 'editCommands',
4099 'editFileCommands',
4100 'evalController',
4101 'gotoCommands',
4102 'helpCommands',
4103 'keyHandler',
4104 'keyHandlerCommands',
4105 'killBufferCommands',
4106 'leoCommands',
4107 'leoTestManager',
4108 'macroCommands',
4109 'miniBufferWidget',
4110 'printingController',
4111 'queryReplaceCommands',
4112 'rectangleCommands',
4113 'searchCommands',
4114 'spellCommands',
4115 'vimCommands', # Not likely to be useful.
4116 )
4117 for ivar in table:
4118 subcommander = getattr(c, ivar, None)
4119 if subcommander:
4120 func = getattr(subcommander, command, None)
4121 if func:
4122 return func
4123 return None
4124 #@+node:felix.20210621233316.91: *4* server._get_focus
4125 def _get_focus(self):
4126 """Server helper method to get the focused panel name string"""
4127 tag = '_get_focus'
4128 try:
4129 w = g.app.gui.get_focus()
4130 focus = g.app.gui.widget_name(w)
4131 except Exception as e:
4132 raise ServerError(f"{tag}: exception trying to get the focused widget: {e}")
4133 return focus
4134 #@+node:felix.20210621233316.90: *4* server._get_p
4135 def _get_p(self, param, strict=False):
4136 """
4137 Return _ap_to_p(param["ap"]) or c.p.,
4138 or False if the strict flag is set
4139 """
4140 tag = '_get_ap'
4141 c = self.c
4142 if not c: # pragma: no cover
4143 raise ServerError(f"{tag}: no c")
4145 ap = param.get("ap")
4146 if ap:
4147 p = self._ap_to_p(ap) # Convertion
4148 if p:
4149 if not c.positionExists(p): # pragma: no cover
4150 raise ServerError(f"{tag}: position does not exist. ap: {ap!r}")
4151 return p # Return the position
4152 if strict:
4153 return False
4154 # Fallback to c.p
4155 if not c.p: # pragma: no cover
4156 raise ServerError(f"{tag}: no c.p")
4158 return c.p
4159 #@+node:felix.20210621233316.92: *4* server._get_position_d
4160 def _get_position_d(self, p):
4161 """
4162 Return a python dict that is adding
4163 graphical representation data and flags
4164 to the base 'ap' dict from _p_to_ap.
4165 (To be used by the connected client GUI.)
4166 """
4167 d = self._p_to_ap(p)
4168 d['headline'] = p.h
4169 d['level'] = p.level()
4170 if p.v.u:
4171 if g.leoServer.leoServerConfig and g.leoServer.leoServerConfig.get("uAsBoolean", False):
4172 # uAsBoolean is 'thruthy'
4173 d['u'] = True
4174 else:
4175 # Normal output if no options set
4176 d['u'] = p.v.u
4177 if bool(p.b):
4178 d['hasBody'] = True
4179 if p.hasChildren():
4180 d['hasChildren'] = True
4181 if p.isCloned():
4182 d['cloned'] = True
4183 if p.isDirty():
4184 d['dirty'] = True
4185 if p.isExpanded():
4186 d['expanded'] = True
4187 if p.isMarked():
4188 d['marked'] = True
4189 if p.isAnyAtFileNode():
4190 d['atFile'] = True
4191 if p == self.c.p:
4192 d['selected'] = True
4193 return d
4194 #@+node:felix.20210705211625.1: *4* server._is_jsonable
4195 def _is_jsonable(self, x):
4196 try:
4197 json.dumps(x, cls=SetEncoder)
4198 return True
4199 except(TypeError, OverflowError):
4200 return False
4201 #@+node:felix.20210621233316.94: *4* server._make_minimal_response
4202 def _make_minimal_response(self, package=None):
4203 """
4204 Return a json string representing a response dict.
4206 The 'package' kwarg, if present, must be a python dict describing a
4207 response. package may be an empty dict or None.
4209 The 'p' kwarg, if present, must be a position.
4211 First, this method creates a response (a python dict) containing all
4212 the keys in the 'package' dict.
4214 Then it adds 'id' to the package.
4216 Finally, this method returns the json string corresponding to the
4217 response.
4218 """
4219 if package is None:
4220 package = {}
4222 # Always add id.
4223 package["id"] = self.current_id
4225 return json.dumps(package, separators=(',', ':'), cls=SetEncoder)
4226 #@+node:felix.20210621233316.93: *4* server._make_response
4227 def _make_response(self, package=None):
4228 """
4229 Return a json string representing a response dict.
4231 The 'package' kwarg, if present, must be a python dict describing a
4232 response. package may be an empty dict or None.
4234 The 'p' kwarg, if present, must be a position.
4236 First, this method creates a response (a python dict) containing all
4237 the keys in the 'package' dict, with the following added keys:
4239 - "id": The incoming id.
4240 - "commander": A dict describing self.c.
4241 - "node": None, or an archived position describing self.c.p.
4243 Finally, this method returns the json string corresponding to the
4244 response.
4245 """
4246 global traces
4247 tag = '_make_response'
4248 trace = self.log_flag or 'response' in traces
4249 verbose = 'verbose' in traces
4250 c = self.c # It is valid for c to be None.
4251 if package is None:
4252 package = {}
4253 p = package.get("p")
4254 if p:
4255 del package["p"]
4256 # Raise an *internal* error if checks fail.
4257 if isinstance(package, str): # pragma: no cover
4258 raise InternalServerError(f"{tag}: bad package kwarg: {package!r}")
4259 if p and not isinstance(p, Position): # pragma: no cover
4260 raise InternalServerError(f"{tag}: bad p kwarg: {p!r}")
4261 if p and not c: # pragma: no cover
4262 raise InternalServerError(f"{tag}: p but not c")
4263 if p and not c.positionExists(p): # pragma: no cover
4264 raise InternalServerError(f"{tag}: p does not exist: {p!r}")
4265 if c and not c.p: # pragma: no cover
4266 raise InternalServerError(f"{tag}: empty c.p")
4268 # Always add id
4269 package["id"] = self.current_id
4271 # The following keys are relevant only if there is an open commander.
4272 if c:
4273 # Allow commands, especially _get_redraw_d, to specify p!
4274 p = p or c.p
4275 package["commander"] = {
4276 "changed": c.isChanged(),
4277 "fileName": c.fileName(), # Can be None for new files.
4278 }
4279 # Add all the node data, including:
4280 # - "node": self._p_to_ap(p) # Contains p.gnx, p.childIndex and p.stack.
4281 # - All the *cheap* redraw data for p.
4282 redraw_d = self._get_position_d(p)
4283 package["node"] = redraw_d
4285 # Handle traces.
4286 if trace and verbose: # pragma: no cover
4287 g.printObj(package, tag=f"response {self.current_id}")
4288 print('', flush=True)
4289 elif trace: # pragma: no cover
4290 keys = sorted(package.keys())
4291 keys_s = ', '.join(keys)
4292 print(f"response {self.current_id:<4} {keys_s}", flush=True)
4294 return json.dumps(package, separators=(',', ':'), cls=SetEncoder)
4295 #@+node:felix.20210621233316.95: *4* server._p_to_ap
4296 def _p_to_ap(self, p):
4297 """
4298 * From Leo plugin leoflexx.py *
4300 Convert Leo position p to a serializable archived position.
4302 This returns only position-related data.
4303 get_position_data returns all data needed to redraw the screen.
4304 """
4305 self._check_c()
4306 stack = [{'gnx': v.gnx, 'childIndex': childIndex}
4307 for (v, childIndex) in p.stack]
4308 return {
4309 'childIndex': p._childIndex,
4310 'gnx': p.v.gnx,
4311 'stack': stack,
4312 }
4313 #@+node:felix.20210621233316.96: *4* server._positionFromGnx
4314 def _positionFromGnx(self, gnx):
4315 """Return first p node with this gnx or false"""
4316 c = self._check_c()
4317 for p in c.all_unique_positions():
4318 if p.v.gnx == gnx:
4319 return p
4320 return False
4321 #@+node:felix.20210622232409.1: *4* server._send_async_output & helper
4322 def _send_async_output(self, package, toAll=False):
4323 """
4324 Send data asynchronously to the client
4325 """
4326 tag = "send async output"
4327 jsonPackage = json.dumps(package, separators=(',', ':'), cls=SetEncoder)
4328 if "async" not in package:
4329 InternalServerError(f"\n{tag}: async member missing in package {jsonPackage} \n")
4330 if self.loop:
4331 self.loop.create_task(self._async_output(jsonPackage, toAll))
4332 else:
4333 InternalServerError(f"\n{tag}: loop not ready {jsonPackage} \n")
4334 #@+node:felix.20210621233316.89: *5* server._async_output
4335 async def _async_output(self, json, toAll=False): # pragma: no cover (tested in server)
4336 """Output json string to the web_socket"""
4337 global connectionsTotal
4338 tag = '_async_output'
4339 outputBytes = bytes(json, 'utf-8')
4340 if toAll:
4341 if connectionsPool: # asyncio.wait doesn't accept an empty list
4342 await asyncio.wait([asyncio.create_task(client.send(outputBytes)) for client in connectionsPool])
4343 else:
4344 g.trace(f"{tag}: no web socket. json: {json!r}")
4345 else:
4346 if self.web_socket:
4347 await self.web_socket.send(outputBytes)
4348 else:
4349 g.trace(f"{tag}: no web socket. json: {json!r}")
4350 #@+node:felix.20210621233316.97: *4* server._test_round_trip_positions
4351 def _test_round_trip_positions(self, c): # pragma: no cover (tested in client).
4352 """Test the round tripping of p_to_ap and ap_to_p."""
4353 tag = '_test_round_trip_positions'
4354 for p in c.all_unique_positions():
4355 ap = self._p_to_ap(p)
4356 p2 = self._ap_to_p(ap)
4357 if p != p2:
4358 self._dump_outline(c)
4359 raise ServerError(f"{tag}: round-trip failed: ap: {ap!r}, p: {p!r}, p2: {p2!r}")
4360 #@+node:felix.20210625002950.1: *4* server._yieldAllRootChildren
4361 def _yieldAllRootChildren(self):
4362 """Return all root children P nodes"""
4363 c = self._check_c()
4364 p = c.rootPosition()
4365 while p:
4366 yield p
4367 p.moveToNext()
4369 #@-others
4370#@+node:felix.20210621233316.105: ** function: main & helpers
4371def main(): # pragma: no cover (tested in client)
4372 """python script for leo integration via leoBridge"""
4373 # pylint: disable=used-before-assignment
4374 global websockets
4375 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile
4376 if not websockets:
4377 print('websockets not found')
4378 print('pip install websockets')
4379 return
4381 #@+others
4382 #@+node:felix.20210807214524.1: *3* function: cancel_tasks
4383 def cancel_tasks(to_cancel, loop):
4384 if not to_cancel:
4385 return
4387 for task in to_cancel:
4388 task.cancel()
4390 loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
4392 for task in to_cancel:
4393 if task.cancelled():
4394 continue
4395 if task.exception() is not None:
4396 loop.call_exception_handler(
4397 {
4398 "message": "unhandled exception during asyncio.run() shutdown",
4399 "exception": task.exception(),
4400 "task": task,
4401 }
4402 )
4403 #@+node:ekr.20210825115746.1: *3* function: center_tk_frame
4404 def center_tk_frame(top):
4405 """Center the top-level Frame."""
4406 # https://stackoverflow.com/questions/3352918
4407 top.update_idletasks()
4408 screen_width = top.winfo_screenwidth()
4409 screen_height = top.winfo_screenheight()
4410 size = tuple(int(_) for _ in top.geometry().split('+')[0].split('x'))
4411 x = screen_width / 2 - size[0] / 2
4412 y = screen_height / 2 - size[1] / 2
4413 top.geometry("+%d+%d" % (x, y))
4414 #@+node:felix.20210804130751.1: *3* function: close_server
4415 def close_Server():
4416 """
4417 Close the server by stopping the loop
4418 """
4419 print('Closing Leo Server', flush=True)
4420 if loop.is_running():
4421 loop.stop()
4422 else:
4423 print('Loop was not running', flush=True)
4424 #@+node:ekr.20210825172913.1: *3* function: general_yes_no_dialog & helpers
4425 def general_yes_no_dialog(
4426 c,
4427 title, # Not used.
4428 message=None, # Must exist.
4429 yesMessage="&Yes", # Not used.
4430 noMessage="&No", # Not used.
4431 yesToAllMessage=None, # Not used.
4432 defaultButton="Yes", # Not used
4433 cancelMessage=None, # Not used.
4434 ):
4435 """
4436 Monkey-patched implementation of LeoQtGui.runAskYesNoCancelDialog
4437 offering *only* Yes/No buttons.
4439 This will fallback to a tk implementation if the qt library is unavailable.
4441 This raises a dialog and return either 'yes' or 'no'.
4442 """
4443 #@+others # define all helper functions.
4444 #@+node:ekr.20210801175921.1: *4* function: tk_runAskYesNoCancelDialog & helpers
4445 def tk_runAskYesNoCancelDialog(c):
4446 """
4447 Tk version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons.
4448 """
4449 if g.unitTesting:
4450 return None
4451 root = top = val = None # Non-locals
4452 #@+others # define helper functions
4453 #@+node:ekr.20210801180311.4: *5* function: create_yes_no_frame
4454 def create_yes_no_frame(message, top):
4455 """Create the dialog's frame."""
4456 frame = Tk.Frame(top)
4457 frame.pack(side="top", expand=1, fill="both")
4458 label = Tk.Label(frame, text=message, bg="white")
4459 label.pack(pady=10)
4460 # Create buttons.
4461 f = Tk.Frame(top)
4462 f.pack(side="top", padx=30)
4463 b = Tk.Button(f, width=6, text="Yes", bd=4, underline=0, command=yesButton)
4464 b.pack(side="left", padx=5, pady=10)
4465 b = Tk.Button(f, width=6, text="No", bd=2, underline=0, command=noButton)
4466 b.pack(side="left", padx=5, pady=10)
4467 #@+node:ekr.20210801180311.5: *5* function: callbacks
4468 def noButton(event=None):
4469 """Do default click action in ok button."""
4470 nonlocal val
4471 print(f"Not saved: {c.fileName()}")
4472 val = "no"
4473 top.destroy()
4475 def yesButton(event=None):
4476 """Do default click action in ok button."""
4477 nonlocal val
4478 print(f"Saved: {c.fileName()}")
4479 val = "yes"
4480 top.destroy()
4481 #@-others
4482 root = Tk.Tk()
4483 root.withdraw()
4484 root.update()
4486 top = Tk.Toplevel(root)
4487 top.title("Saved changed outline?")
4488 create_yes_no_frame(message, top)
4489 top.bind("<Return>", yesButton)
4490 top.bind("y", yesButton)
4491 top.bind("Y", yesButton)
4492 top.bind("n", noButton)
4493 top.bind("N", noButton)
4494 top.lift()
4496 center_tk_frame(top)
4498 top.grab_set() # Make the dialog a modal dialog.
4500 root.update()
4501 root.wait_window(top)
4503 top.destroy()
4504 root.destroy()
4505 return val
4506 #@+node:ekr.20210825170952.1: *4* function: qt_runAskYesNoCancelDialog
4507 def qt_runAskYesNoCancelDialog(c):
4508 """
4509 Qt version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons.
4510 """
4511 if g.unitTesting:
4512 return None
4513 dialog = QtWidgets.QMessageBox(None)
4514 dialog.setIcon(Information.Warning)
4515 dialog.setWindowTitle("Saved changed outline?")
4516 if message:
4517 dialog.setText(message)
4518 # Creation order determines returned value.
4519 yes = dialog.addButton(yesMessage, ButtonRole.YesRole)
4520 dialog.addButton(noMessage, ButtonRole.NoRole)
4521 dialog.setDefaultButton(yes)
4522 # Set the Leo icon.
4523 core_dir = os.path.dirname(__file__)
4524 icon_path = os.path.join(core_dir, "..", "Icons", "leoApp.ico")
4525 if os.path.exists(icon_path):
4526 pixmap = QtGui.QPixmap()
4527 pixmap.load(icon_path)
4528 icon = QtGui.QIcon(pixmap)
4529 dialog.setWindowIcon(icon)
4530 # None of these grabs focus from the console window.
4531 dialog.raise_()
4532 dialog.setFocus()
4533 app.processEvents() # type:ignore
4534 # val is the same as the creation order.
4535 # Tested with both Qt6 and Qt5.
4536 val = dialog.exec() if isQt6 else dialog.exec_()
4537 if val == 0:
4538 print(f"Saved: {c.fileName()}")
4539 return 'yes'
4540 print(f"Not saved: {c.fileName()}")
4541 return 'no'
4542 #@-others
4543 try:
4544 # Careful: raise the Tk dialog if there are errors in the Qt code.
4545 from leo.core.leoQt import isQt6, QtGui, QtWidgets
4546 from leo.core.leoQt import ButtonRole, Information
4547 if QtGui and QtWidgets:
4548 app = QtWidgets.QApplication([])
4549 assert app
4550 val = qt_runAskYesNoCancelDialog(c)
4551 assert val in ('yes', 'no')
4552 return val
4553 except Exception:
4554 pass
4555 if Tk:
4556 return tk_runAskYesNoCancelDialog(c)
4557 # #2512: There is no way to raise a dialog.
4558 return 'yes' # Just save the file!
4560 #@+node:felix.20210621233316.107: *3* function: get_args
4561 def get_args(): # pragma: no cover
4562 """
4563 Get arguments from the command line and sets them globally.
4564 """
4565 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile, traces
4567 def leo_file(s):
4568 if os.path.exists(s):
4569 return s
4570 print(f"\nNot a .leo file: {s!r}")
4571 sys.exit(1)
4573 description = ''.join([
4574 " leoserver.py\n",
4575 " ------------\n",
4576 " Offers single or multiple concurrent websockets\n",
4577 " for JSON based remote-procedure-calls\n",
4578 " to a shared instance of leo.core.leoBridge\n",
4579 " \n",
4580 " Clients may be written in any language:\n",
4581 " - leo.core.leoclient is an example client written in python.\n",
4582 " - leoInteg (https://github.com/boltex/leointeg) is written in typescript.\n"
4583 ])
4584 # Usage:
4585 # leoserver.py [-a <address>] [-p <port>] [-l <limit>] [-f <file>] [--dirty] [--persist]
4586 usage = 'python leo.core.leoserver [options...]'
4587 trace_s = 'request,response,verbose'
4588 valid_traces = [z.strip() for z in trace_s.split(',')]
4589 parser = argparse.ArgumentParser(description=description, usage=usage,
4590 formatter_class=argparse.RawTextHelpFormatter)
4591 add = parser.add_argument
4592 add('-a', '--address', dest='wsHost', type=str, default=wsHost, metavar='STR',
4593 help='server address. Defaults to ' + str(wsHost))
4594 add('-p', '--port', dest='wsPort', type=int, default=wsPort, metavar='N',
4595 help='port number. Defaults to ' + str(wsPort))
4596 add('-l', '--limit', dest='wsLimit', type=int, default=wsLimit, metavar='N',
4597 help='maximum number of clients. Defaults to ' + str(wsLimit))
4598 add('-f', '--file', dest='argFile', type=leo_file, metavar='PATH',
4599 help='open a .leo file at startup')
4600 add('--persist', dest='wsPersist', action='store_true',
4601 help='do not quit when last client disconnects')
4602 add('-d', '--dirty', dest='wsSkipDirty', action='store_true',
4603 help='do not warn about dirty files when quitting')
4604 add('--trace', dest='traces', type=str, metavar='STRINGS',
4605 help=f"comma-separated list of {trace_s}")
4606 add('-v', '--version', dest='v', action='store_true',
4607 help='show version and exit')
4608 # Parse
4609 args = parser.parse_args()
4610 # Handle the args and set them up globally
4611 wsHost = args.wsHost
4612 wsPort = args.wsPort
4613 wsLimit = args.wsLimit
4614 wsPersist = bool(args.wsPersist)
4615 wsSkipDirty = bool(args.wsSkipDirty)
4616 argFile = args.argFile
4617 if args.traces:
4618 ok = True
4619 for z in args.traces.split(','):
4620 if z in valid_traces:
4621 traces.append(z)
4622 else:
4623 ok = False
4624 print(f"Ignoring invalid --trace value: {z!r}", flush=True)
4625 if not ok:
4626 print(f"Valid traces are: {','.join(valid_traces)}", flush=True)
4627 print(f"--trace={','.join(traces)}", flush=True)
4628 if args.v:
4629 print(__version__)
4630 sys.exit(0)
4631 # Sanitize limit.
4632 if wsLimit < 1:
4633 wsLimit = 1
4634 #@+node:felix.20210803174312.1: *3* function: notify_clients
4635 async def notify_clients(action, excludedConn=None):
4636 global connectionsTotal
4637 if connectionsPool: # asyncio.wait doesn't accept an empty list
4638 opened = bool(controller.c) # c can be none if no files opened
4639 m = json.dumps({
4640 "async": "refresh",
4641 "action": action,
4642 "opened": opened,
4643 }, separators=(',', ':'), cls=SetEncoder)
4644 clientSetCopy = connectionsPool.copy()
4645 if excludedConn:
4646 clientSetCopy.discard(excludedConn)
4647 if clientSetCopy:
4648 # if still at least one to notify
4649 await asyncio.wait([asyncio.create_task(client.send(m)) for client in clientSetCopy])
4651 #@+node:felix.20210803174312.2: *3* function: register_client
4652 async def register_client(websocket):
4653 global connectionsTotal
4654 connectionsPool.add(websocket)
4655 await notify_clients("unregister", websocket)
4656 #@+node:felix.20210807160828.1: *3* function: save_dirty
4657 def save_dirty():
4658 """
4659 Ask the user about dirty files if any remained opened.
4660 """
4661 # Monkey-patch the dialog method first.
4662 g.app.gui.runAskYesNoCancelDialog = general_yes_no_dialog
4663 # Loop all commanders and 'close' them for dirty check
4664 commanders = g.app.commanders()
4665 for commander in commanders:
4666 if commander.isChanged() and commander.fileName():
4667 commander.close() # Patched 'ask' methods will open dialog
4668 #@+node:felix.20210803174312.3: *3* function: unregister_client
4669 async def unregister_client(websocket):
4670 global connectionsTotal
4671 connectionsPool.remove(websocket)
4672 await notify_clients("unregister")
4673 #@+node:felix.20210621233316.106: *3* function: ws_handler (server)
4674 async def ws_handler(websocket, path):
4675 """
4676 The web socket handler: server.ws_server.
4678 It must be a coroutine accepting two arguments: a WebSocketServerProtocol and the request URI.
4679 """
4680 global connectionsTotal, wsLimit
4681 tag = 'server'
4682 trace = False
4683 verbose = False
4684 connected = False
4686 try:
4687 # Websocket connection startup
4688 if connectionsTotal >= wsLimit:
4689 print(f"{tag}: User Refused, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True)
4690 await websocket.close(1001)
4691 return
4692 connected = True # local variable
4693 connectionsTotal += 1 # global variable
4694 print(f"{tag}: User Connected, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True)
4695 # If first connection set it as the main client connection
4696 controller._init_connection(websocket)
4697 await register_client(websocket)
4698 # Start by sending empty as 'ok'.
4699 n = 0
4700 await websocket.send(controller._make_response({"leoID": g.app.leoID}))
4701 controller._emit_signon()
4703 # Websocket connection message handling loop
4704 async for json_message in websocket:
4705 try:
4706 n += 1
4707 d = None
4708 d = json.loads(json_message)
4709 if trace and verbose:
4710 print(f"{tag}: got: {d}", flush=True)
4711 elif trace:
4712 print(f"{tag}: got: {d}", flush=True)
4713 answer = controller._do_message(d)
4714 except TerminateServer as e:
4715 # pylint: disable=no-value-for-parameter,unexpected-keyword-arg
4716 raise websockets.exceptions.ConnectionClosed(code=1000, reason=e)
4717 except ServerError as e:
4718 data = f"{d}" if d else f"json syntax error: {json_message!r}"
4719 error = f"{tag}: ServerError: {e}...\n{tag}: {data}"
4720 print("", flush=True)
4721 print(error, flush=True)
4722 print("", flush=True)
4723 package = {
4724 "id": controller.current_id,
4725 "action": controller.action,
4726 "request": data,
4727 "ServerError": f"{e}",
4728 }
4729 answer = json.dumps(package, separators=(',', ':'), cls=SetEncoder)
4730 except InternalServerError as e: # pragma: no cover
4731 print(f"{tag}: InternalServerError {e}", flush=True)
4732 break
4733 except Exception as e: # pragma: no cover
4734 print(f"{tag}: Unexpected Exception! {e}", flush=True)
4735 g.print_exception()
4736 print('', flush=True)
4737 break
4738 await websocket.send(answer)
4740 # If not a 'getter' send refresh signal to other clients
4741 if controller.action[0:5] != "!get_" and controller.action != "!do_nothing":
4742 await notify_clients(controller.action, websocket)
4744 except websockets.exceptions.ConnectionClosedError as e: # pragma: no cover
4745 print(f"{tag}: connection closed error: {e}")
4746 except websockets.exceptions.ConnectionClosed as e:
4747 print(f"{tag}: connection closed: {e}")
4748 finally:
4749 if connected:
4750 connectionsTotal -= 1
4751 await unregister_client(websocket)
4752 print(f"{tag} connection finished. Total: {connectionsTotal}, Limit: {wsLimit}")
4753 # Check for persistence flag if all connections are closed
4754 if connectionsTotal == 0 and not wsPersist:
4755 print("Shutting down leoserver")
4756 # Preemptive closing of tasks
4757 for task in asyncio.all_tasks():
4758 task.cancel()
4759 close_Server() # Stops the run_forever loop
4760 #@-others
4762 # Make the first real line of output more visible.
4763 print("", flush=True)
4765 # Sets sHost, wsPort, wsLimit, wsPersist, wsSkipDirty fileArg and traces
4766 get_args() # Set global values from the command line arguments
4767 print("Starting LeoBridge... (Launch with -h for help)", flush=True)
4769 # Open leoBridge.
4770 controller = LeoServer() # Single instance of LeoServer, i.e., an instance of leoBridge
4771 if argFile:
4772 # Open specified file argument
4773 try:
4774 print(f"Opening file: {argFile}", flush=True)
4775 controller.open_file({"filename": argFile})
4776 except Exception:
4777 print("Opening file failed", flush=True)
4779 # Start the server.
4780 loop = asyncio.get_event_loop()
4782 try:
4783 try:
4784 server = websockets.serve(ws_handler, wsHost, wsPort, max_size=None) # pylint: disable=no-member
4785 realtime_server = loop.run_until_complete(server)
4786 except OSError as e:
4787 print(e)
4788 print("Trying with IPv4 Family", flush=True)
4789 server = websockets.serve(ws_handler, wsHost, wsPort, family=socket.AF_INET, max_size=None) # pylint: disable=no-member
4790 realtime_server = loop.run_until_complete(server)
4792 signon = SERVER_STARTED_TOKEN + f" at {wsHost} on port: {wsPort}.\n"
4793 if wsPersist:
4794 signon = signon + "Persistent server\n"
4795 if wsSkipDirty:
4796 signon = signon + "No prompt about dirty file(s) when closing server\n"
4797 if wsLimit > 1:
4798 signon = signon + f"Total client limit is {wsLimit}.\n"
4799 signon = signon + "Ctrl+c to break"
4800 print(signon, flush=True)
4801 loop.run_forever()
4803 except KeyboardInterrupt:
4804 print("Process interrupted", flush=True)
4806 finally:
4807 # Execution continues here after server is interupted (e.g. with ctrl+c)
4808 realtime_server.close()
4809 if not wsSkipDirty:
4810 print("Checking for changed commanders...", flush=True)
4811 save_dirty()
4812 cancel_tasks(asyncio.all_tasks(loop), loop)
4813 loop.run_until_complete(loop.shutdown_asyncgens())
4814 loop.close()
4815 asyncio.set_event_loop(None)
4816 print("Stopped leobridge server", flush=True)
4817#@-others
4818if __name__ == '__main__':
4819 # pytest will *not* execute this code.
4820 main()
4821#@-leo