Coverage for C:\leo.repo\leo-editor\leo\core\leoserver.py : 59%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#@+leo-ver=5-thin
2#@+node:felix.20210621233316.1: * @file leoserver.py
3#@@language python
4#@@tabwidth -4
5"""
6Leo's internet server.
8Written by Félix Malboeuf and Edward K. Ream.
9"""
10# pylint: disable=import-self,raise-missing-from,wrong-import-position
11#@+<< imports >>
12#@+node:felix.20210621233316.2: ** << imports >>
13import argparse
14import asyncio
15import inspect
16import json
17import os
18import sys
19import socket
20import textwrap
21import time
22import tkinter as Tk
23from typing import List, Union
24# Third-party.
25# #2300
26try:
27 import websockets
28except Exception:
29 websockets = None
30# Make sure leo-editor folder is on sys.path.
31core_dir = os.path.dirname(__file__)
32leo_path = os.path.normpath(os.path.join(core_dir, '..', '..'))
33assert os.path.exists(leo_path), repr(leo_path)
34if leo_path not in sys.path:
35 sys.path.append(leo_path)
36# Leo
37from leo.core.leoNodes import Position
38from leo.core.leoGui import StringFindTabManager
39from leo.core.leoExternalFiles import ExternalFilesController
40#@-<< imports >>
41version_tuple = (1, 0, 1)
42v1, v2, v3 = version_tuple
43__version__ = f"leoserver.py version {v1}.{v2}.{v3}"
44g = None # The bridge's leoGlobals module.
46# Server defaults
47SERVER_STARTED_TOKEN = "LeoBridge started" # Output when started successfully
48# Websocket connections (to be sent 'notify' messages)
49connectionsPool = set() # type:ignore
50connectionsTotal = 0 # Current connected client total
51# Customizable server options
52argFile = ""
53traces: List = []
54wsLimit = 1
55wsPersist = False
56wsSkipDirty = False
57wsHost = "localhost"
58wsPort = 32125
60#@+others
61#@+node:felix.20210712224107.1: ** setup JSON encoder
62class SetEncoder(json.JSONEncoder):
63 def default(self, obj):
64 if isinstance(obj, set):
65 return list(obj)
66 return json.JSONEncoder.default(self, obj)
67#@+node:felix.20210621233316.3: ** Exception classes
68class InternalServerError(Exception): # pragma: no cover
69 """The server violated its own coding conventions."""
70 pass
72class ServerError(Exception): # pragma: no cover
73 """The server received an erroneous package."""
74 pass
76class TerminateServer(Exception): # pragma: no cover
77 """Ask the server to terminate."""
78 pass
79#@+node:felix.20210626222905.1: ** class ServerExternalFilesController
80class ServerExternalFilesController(ExternalFilesController):
81 """EFC Modified from Leo's sources"""
82 # pylint: disable=no-else-return
84 #@+others
85 #@+node:felix.20210626222905.2: *3* sefc.ctor
86 def __init__(self):
87 """Ctor for ExternalFiles class."""
88 super().__init__()
90 self.on_idle_count = 0
91 # Keys are full paths, values are modification times.
92 # DO NOT alter directly, use set_time(path) and
93 # get_time(path), see set_time() for notes.
94 self.yesno_all_time: Union[None, bool, float] = None # previous yes/no to all answer, time of answer
95 self.yesno_all_answer = None # answer, 'yes-all', or 'no-all'
97 # if yesAll/noAll forced, then just show info message after idle_check_commander
98 self.infoMessage = None
99 # False or "detected", "refreshed" or "ignored"
101 g.app.idleTimeManager.add_callback(self.on_idle)
103 self.waitingForAnswer = False
104 self.lastPNode = None # last p node that was asked for if not set to "AllYes\AllNo"
105 self.lastCommander = None
106 #@+node:felix.20210626222905.6: *3* sefc.clientResult
107 def clientResult(self, p_result):
108 """Received result from connected client that was 'asked' yes/no/... """
109 # Got the result to an asked question/warning from the client
110 if not self.waitingForAnswer:
111 print("ERROR: Received Result but no Asked Dialog", flush=True)
112 return
114 # check if p_result was from a warn (ok) or an ask ('yes','yes-all','no','no-all')
115 # act accordingly
117 # 1- if ok, unblock 'warn'
118 # 2- if no, unblock 'ask'
119 # ------------------------------------------ Nothing special to do
121 # 3- if noAll: set noAll, and unblock 'ask'
122 if p_result and "-all" in p_result.lower():
123 self.yesno_all_time = time.time()
124 self.yesno_all_answer = p_result.lower()
125 # ------------------------------------------ Also covers setting yesAll in #5
127 path = ""
128 if self.lastPNode:
129 path = g.fullPath(self.lastCommander, self.lastPNode)
130 # 4- if yes: REFRESH self.lastPNode, and unblock 'ask'
131 # 5- if yesAll: REFRESH self.lastPNode, set yesAll, and unblock 'ask'
132 if bool(p_result and 'yes' in p_result.lower()):
133 self.lastCommander.selectPosition(self.lastPNode)
134 self.lastCommander.refreshFromDisk()
135 elif self.lastCommander:
136 path = self.lastCommander.fileName()
137 # 6- Same but for Leo file commander (close and reopen .leo file)
138 if bool(p_result and 'yes' in p_result.lower()):
139 # self.lastCommander.close() Stops too much if last file closed
140 g.app.closeLeoWindow(self.lastCommander.frame, finish_quit=False)
141 g.leoServer.open_file({"filename":path }) # ignore returned value
143 # Always update the path & time to prevent future warnings for this path.
144 if path:
145 self.set_time(path)
146 self.checksum_d[path] = self.checksum(path)
148 self.waitingForAnswer = False # unblock
149 # unblock: run the loop as if timer had hit
150 if self.lastCommander:
151 self.idle_check_commander(self.lastCommander)
152 #@+node:felix.20210714205425.1: *3* sefc.entries
153 #@+node:felix.20210626222905.19: *4* sefc.check_overwrite
154 def check_overwrite(self, c, path):
155 if self.has_changed(path):
156 package = {"async": "info", "message": "Overwritten "+ path}
157 g.leoServer._send_async_output(package, True)
158 return True
160 #@+node:felix.20210714205604.1: *4* sefc.on_idle & helpers
161 def on_idle(self):
162 """
163 Check for changed open-with files and all external files in commanders
164 for which @bool check_for_changed_external_file is True.
165 """
166 # Fix for flushing the terminal console to pass through
167 sys.stdout.flush()
169 if not g.app or g.app.killed:
170 return
171 if self.waitingForAnswer:
172 return
174 self.on_idle_count += 1
176 if self.unchecked_commanders:
177 # Check the next commander for which
178 # @bool check_for_changed_external_file is True.
179 c = self.unchecked_commanders.pop()
180 self.lastCommander = c
181 self.lastPNode = None # when none, a client result means its for the leo file.
182 self.idle_check_commander(c)
183 else:
184 # Add all commanders for which
185 # @bool check_for_changed_external_file is True.
186 self.unchecked_commanders = [
187 z for z in g.app.commanders() if self.is_enabled(z)
188 ]
189 #@+node:felix.20210626222905.4: *5* sefc.idle_check_commander
190 def idle_check_commander(self, c):
191 """
192 Check all external files corresponding to @<file> nodes in c for
193 changes.
194 """
195 self.infoMessage = None # reset infoMessage
196 # False or "detected", "refreshed" or "ignored"
198 # #1240: Check the .leo file itself.
199 self.idle_check_leo_file(c)
200 #
201 # #1100: always scan the entire file for @<file> nodes.
202 # #1134: Nested @<file> nodes are no longer valid, but this will do no harm.
203 for p in c.all_unique_positions():
204 if self.waitingForAnswer:
205 break
206 if p.isAnyAtFileNode():
207 self.idle_check_at_file_node(c, p)
209 # if yesAll/noAll forced, then just show info message
210 if self.infoMessage:
211 package = {"async": "info", "message": self.infoMessage}
212 g.leoServer._send_async_output(package, True)
213 #@+node:felix.20210627013530.1: *5* sefc.idle_check_leo_file
214 def idle_check_leo_file(self, c):
215 """Check c's .leo file for external changes."""
216 path = c.fileName()
217 if not self.has_changed(path):
218 return
219 # Always update the path & time to prevent future warnings.
220 self.set_time(path)
221 self.checksum_d[path] = self.checksum(path)
222 # For now, ignore the #1888 fix method
223 if self.ask(c, path):
224 #reload Commander
225 # self.lastCommander.close() Stops too much if last file closed
226 g.app.closeLeoWindow(self.lastCommander.frame, finish_quit=False)
227 g.leoServer.open_file({"filename":path }) # ignore returned value
228 #@+node:felix.20210626222905.5: *5* sefc.idle_check_at_file_node
229 def idle_check_at_file_node(self, c, p):
230 """Check the @<file> node at p for external changes."""
231 trace = False
232 path = g.fullPath(c, p)
233 has_changed = self.has_changed(path)
234 if trace:
235 g.trace('changed', has_changed, p.h)
236 if has_changed:
237 self.lastPNode = p # can be set here because its the same process for ask/warn
238 if p.isAtAsisFileNode() or p.isAtNoSentFileNode():
239 # Fix #1081: issue a warning.
240 self.warn(c, path, p=p)
241 elif self.ask(c, path, p=p):
242 old_p = c.p # To restore selection if refresh option set to yes-all & is descendant of at-file
243 c.selectPosition(self.lastPNode)
244 c.refreshFromDisk() # Ends with selection on new c.p which is the at-file node
245 # check with leoServer's config first, and if new c.p is ancestor of old_p
246 if g.leoServer.leoServerConfig:
247 if g.leoServer.leoServerConfig["defaultReloadIgnore"].lower()=='yes-all':
248 if c.positionExists(old_p) and c.p.isAncestorOf(old_p):
249 c.selectPosition(old_p)
251 # Always update the path & time to prevent future warnings.
252 self.set_time(path)
253 self.checksum_d[path] = self.checksum(path)
254 #@+node:felix.20210626222905.18: *4* sefc.open_with
255 def open_with(self, c, d):
256 """open-with is bypassed in leoserver (for now)"""
257 return
259 #@+node:felix.20210626222905.7: *3* sefc.utilities
260 #@+node:felix.20210626222905.8: *4* sefc.ask
261 def ask(self, c, path, p=None):
262 """
263 Ask user whether to overwrite an @<file> tree.
264 Return True if the user agrees by default, or skips and asks
265 client, blocking further checks until result received.
266 """
267 # check with leoServer's config first
268 if g.leoServer.leoServerConfig:
269 check_config = g.leoServer.leoServerConfig["defaultReloadIgnore"].lower()
270 if not bool('none' in check_config):
271 if bool('yes' in check_config):
272 self.infoMessage = "refreshed"
273 return True
274 else:
275 self.infoMessage = "ignored"
276 return False
277 # let original function resolve
279 if self.yesno_all_time + 3 >= time.time() and self.yesno_all_answer:
280 self.yesno_all_time = time.time() # Still reloading? Extend time
281 # if yesAll/noAll forced, then just show info message
282 yesno_all_bool = bool('yes' in self.yesno_all_answer.lower())
283 return yesno_all_bool # We already have our answer here, so return it
284 if not p:
285 where = 'the outline node'
286 else:
287 where = p.h
289 _is_leo = path.endswith(('.leo', '.db'))
291 if _is_leo:
292 s = '\n'.join([
293 f'{g.splitLongFileName(path)} has changed outside Leo.',
294 'Reload it?'
295 ])
296 else:
297 s = '\n'.join([
298 f'{g.splitLongFileName(path)} has changed outside Leo.',
299 f"Reload {where} in Leo?",
300 ])
302 package = {"async": "ask", "ask": 'Overwrite the version in Leo?',
303 "message": s, "yes_all": not _is_leo, "no_all": not _is_leo}
305 g.leoServer._send_async_output(package) # Ask the connected client
306 self.waitingForAnswer = True # Block the loop and further checks until 'clientResult'
307 return False # return false so as not to refresh until 'clientResult' says so
308 #@+node:felix.20210626222905.13: *4* sefc.is_enabled
309 def is_enabled(self, c):
310 """Return the cached @bool check_for_changed_external_file setting."""
311 # check with the leoServer config first
312 if g.leoServer.leoServerConfig:
313 check_config = g.leoServer.leoServerConfig["checkForChangeExternalFiles"].lower()
314 if bool('check' in check_config):
315 return True
316 if bool('ignore' in check_config):
317 return False
318 # let original function resolve
319 return super().is_enabled(c)
320 #@+node:felix.20210626222905.16: *4* sefc.warn
321 def warn(self, c, path, p):
322 """
323 Warn that an @asis or @nosent node has been changed externally.
325 There is *no way* to update the tree automatically.
326 """
327 # check with leoServer's config first
328 if g.leoServer.leoServerConfig:
329 check_config = g.leoServer.leoServerConfig["defaultReloadIgnore"].lower()
331 if check_config != "none":
332 # if not 'none' then do not warn, just infoMessage 'warn' at most
333 if not self.infoMessage:
334 self.infoMessage = "warn"
335 return
337 if g.unitTesting or c not in g.app.commanders():
338 return
339 if not p:
340 g.trace('NO P')
341 return
343 s = '\n'.join([
344 '%s has changed outside Leo.\n' % g.splitLongFileName(
345 path),
346 'Leo can not update this file automatically.\n',
347 'This file was created from %s.\n' % p.h,
348 'Warning: refresh-from-disk will destroy all children.'
349 ])
351 package = {"async": "warn",
352 "warn": 'External file changed', "message": s}
354 g.leoServer._send_async_output(package, True)
355 self.waitingForAnswer = True
356 #@-others
357#@+node:felix.20210621233316.4: ** class LeoServer
358class LeoServer:
359 """Leo Server Controller"""
360 #@+others
361 #@+node:felix.20210621233316.5: *3* server.__init__
362 def __init__(self, testing=False):
364 import leo.core.leoApp as leoApp
365 import leo.core.leoBridge as leoBridge
367 global g
368 t1 = time.process_time()
369 #
370 # Init ivars first.
371 self.c = None # Currently Selected Commander.
372 self.dummy_c = None # Set below, after we set g.
373 self.action = None
374 self.bad_commands_list = [] # Set below.
375 #
376 # Debug utilities
377 self.current_id = 0 # Id of action being processed.
378 self.log_flag = False # set by "log" key
379 #
380 # Start the bridge.
381 self.bridge = leoBridge.controller(
382 gui='nullGui',
383 loadPlugins=True, # True: attempt to load plugins.
384 readSettings=True, # True: read standard settings files.
385 silent=True, # True: don't print signon messages.
386 verbose=False, # True: prints messages that would be sent to the log pane.
387 )
388 self.g = g = self.bridge.globals() # Also sets global 'g' object
389 g.in_leo_server = True # #2098.
390 g.leoServer = self # Set server singleton global reference
391 self.leoServerConfig = None
392 # * Intercept Log Pane output: Sends to client's log pane
393 g.es = self._es # pointer - not a function call
394 #
395 # Set in _init_connection
396 self.web_socket = None # Main Control Client
397 self.loop = None
398 #
399 # To inspect commands
400 self.dummy_c = g.app.newCommander(fileName=None)
401 self.bad_commands_list = self._bad_commands(self.dummy_c)
402 #
403 # * Replacement instances to Leo's codebase : getScript, IdleTime and externalFilesController
404 g.getScript = self._getScript
405 g.IdleTime = self._idleTime
406 #
407 # override for "revert to file" operation
408 g.app.gui.runAskOkDialog = self._runAskOkDialog
409 g.app.gui.runAskYesNoDialog = self._runAskYesNoDialog
410 g.app.gui.runAskYesNoCancelDialog = self._runAskYesNoCancelDialog
411 g.app.gui.show_find_success = self._show_find_success
412 self.headlineWidget = g.bunch(_name='tree')
413 #
414 # Complete the initialization, as in LeoApp.initApp.
415 g.app.idleTimeManager = leoApp.IdleTimeManager()
416 g.app.externalFilesController = ServerExternalFilesController() # Replace
417 g.app.idleTimeManager.start()
418 t2 = time.process_time()
419 if not testing:
420 print(f"LeoServer: init leoBridge in {t2-t1:4.2} sec.", flush=True)
421 #@+node:felix.20210622235127.1: *3* server:leo overridden methods
422 #@+node:felix.20210711194729.1: *4* LeoServer._runAskOkDialog
423 def _runAskOkDialog(self, c, title, message=None, text="Ok"):
424 """Create and run an askOK dialog ."""
425 # Called by many commands in Leo
426 if message:
427 s = title + " " + message
428 else:
429 s = title
430 package = {"async": "info", "message": s}
431 g.leoServer._send_async_output(package)
432 #@+node:felix.20210711194736.1: *4* LeoServer._runAskYesNoDialog
433 def _runAskYesNoDialog(self, c, title, message=None, yes_all=False, no_all=False):
434 """Create and run an askYesNo dialog."""
435 # used in ask with title: 'Overwrite the version in Leo?'
436 # used in revert with title: 'Revert'
437 # used in create ly leo settings with title: 'Create myLeoSettings.leo?'
438 # used in move nodes with title: 'Move Marked Nodes?'
439 s = "runAskYesNoDialog called"
440 if title.startswith('Overwrite'):
441 s = "@<file> tree was overwritten"
442 elif title.startswith('Revert'):
443 s= "Leo outline reverted to last saved contents"
444 elif title.startswith('Create'):
445 s= "myLeoSettings.leo created"
446 elif title.startswith('Move'):
447 s= "Marked nodes were moved"
448 package = {"async": "info", "message": s}
449 g.leoServer._send_async_output(package)
450 return "yes"
451 #@+node:felix.20210711194745.1: *4* LeoServer._runAskYesNoCancelDialog
452 def _runAskYesNoCancelDialog(self, c, title,
453 message=None, yesMessage="Yes", noMessage="No",
454 yesToAllMessage=None, defaultButton="Yes", cancelMessage=None,
455 ):
456 """Create and run an askYesNoCancel dialog ."""
457 # used in dangerous write with title: 'Overwrite existing file?'
458 # used in prompt for save with title: 'Confirm'
459 s = "runAskYesNoCancelDialog called"
460 if title.startswith('Overwrite'):
461 s= "File Overwritten"
462 elif title.startswith('Confirm'):
463 s= "File Saved"
464 package = {"async": "info", "message": s}
465 g.leoServer._send_async_output(package)
466 return "yes"
467 #@+node:felix.20210622235209.1: *4* LeoServer._es
468 def _es(self, * args, **keys): # pragma: no cover (tested in client).
469 """Output to the Log Pane"""
470 d = {
471 'color': None,
472 'commas': False,
473 'newline': True,
474 'spaces': True,
475 'tabName': 'Log',
476 'nodeLink': None,
477 }
478 d = g.doKeywordArgs(keys, d)
479 color = d.get('color')
480 color = g.actualColor(color)
481 s = g.translateArgs(args, d)
482 package = {"async": "log", "log": s}
483 if color:
484 package["color"] = color
485 self._send_async_output(package, True)
486 #@+node:felix.20210626002856.1: *4* LeoServer._getScript
487 def _getScript(self, c, p,
488 useSelectedText=True,
489 forcePythonSentinels=True,
490 useSentinels=True,
491 ):
492 """
493 Return the expansion of the selected text of node p.
494 Return the expansion of all of node p's body text if
495 p is not the current node or if there is no text selection.
496 """
497 w = c.frame.body.wrapper
498 if not p:
499 p = c.p
500 try:
501 if w and p == c.p and useSelectedText and w.hasSelection():
502 s = w.getSelectedText()
503 else:
504 s = p.b
505 # Remove extra leading whitespace so the user may execute indented code.
506 s = textwrap.dedent(s)
507 s = g.extractExecutableString(c, p, s)
508 script = g.composeScript(c, p, s,
509 forcePythonSentinels=forcePythonSentinels,
510 useSentinels=useSentinels)
511 except Exception:
512 g.es_print("unexpected exception in g.getScript", flush=True)
513 g.es_exception()
514 script = ''
515 return script
516 #@+node:felix.20210627004238.1: *4* LeoServer._asyncIdleLoop
517 async def _asyncIdleLoop(self, seconds, func):
518 while True:
519 await asyncio.sleep(seconds)
520 func(self)
521 #@+node:felix.20210627004039.1: *4* LeoServer._idleTime
522 def _idleTime(self, fn, delay, tag):
523 asyncio.get_event_loop().create_task(self._asyncIdleLoop(delay/1000, fn))
524 #@+node:felix.20210626003327.1: *4* LeoServer._show_find_success
525 def _show_find_success(self, c, in_headline, insert, p):
526 """Handle a successful find match."""
527 if in_headline:
528 g.app.gui.set_focus(c, self.headlineWidget)
529 # no return
530 #@+node:felix.20210621233316.6: *3* server:public commands
531 #@+node:felix.20210621233316.7: *4* server:button commands
532 # These will fail unless the open_file inits c.theScriptingController.
533 #@+node:felix.20210621233316.8: *5* _check_button_command
534 def _check_button_command(self, tag): # pragma: no cover (no scripting controller)
535 """
536 Check that a button command is possible.
537 Raise ServerError if not. Otherwise, return sc.buttonsDict.
538 """
539 c = self._check_c()
540 sc = getattr(c, "theScriptingController", None)
541 if not sc:
542 # This will happen unless mod_scripting is loaded!
543 raise ServerError(f"{tag}: no scripting controller")
544 return sc.buttonsDict
545 #@+node:felix.20220220203658.1: *5* _get_rclickTree
546 def _get_rclickTree(self, rclicks):
547 rclickList = []
549 for rc in rclicks:
550 children = []
551 if rc.children:
552 children = self._get_rclickTree(rc.children)
553 rclickList.append({"name": rc.position.h, "children":children })
555 return rclickList
558 #@+node:felix.20210621233316.9: *5* server.click_button
559 def click_button(self, param): # pragma: no cover (no scripting controller)
560 """Handles buttons clicked in client from the '@button' panel"""
561 tag = 'click_button'
562 index = param.get("index")
563 if not index:
564 raise ServerError(f"{tag}: no button index given")
565 d = self._check_button_command(tag)
566 button = None
567 for key in d:
568 # Some button keys are objects so we have to convert first
569 if str(key) == index:
570 button = key
572 if not button:
573 raise ServerError(f"{tag}: button {index!r} does not exist")
575 try:
576 w_rclick = param.get("rclick", False)
577 if w_rclick and hasattr(button, 'rclicks'):
578 # not zero
579 toChooseFrom = button.rclicks
580 for i_rc in w_rclick:
581 w_rclickChosen = toChooseFrom[i_rc]
582 toChooseFrom = w_rclickChosen.children
583 if w_rclickChosen:
584 c = self._check_c()
585 sc = getattr(c, "theScriptingController", None)
586 sc.executeScriptFromButton(button, "", w_rclickChosen.position, "")
588 else:
589 button.command()
590 except Exception as e:
591 raise ServerError(f"{tag}: exception clicking button {index!r}: {e}")
592 # Tag along a possible return value with info sent back by _make_response
593 return self._make_response()
594 #@+node:felix.20210621233316.10: *5* server.get_buttons
595 def get_buttons(self, param): # pragma: no cover (no scripting controller)
596 """
597 Gets the currently opened file's @buttons list
598 as an array of dict.
600 Typescript RClick recursive interface:
601 RClick: {name: string, children: RClick[]}
603 Typescript return interface:
604 {
605 name: string;
606 index: string;
607 rclicks: RClick[];
608 }[]
609 """
610 d = self._check_button_command('get_buttons')
612 buttons = []
613 # Some button keys are objects so we have to convert first
614 for key in d:
615 rclickList = []
616 if hasattr(key, 'rclicks'):
617 rclickList = self._get_rclickTree(key.rclicks)
618 # buttonRClicks = key.rclicks
619 # for rc in buttonRClicks:
620 # rclickList.append(rc.position.h)
622 entry = {"name": d[key], "index": str(key), "rclicks": rclickList}
623 buttons.append(entry)
625 return self._make_minimal_response({
626 "buttons": buttons
627 })
628 #@+node:felix.20210621233316.11: *5* server.remove_button
629 def remove_button(self, param): # pragma: no cover (no scripting controller)
630 """Remove button by index 'key string'."""
631 tag = 'remove_button'
632 index = param.get("index")
633 if not index:
634 raise ServerError(f"{tag}: no button index given")
635 d = self._check_button_command(tag)
636 # Some button keys are objects so we have to convert first
637 key = None
638 for i_key in d:
639 if str(i_key) == index:
640 key = i_key
641 if key:
642 try:
643 del d [key]
644 except Exception as e:
645 raise ServerError(f"{tag}: exception removing button {index!r}: {e}")
646 else:
647 raise ServerError(f"{tag}: button {index!r} does not exist")
649 return self._make_response()
650 #@+node:felix.20211016235830.1: *5* server.goto_script
651 def goto_script(self, param): # pragma: no cover (no scripting controller)
652 """Goto the script this button originates."""
653 tag = 'goto_script'
654 index = param.get("index")
655 if not index:
656 raise ServerError(f"{tag}: no button index given")
657 d = self._check_button_command(tag)
658 # Some button keys are objects so we have to convert first
659 key = None
660 for i_key in d:
661 if str(i_key) == index:
662 key = i_key
663 if key:
664 try:
665 gnx = key.command.gnx
666 c = self._check_c()
667 # pylint: disable=undefined-loop-variable
668 for p in c.all_positions():
669 if p.gnx == gnx:
670 break
671 if p:
672 assert c.positionExists(p)
673 c.selectPosition(p)
674 else:
675 raise ServerError(f"{tag}: not found {gnx}")
676 except Exception as e:
677 raise ServerError(f"{tag}: exception going to script of button {index!r}: {e}")
678 else:
679 raise ServerError(f"{tag}: button {index!r} does not exist")
681 return self._make_response()
682 #@+node:felix.20210621233316.12: *4* server:file commands
683 #@+node:felix.20210621233316.13: *5* server.open_file
684 def open_file(self, param):
685 """
686 Open a leo file with the given filename.
687 Create a new document if no name.
688 """
689 found, tag = False, 'open_file'
690 filename = param.get('filename') # Optional.
691 if filename:
692 for c in g.app.commanders():
693 if c.fileName() == filename:
694 found = True
695 if not found:
696 c = self.bridge.openLeoFile(filename)
697 # Add ftm. This won't happen if opened outside leoserver
698 c.findCommands.ftm = StringFindTabManager(c)
699 if not c: # pragma: no cover
700 raise ServerError(f"{tag}: bridge did not open {filename!r}")
701 if not c.frame.body.wrapper: # pragma: no cover
702 raise ServerError(f"{tag}: no wrapper")
703 # Assign self.c
704 self.c = c
705 c.selectPosition(c.rootPosition()) # Required.
706 # Check the outline!
707 c.recreateGnxDict() # refresh c.fileCommands.gnxDict used in ap_to_p
708 self._check_outline(c)
709 if self.log_flag: # pragma: no cover
710 self._dump_outline(c)
712 result = {"total": len(g.app.commanders()), "filename": self.c.fileName()}
714 return self._make_response(result)
715 #@+node:felix.20210621233316.14: *5* server.open_files
716 def open_files(self, param):
717 """
718 Opens an array of leo files.
719 Returns an object with total opened files
720 and name of currently last opened & selected document.
721 """
722 files = param.get('files') # Optional.
723 if files:
724 for i_file in files:
725 if os.path.isfile(i_file):
726 self.open_file({"filename": i_file})
727 total = len(g.app.commanders())
728 filename = self.c.fileName() if total else ""
729 result = {"total": total, "filename": filename}
730 return self._make_response(result)
731 #@+node:felix.20210621233316.15: *5* server.set_opened_file
732 def set_opened_file(self, param):
733 """
734 Choose the new active commander from array of opened files.
735 Returns an object with total opened files
736 and name of currently last opened & selected document.
737 """
738 tag = 'set_opened_file'
739 index = param.get('index')
740 total = len(g.app.commanders())
741 if total and index < total:
742 self.c = g.app.commanders()[index]
743 # maybe needed for frame wrapper
744 self.c.selectPosition(self.c.p)
745 self._check_outline(self.c)
746 result = {"total": total, "filename": self.c.fileName()}
747 return self._make_response(result)
748 raise ServerError(f"{tag}: commander at index {index} does not exist")
749 #@+node:felix.20210621233316.16: *5* server.close_file
750 def close_file(self, param):
751 """
752 Closes an outline opened with open_file.
753 Use a 'forced' flag to force close.
754 Returns a 'total' member in the package if close is successful.
755 """
756 c = self._check_c()
757 forced = param.get("forced")
758 if c:
759 # First, revert to prevent asking user.
760 if forced and c.changed:
761 if c.fileName():
762 c.revert()
763 else:
764 c.changed = False # Needed in g.app.closeLeoWindow
765 # Then, if still possible, close it.
766 if forced or not c.changed:
767 # c.close() # Stops too much if last file closed
768 g.app.closeLeoWindow(c.frame, finish_quit=False)
769 else:
770 # Cannot close, return empty response without 'total' (ask to save, ignore or cancel)
771 return self._make_response()
772 # New 'c': Select the first open outline, if any.
773 commanders = g.app.commanders()
774 self.c = commanders and commanders[0] or None
775 if self.c:
776 result = {"total": len(g.app.commanders()), "filename": self.c.fileName()}
777 else:
778 result = {"total": 0}
779 return self._make_response(result)
780 #@+node:felix.20210621233316.17: *5* server.save_file
781 def save_file(self, param): # pragma: no cover (too dangerous).
782 """Save the leo outline."""
783 tag = 'save_file'
784 c = self._check_c()
785 if c:
786 try:
787 if "name" in param:
788 c.save(fileName=param['name'])
789 else:
790 c.save()
791 except Exception as e:
792 print(f"{tag} Error while saving {param['name']}", flush=True)
793 print(e, flush=True)
795 return self._make_response() # Just send empty as 'ok'
796 #@+node:felix.20210621233316.18: *5* server.import_any_file
797 def import_any_file(self, param):
798 """
799 Import file(s) from array of file names
800 """
801 tag = 'import_any_file'
802 c = self._check_c()
803 ic = c.importCommands
804 names = param.get('filenames')
805 if names:
806 g.chdir(names[0])
807 if not names:
808 raise ServerError(f"{tag}: No file names provided")
809 # New in Leo 4.9: choose the type of import based on the extension.
810 derived = [z for z in names if c.looksLikeDerivedFile(z)]
811 others = [z for z in names if z not in derived]
812 if derived:
813 ic.importDerivedFiles(parent=c.p, paths=derived)
814 for fn in others:
815 junk, ext = g.os_path_splitext(fn)
816 ext = ext.lower() # #1522
817 if ext.startswith('.'):
818 ext = ext[1:]
819 if ext == 'csv':
820 ic.importMindMap([fn])
821 elif ext in ('cw', 'cweb'):
822 ic.importWebCommand([fn], "cweb")
823 # Not useful. Use @auto x.json instead.
824 # elif ext == 'json':
825 # ic.importJSON([fn])
826 elif fn.endswith('mm.html'):
827 ic.importFreeMind([fn])
828 elif ext in ('nw', 'noweb'):
829 ic.importWebCommand([fn], "noweb")
830 elif ext == 'more':
831 # (Félix) leoImport Should be on c?
832 c.leoImport.MORE_Importer(c).import_file(fn) # #1522.
833 elif ext == 'txt':
834 # (Félix) import_txt_file Should be on c?
835 # #1522: Create an @edit node.
836 c.import_txt_file(c, fn)
837 else:
838 # Make *sure* that parent.b is empty.
839 last = c.lastTopLevel()
840 parent = last.insertAfter()
841 parent.v.h = 'Imported Files'
842 ic.importFilesCommand(
843 files=[fn],
844 parent=parent,
845 treeType='@auto', # was '@clean'
846 # Experimental: attempt to use permissive section ref logic.
847 )
848 return self._make_response() # Just send empty as 'ok'
849 #@+node:felix.20210621233316.19: *4* server.search commands
850 #@+node:felix.20210621233316.20: *5* server.get_search_settings
851 def get_search_settings(self, param):
852 """
853 Gets search options
854 """
855 tag = 'get_search_settings'
856 c = self._check_c()
857 try:
858 settings = c.findCommands.ftm.get_settings()
859 # Use the "__dict__" of the settings, to be serializable as a json string.
860 result = {"searchSettings": settings.__dict__}
861 except Exception as e:
862 raise ServerError(f"{tag}: exception getting search settings: {e}")
863 return self._make_response(result)
864 #@+node:felix.20210621233316.21: *5* server.set_search_settings
865 def set_search_settings(self, param):
866 """
867 Sets search options. Init widgets and ivars from param.searchSettings
868 """
869 tag = 'set_search_settings'
870 c = self._check_c()
871 find = c.findCommands
872 ftm = c.findCommands.ftm
873 searchSettings = param.get('searchSettings')
874 if not searchSettings:
875 raise ServerError(f"{tag}: searchSettings object is missing")
876 # Try to set the search settings
877 try:
878 # Find/change text boxes.
879 table = (
880 ('find_findbox', 'find_text', ''),
881 ('find_replacebox', 'change_text', ''),
882 )
883 for widget_ivar, setting_name, default in table:
884 w = getattr(ftm, widget_ivar)
885 s = searchSettings.get(setting_name) or default
886 w.clear()
887 w.insert(s)
888 # Check boxes.
889 table2 = (
890 ('ignore_case', 'check_box_ignore_case'),
891 ('mark_changes', 'check_box_mark_changes'),
892 ('mark_finds', 'check_box_mark_finds'),
893 ('pattern_match', 'check_box_regexp'),
894 ('search_body', 'check_box_search_body'),
895 ('search_headline', 'check_box_search_headline'),
896 ('whole_word', 'check_box_whole_word'),
897 )
898 for setting_name, widget_ivar in table2:
899 w = getattr(ftm, widget_ivar)
900 val = searchSettings.get(setting_name)
901 setattr(find, setting_name, val)
902 if val != w.isChecked():
903 w.toggle()
904 # Radio buttons
905 table3 = (
906 ('node_only', 'node_only', 'radio_button_node_only'),
907 ('entire_outline', None, 'radio_button_entire_outline'),
908 ('suboutline_only', 'suboutline_only', 'radio_button_suboutline_only'),
909 )
910 for setting_name, ivar, widget_ivar in table3:
911 w = getattr(ftm, widget_ivar)
912 val = searchSettings.get(setting_name, False)
913 if ivar is not None:
914 assert hasattr(find, setting_name), setting_name
915 setattr(find, setting_name, val)
916 if val != w.isChecked():
917 w.toggle()
918 # Ensure one radio button is set.
919 w = ftm.radio_button_entire_outline
920 if not searchSettings.get('node_only', False) and not searchSettings.get('suboutline_only', False):
921 setattr(find, 'entire_outline', True)
922 if not w.isChecked():
923 w.toggle()
924 else:
925 setattr(find, 'entire_outline', False)
926 if w.isChecked():
927 w.toggle()
928 except Exception as e:
929 raise ServerError(f"{tag}: exception setting search settings: {e}")
930 # Confirm by sending back the settings to the client
931 try:
932 settings = ftm.get_settings()
933 # Use the "__dict__" of the settings, to be serializable as a json string.
934 result = {"searchSettings": settings.__dict__}
935 except Exception as e:
936 raise ServerError(f"{tag}: exception getting search settings: {e}")
937 return self._make_response(result)
938 #@+node:felix.20210621233316.22: *5* server.find_all
939 def find_all(self, param):
940 """Run Leo's find all command and return results."""
941 tag = 'find_all'
942 c = self._check_c()
943 fc = c.findCommands
944 try:
945 settings = fc.ftm.get_settings()
946 result = fc.do_find_all(settings)
947 except Exception as e:
948 raise ServerError(f"{tag}: exception running 'find all': {e}")
949 focus = self._get_focus()
950 return self._make_response({"found": result, "focus": focus})
951 #@+node:felix.20210621233316.23: *5* server.find_next
952 def find_next(self, param):
953 """Run Leo's find-next command and return results."""
954 tag = 'find_next'
955 c = self._check_c()
956 p = c.p
957 fc = c.findCommands
958 fromOutline = param.get("fromOutline")
959 fromBody = not fromOutline
960 #
961 focus = self._get_focus()
962 inOutline = ("tree" in focus) or ("head" in focus)
963 inBody = not inOutline
964 #
965 if fromOutline and inBody:
966 fc.in_headline = True
967 elif fromBody and inOutline:
968 fc.in_headline = False
969 # w = c.frame.body.wrapper
970 c.bodyWantsFocus()
971 c.bodyWantsFocusNow()
972 #
973 if fc.in_headline:
974 ins = len(p.h)
975 gui_w = c.edit_widget(p)
976 gui_w.setSelectionRange(ins, ins, insert=ins)
977 #
978 try:
979 # Let cursor as-is
980 settings = fc.ftm.get_settings()
981 p, pos, newpos = fc.do_find_next(settings)
982 except Exception as e:
983 raise ServerError(f"{tag}: Running find operation gave exception: {e}")
984 #
985 # get focus again after the operation
986 focus = self._get_focus()
987 result = {"found": bool(p), "pos": pos,
988 "newpos": newpos, "focus": focus}
989 return self._make_response(result)
990 #@+node:felix.20210621233316.24: *5* server.find_previous
991 def find_previous(self, param):
992 """Run Leo's find-previous command and return results."""
993 tag = 'find_previous'
994 c = self._check_c()
995 p = c.p
996 fc = c.findCommands
997 fromOutline = param.get("fromOutline")
998 fromBody = not fromOutline
999 #
1000 focus = self._get_focus()
1001 inOutline = ("tree" in focus) or ("head" in focus)
1002 inBody = not inOutline
1003 #
1004 if fromOutline and inBody:
1005 fc.in_headline = True
1006 elif fromBody and inOutline:
1007 fc.in_headline = False
1008 # w = c.frame.body.wrapper
1009 c.bodyWantsFocus()
1010 c.bodyWantsFocusNow()
1011 #
1012 if fc.in_headline:
1013 gui_w = c.edit_widget(p)
1014 gui_w.setSelectionRange(0, 0, insert=0)
1015 #
1016 try:
1017 # set widget cursor pos to 0 if in headline
1018 settings = fc.ftm.get_settings()
1019 p, pos, newpos = fc.do_find_prev(settings)
1020 except Exception as e:
1021 raise ServerError(f"{tag}: Running find operation gave exception: {e}")
1022 #
1023 # get focus again after the operation
1024 focus = self._get_focus()
1025 result = {"found": bool(p), "pos": pos,
1026 "newpos": newpos, "focus": focus}
1027 return self._make_response(result)
1028 #@+node:felix.20210621233316.25: *5* server.replace
1029 def replace(self, param):
1030 """Run Leo's replace command and return results."""
1031 tag = 'replace'
1032 c = self._check_c()
1033 fc = c.findCommands
1034 try:
1035 settings = fc.ftm.get_settings()
1036 fc.change(settings)
1037 except Exception as e:
1038 raise ServerError(f"{tag}: Running change operation gave exception: {e}")
1039 focus = self._get_focus()
1040 result = {"found": True, "focus": focus}
1041 return self._make_response(result)
1042 #@+node:felix.20210621233316.26: *5* server.replace_then_find
1043 def replace_then_find(self, param):
1044 """Run Leo's replace then find next command and return results."""
1045 tag = 'replace_then_find'
1046 c = self._check_c()
1047 fc = c.findCommands
1048 try:
1049 settings = fc.ftm.get_settings()
1050 result = fc.do_change_then_find(settings)
1051 except Exception as e:
1052 raise ServerError(f"{tag}: Running change operation gave exception: {e}")
1053 focus = self._get_focus()
1054 return self._make_response({"found": result, "focus": focus})
1055 #@+node:felix.20210621233316.27: *5* server.replace_all
1056 def replace_all(self, param):
1057 """Run Leo's replace all command and return results."""
1058 tag = 'replace_all'
1059 c = self._check_c()
1060 fc = c.findCommands
1061 try:
1062 settings = fc.ftm.get_settings()
1063 result = fc.do_change_all(settings)
1064 except Exception as e:
1065 raise ServerError(f"{tag}: Running change operation gave exception: {e}")
1066 focus = self._get_focus()
1067 return self._make_response({"found": result, "focus": focus})
1068 #@+node:felix.20210621233316.28: *5* server.clone_find_all
1069 def clone_find_all(self, param):
1070 """Run Leo's clone-find-all command and return results."""
1071 tag = 'clone_find_all'
1072 c = self._check_c()
1073 fc = c.findCommands
1074 try:
1075 settings = fc.ftm.get_settings()
1076 result = fc.do_clone_find_all(settings)
1077 except Exception as e:
1078 raise ServerError(f"{tag}: Running clone find operation gave exception: {e}")
1079 focus = self._get_focus()
1080 return self._make_response({"found": result, "focus": focus})
1081 #@+node:felix.20210621233316.29: *5* server.clone_find_all_flattened
1082 def clone_find_all_flattened(self, param):
1083 """Run Leo's clone-find-all-flattened command and return results."""
1084 tag = 'clone_find_all_flattened'
1085 c = self._check_c()
1086 fc = c.findCommands
1087 try:
1088 settings = fc.ftm.get_settings()
1089 result = fc.do_clone_find_all_flattened(settings)
1090 except Exception as e:
1091 raise ServerError(f"{tag}: Running clone find operation gave exception: {e}")
1092 focus = self._get_focus()
1093 return self._make_response({"found": result, "focus": focus})
1094 #@+node:felix.20210621233316.30: *5* server.find_var
1095 def find_var(self, param):
1096 """Run Leo's find-var command and return results."""
1097 tag = 'find_var'
1098 c = self._check_c()
1099 fc = c.findCommands
1100 try:
1101 fc.find_var()
1102 except Exception as e:
1103 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}")
1104 focus = self._get_focus()
1105 return self._make_response({"found": True, "focus": focus})
1106 #@+node:felix.20210722010004.1: *5* server.clone_find_all_flattened_marked
1107 def clone_find_all_flattened_marked(self, param):
1108 """Run Leo's clone-find-all-flattened-marked command."""
1109 tag = 'clone_find_all_flattened_marked'
1110 c = self._check_c()
1111 fc = c.findCommands
1112 try:
1113 fc.do_find_marked(flatten=True)
1114 except Exception as e:
1115 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}")
1116 focus = self._get_focus()
1117 return self._make_response({"found": True, "focus": focus})
1118 #@+node:felix.20210722010005.1: *5* server.clone_find_all_marked
1119 def clone_find_all_marked(self, param):
1120 """Run Leo's clone-find-all-marked command """
1121 tag = 'clone_find_all_marked'
1122 c = self._check_c()
1123 fc = c.findCommands
1124 try:
1125 fc.do_find_marked(flatten=False)
1126 except Exception as e:
1127 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}")
1128 focus = self._get_focus()
1129 return self._make_response({"found": True, "focus": focus})
1130 #@+node:felix.20210621233316.31: *5* server.find_def
1131 def find_def(self, param):
1132 """Run Leo's find-def command and return results."""
1133 tag = 'find_def'
1134 c = self._check_c()
1135 fc = c.findCommands
1136 try:
1137 fc.find_def()
1138 except Exception as e:
1139 raise ServerError(f"{tag}: Running find symbol definition gave exception: {e}")
1140 focus = self._get_focus()
1141 return self._make_response({"found": True, "focus": focus})
1142 #@+node:felix.20210621233316.32: *5* server.goto_global_line
1143 def goto_global_line(self, param):
1144 """Run Leo's goto-global-line command and return results."""
1145 tag = 'goto_global_line'
1146 c = self._check_c()
1147 gc = c.gotoCommands
1148 line = param.get('line', 1)
1149 try:
1150 junk_p, junk_offset, found = gc.find_file_line(n=int(line))
1151 except Exception as e:
1152 raise ServerError(f"{tag}: Running clone find operation gave exception: {e}")
1153 focus = self._get_focus()
1154 return self._make_response({"found": found, "focus": focus})
1155 #@+node:felix.20210621233316.33: *5* server.clone_find_tag
1156 def clone_find_tag(self, param):
1157 """Run Leo's clone-find-tag command and return results."""
1158 tag = 'clone_find_tag'
1159 c = self._check_c()
1160 fc = c.findCommands
1161 tag_param = param.get("tag")
1162 if not tag_param: # pragma: no cover
1163 raise ServerError(f"{tag}: no tag")
1164 settings = fc.ftm.get_settings()
1165 if self.log_flag: # pragma: no cover
1166 g.printObj(settings, tag=f"{tag}: settings for {c.shortFileName()}")
1167 n, p = fc.do_clone_find_tag(tag_param)
1168 if self.log_flag: # pragma: no cover
1169 g.trace("tag: {tag_param} n: {n} p: {p and p.h!r}")
1170 print('', flush=True)
1171 return self._make_response({"n": n})
1172 #@+node:felix.20210621233316.34: *5* server.tag_children
1173 def tag_children(self, param):
1174 """Run Leo's tag-children command"""
1175 # This is not a find command!
1176 tag = 'tag_children'
1177 c = self._check_c()
1178 fc = c.findCommands
1179 tag_param = param.get("tag")
1180 if tag_param is None: # pragma: no cover
1181 raise ServerError(f"{tag}: no tag")
1182 # Unlike find commands, do_tag_children does not use a settings dict.
1183 fc.do_tag_children(c.p, tag_param)
1184 return self._make_response()
1185 #@+node:felix.20210621233316.35: *4* server:getter commands
1186 #@+node:felix.20210621233316.36: *5* server.get_all_open_commanders
1187 def get_all_open_commanders(self, param):
1188 """Return array describing each commander in g.app.commanders()."""
1189 files = [
1190 {
1191 "changed": c.isChanged(),
1192 "name": c.fileName(),
1193 "selected": c == self.c,
1194 } for c in g.app.commanders()
1195 ]
1196 return self._make_minimal_response({"files": files})
1197 #@+node:felix.20210621233316.37: *5* server.get_all_positions
1198 def get_all_positions(self, param):
1199 """
1200 Return a list of position data for all positions.
1202 Useful as a sanity check for debugging.
1203 """
1204 c = self._check_c()
1205 result = [
1206 self._get_position_d(p) for p in c.all_positions(copy=False)
1207 ]
1208 return self._make_minimal_response({"position-data-list": result})
1209 #@+node:felix.20210621233316.38: *5* server.get_all_gnx
1210 def get_all_gnx(self, param):
1211 """Get gnx array from all unique nodes"""
1212 if self.log_flag: # pragma: no cover
1213 print('\nget_all_gnx\n', flush=True)
1214 c = self._check_c()
1215 all_gnx = [p.v.gnx for p in c.all_unique_positions(copy=False)]
1216 return self._make_minimal_response({"gnx": all_gnx})
1217 #@+node:felix.20210621233316.39: *5* server.get_body
1218 def get_body(self, param):
1219 """
1220 Return the body content body specified via GNX.
1221 """
1222 c = self._check_c()
1223 gnx = param.get("gnx")
1224 v = c.fileCommands.gnxDict.get(gnx) # vitalije
1225 body = ""
1226 if v:
1227 body = v.b or ""
1228 # Support asking for unknown gnx when client switches rapidly
1229 return self._make_minimal_response({"body": body})
1230 #@+node:felix.20210621233316.40: *5* server.get_body_length
1231 def get_body_length(self, param):
1232 """
1233 Return p.b's length in bytes, where p is c.p if param["ap"] is missing.
1234 """
1235 c = self._check_c()
1236 gnx = param.get("gnx")
1237 w_v = c.fileCommands.gnxDict.get(gnx) # vitalije
1238 if w_v:
1239 # Length in bytes, not just by character count.
1240 return self._make_minimal_response({"len": len(w_v.b.encode('utf-8'))})
1241 return self._make_minimal_response({"len": 0}) # empty as default
1242 #@+node:felix.20210621233316.41: *5* server.get_body_states
1243 def get_body_states(self, param):
1244 """
1245 Return body data for p, where p is c.p if param["ap"] is missing.
1246 The cursor positions are given as {"line": line, "col": col, "index": i}
1247 with line and col along with a redundant index for convenience and flexibility.
1248 """
1249 c = self._check_c()
1250 p = self._get_p(param)
1251 wrapper = c.frame.body.wrapper
1253 def row_col_wrapper_dict(i):
1254 if not i:
1255 i = 0 # prevent none type
1256 # BUG: this uses current selection wrapper only, use
1257 # g.convertPythonIndexToRowCol instead !
1258 junk, line, col = wrapper.toPythonIndexRowCol(i)
1259 return {"line": line, "col": col, "index": i}
1261 def row_col_pv_dict(i, s):
1262 if not i:
1263 i = 0 # prevent none type
1264 # BUG: this uses current selection wrapper only, use
1265 # g.convertPythonIndexToRowCol instead !
1266 line, col = g.convertPythonIndexToRowCol(s, i)
1267 return {"line": line, "col": col, "index": i}
1269 # Get the language.
1270 aList = g.get_directives_dict_list(p)
1271 d = g.scanAtCommentAndAtLanguageDirectives(aList)
1272 language = (
1273 d and d.get('language')
1274 or g.getLanguageFromAncestorAtFileNode(p)
1275 or c.config.getLanguage('target-language')
1276 or 'plain'
1277 )
1278 # Get the body wrap state
1279 wrap = g.scanAllAtWrapDirectives(c, p)
1280 tabWidth = g.scanAllAtTabWidthDirectives(c, p)
1281 if not isinstance(tabWidth, int):
1282 tabWidth = False
1283 # get values from wrapper if it's the selected node.
1284 if c.p.v.gnx == p.v.gnx:
1285 insert = wrapper.getInsertPoint()
1286 start, end = wrapper.getSelectionRange(True)
1287 scroll = wrapper.getYScrollPosition()
1288 states = {
1289 'language': language.lower(),
1290 'wrap': wrap,
1291 'tabWidth': tabWidth,
1292 'selection': {
1293 "gnx": p.v.gnx,
1294 "scroll": scroll,
1295 "insert": row_col_wrapper_dict(insert),
1296 "start": row_col_wrapper_dict(start),
1297 "end": row_col_wrapper_dict(end)
1298 }
1299 }
1300 else: # pragma: no cover
1301 insert = p.v.insertSpot
1302 start = p.v.selectionStart
1303 end = p.v.selectionStart + p.v.selectionLength
1304 scroll = p.v.scrollBarSpot
1305 states = {
1306 'language': language.lower(),
1307 'wrap': wrap,
1308 'tabWidth': tabWidth,
1309 'selection': {
1310 "gnx": p.v.gnx,
1311 "scroll": scroll,
1312 "insert": row_col_pv_dict(insert, p.v.b),
1313 "start": row_col_pv_dict(start, p.v.b),
1314 "end": row_col_pv_dict(end, p.v.b)
1315 }
1316 }
1317 return self._make_minimal_response(states)
1318 #@+node:felix.20210621233316.42: *5* server.get_children
1319 def get_children(self, param):
1320 """
1321 Return the node data for children of p,
1322 where p is root if param.ap is missing
1323 """
1324 c = self._check_c()
1325 children = [] # default empty array
1326 if param.get("ap"):
1327 # Maybe empty param, for tree-root children(s).
1328 # _get_p called with the strict=True parameter because
1329 # we don't want c.p. after switch to another document while refreshing.
1330 p = self._get_p(param, True)
1331 if p and p.hasChildren():
1332 children = [self._get_position_d(child) for child in p.children()]
1333 else:
1334 if c.hoistStack:
1335 # Always start hoisted tree with single hoisted root node
1336 children = [self._get_position_d(c.hoistStack[-1].p)]
1337 else:
1338 # this outputs all Root Children
1339 children = [self._get_position_d(child) for child in self._yieldAllRootChildren()]
1340 return self._make_minimal_response({"children": children})
1341 #@+node:felix.20210621233316.43: *5* server.get_focus
1342 def get_focus(self, param):
1343 """
1344 Return a representation of the focused widget,
1345 one of ("body", "tree", "headline", repr(the_widget)).
1346 """
1347 return self._make_minimal_response({"focus": self._get_focus()})
1348 #@+node:felix.20210621233316.44: *5* server.get_parent
1349 def get_parent(self, param):
1350 """Return the node data for the parent of position p, where p is c.p if param["ap"] is missing."""
1351 self._check_c()
1352 p = self._get_p(param)
1353 parent = p.parent()
1354 data = self._get_position_d(parent) if parent else None
1355 return self._make_minimal_response({"node": data})
1356 #@+node:felix.20210621233316.45: *5* server.get_position_data
1357 def get_position_data(self, param):
1358 """
1359 Return a dict of position data for all positions.
1361 Useful as a sanity check for debugging.
1362 """
1363 c = self._check_c()
1364 result = {
1365 p.v.gnx: self._get_position_d(p)
1366 for p in c.all_unique_positions(copy=False)
1367 }
1368 return self._make_minimal_response({"position-data-dict": result})
1369 #@+node:felix.20210621233316.46: *5* server.get_ua
1370 def get_ua(self, param):
1371 """Return p.v.u, making sure it can be serialized."""
1372 self._check_c()
1373 p = self._get_p(param)
1374 try:
1375 ua = {"ua": p.v.u}
1376 json.dumps(ua, separators=(',', ':'), cls=SetEncoder)
1377 response = {"ua": p.v.u}
1378 except Exception: # pragma: no cover
1379 response = {"ua": repr(p.v.u)}
1380 # _make_response adds all the cheap redraw data.
1381 return self._make_response(response)
1382 #@+node:felix.20210621233316.48: *5* server.get_ui_states
1383 def get_ui_states(self, param):
1384 """
1385 Return the enabled/disabled UI states for the open commander, or defaults if None.
1386 """
1387 c = self._check_c()
1388 tag = 'get_ui_states'
1389 try:
1390 states = {
1391 "changed": c and c.changed,
1392 "canUndo": c and c.canUndo(),
1393 "canRedo": c and c.canRedo(),
1394 "canDemote": c and c.canDemote(),
1395 "canPromote": c and c.canPromote(),
1396 "canDehoist": c and c.canDehoist(),
1397 }
1398 except Exception as e: # pragma: no cover
1399 raise ServerError(f"{tag}: Exception setting state: {e}")
1400 return self._make_minimal_response({"states": states})
1401 #@+node:felix.20211210213603.1: *5* server.get_undos
1402 def get_undos(self, param):
1403 """Return list of undo operations"""
1404 c = self._check_c()
1405 undoer = c.undoer
1406 undos = []
1407 try:
1408 for bead in undoer.beads:
1409 undos.append(bead.undoType)
1410 response = {"bead": undoer.bead, "undos": undos}
1411 except Exception: # pragma: no cover
1412 response = {"bead": 0, "undos": []}
1413 # _make_response adds all the cheap redraw data.
1414 return self._make_minimal_response(response)
1415 #@+node:felix.20210621233316.49: *4* server:node commands
1416 #@+node:felix.20210621233316.50: *5* server.clone_node
1417 def clone_node(self, param):
1418 """
1419 Clone a node.
1420 Try to keep selection, then return the selected node that remains.
1421 """
1422 c = self._check_c()
1423 p = self._get_p(param)
1424 if p == c.p:
1425 c.clone()
1426 else:
1427 oldPosition = c.p
1428 c.selectPosition(p)
1429 c.clone()
1430 if c.positionExists(oldPosition):
1431 c.selectPosition(oldPosition)
1432 # return selected node either ways
1433 return self._make_response()
1435 #@+node:felix.20210621233316.51: *5* server.contract_node
1436 def contract_node(self, param):
1437 """
1438 Contract (Collapse) the node at position p, where p is c.p if p is missing.
1439 """
1440 p = self._get_p(param)
1441 p.contract()
1442 return self._make_response()
1443 #@+node:felix.20210621233316.52: *5* server.copy_node
1444 def copy_node(self, param): # pragma: no cover (too dangerous, for now)
1445 """
1446 Copy a node, don't select it.
1447 Try to keep selection, then return the selected node.
1448 """
1449 c = self._check_c()
1450 p = self._get_p(param)
1451 if p == c.p:
1452 s = c.fileCommands.outline_to_clipboard_string()
1453 else:
1454 oldPosition = c.p # not same node, save position to possibly return to
1455 c.selectPosition(p)
1456 s = c.fileCommands.outline_to_clipboard_string()
1457 if c.positionExists(oldPosition):
1458 # select if old position still valid
1459 c.selectPosition(oldPosition)
1460 return self._make_response({"string": s})
1462 #@+node:felix.20220222172507.1: *5* server.cut_node
1463 def cut_node(self, param): # pragma: no cover (too dangerous, for now)
1464 """
1465 Cut a node, don't select it.
1466 Try to keep selection, then return the selected node that remains.
1467 """
1468 c = self._check_c()
1469 p = self._get_p(param)
1470 if p == c.p:
1471 s = c.fileCommands.outline_to_clipboard_string()
1472 c.cutOutline() # already on this node, so cut it
1473 else:
1474 oldPosition = c.p # not same node, save position to possibly return to
1475 c.selectPosition(p)
1476 s = c.fileCommands.outline_to_clipboard_string()
1477 c.cutOutline()
1478 if c.positionExists(oldPosition):
1479 # select if old position still valid
1480 c.selectPosition(oldPosition)
1481 else:
1482 oldPosition._childIndex = oldPosition._childIndex-1
1483 # Try again with childIndex decremented
1484 if c.positionExists(oldPosition):
1485 # additional try with lowered childIndex
1486 c.selectPosition(oldPosition)
1487 return self._make_response({"string": s})
1488 #@+node:felix.20210621233316.53: *5* server.delete_node
1489 def delete_node(self, param): # pragma: no cover (too dangerous, for now)
1490 """
1491 Delete a node, don't select it.
1492 Try to keep selection, then return the selected node that remains.
1493 """
1494 c = self._check_c()
1495 p = self._get_p(param)
1496 if p == c.p:
1497 c.deleteOutline() # already on this node, so cut it
1498 else:
1499 oldPosition = c.p # not same node, save position to possibly return to
1500 c.selectPosition(p)
1501 c.deleteOutline()
1502 if c.positionExists(oldPosition):
1503 # select if old position still valid
1504 c.selectPosition(oldPosition)
1505 else:
1506 oldPosition._childIndex = oldPosition._childIndex-1
1507 # Try again with childIndex decremented
1508 if c.positionExists(oldPosition):
1509 # additional try with lowered childIndex
1510 c.selectPosition(oldPosition)
1511 return self._make_response()
1512 #@+node:felix.20210621233316.54: *5* server.expand_node
1513 def expand_node(self, param):
1514 """
1515 Expand the node at position p, where p is c.p if p is missing.
1516 """
1517 p = self._get_p(param)
1518 p.expand()
1519 return self._make_response()
1520 #@+node:felix.20210621233316.55: *5* server.insert_node
1521 def insert_node(self, param):
1522 """
1523 Insert a node at given node, then select it once created, and finally return it
1524 """
1525 c = self._check_c()
1526 p = self._get_p(param)
1527 c.selectPosition(p)
1528 c.insertHeadline() # Handles undo, sets c.p
1529 return self._make_response()
1530 #@+node:felix.20210703021435.1: *5* server.insert_child_node
1531 def insert_child_node(self, param):
1532 """
1533 Insert a child node at given node, then select it once created, and finally return it
1534 """
1535 c = self._check_c()
1536 p = self._get_p(param)
1537 c.selectPosition(p)
1538 c.insertHeadline(op_name='Insert Child', as_child=True)
1539 return self._make_response()
1540 #@+node:felix.20210621233316.56: *5* server.insert_named_node
1541 def insert_named_node(self, param):
1542 """
1543 Insert a node at given node, set its headline, select it and finally return it
1544 """
1545 c = self._check_c()
1546 p = self._get_p(param)
1547 newHeadline = param.get('name')
1548 bunch = c.undoer.beforeInsertNode(p)
1549 newNode = p.insertAfter()
1550 # set this node's new headline
1551 newNode.h = newHeadline
1552 newNode.setDirty()
1553 c.undoer.afterInsertNode(
1554 newNode, 'Insert Node', bunch)
1555 c.selectPosition(newNode)
1556 c.setChanged()
1557 return self._make_response()
1558 #@+node:felix.20210703021441.1: *5* server.insert_child_named_node
1559 def insert_child_named_node(self, param):
1560 """
1561 Insert a child node at given node, set its headline, select it and finally return it
1562 """
1563 c = self._check_c()
1564 p = self._get_p(param)
1565 newHeadline = param.get('name')
1566 bunch = c.undoer.beforeInsertNode(p)
1567 if c.config.getBool('insert-new-nodes-at-end'):
1568 newNode = p.insertAsLastChild()
1569 else:
1570 newNode = p.insertAsNthChild(0)
1571 # set this node's new headline
1572 newNode.h = newHeadline
1573 newNode.setDirty()
1574 c.undoer.afterInsertNode(
1575 newNode, 'Insert Node', bunch)
1576 c.selectPosition(newNode)
1577 return self._make_response()
1578 #@+node:felix.20210621233316.57: *5* server.page_down
1579 def page_down(self, param):
1580 """
1581 Selects a node "n" steps down in the tree to simulate page down.
1582 """
1583 c = self._check_c()
1584 n = param.get("n", 3)
1585 for z in range(n):
1586 c.selectVisNext()
1587 return self._make_response()
1588 #@+node:felix.20210621233316.58: *5* server.page_up
1589 def page_up(self, param):
1590 """
1591 Selects a node "N" steps up in the tree to simulate page up.
1592 """
1593 c = self._check_c()
1594 n = param.get("n", 3)
1595 for z in range(n):
1596 c.selectVisBack()
1597 return self._make_response()
1598 #@+node:felix.20220222173659.1: *5* server.paste_node
1599 def paste_node(self, param):
1600 """
1601 Pastes a node,
1602 Try to keep selection, then return the selected node.
1603 """
1604 tag = 'paste_node'
1605 c = self._check_c()
1606 p = self._get_p(param)
1607 s = param.get('name')
1608 if s is None: # pragma: no cover
1609 raise ServerError(f"{tag}: no string given")
1610 if p == c.p:
1611 c.pasteOutline(s=s)
1612 else:
1613 oldPosition = c.p # not same node, save position to possibly return to
1614 c.selectPosition(p)
1615 c.pasteOutline(s=s)
1616 if c.positionExists(oldPosition):
1617 # select if old position still valid
1618 c.selectPosition(oldPosition)
1619 else:
1620 oldPosition._childIndex = oldPosition._childIndex+1
1621 # Try again with childIndex incremented
1622 if c.positionExists(oldPosition):
1623 # additional try with higher childIndex
1624 c.selectPosition(oldPosition)
1625 return self._make_response()
1626 #@+node:felix.20220222173707.1: *5* paste_as_clone_node
1627 def paste_as_clone_node(self, param):
1628 """
1629 Pastes a node as a clone,
1630 Try to keep selection, then return the selected node.
1631 """
1632 tag = 'paste_as_clone_node'
1633 c = self._check_c()
1634 p = self._get_p(param)
1635 s = param.get('name')
1636 if s is None: # pragma: no cover
1637 raise ServerError(f"{tag}: no string given")
1638 if p == c.p:
1639 c.pasteOutlineRetainingClones(s=s)
1640 else:
1641 oldPosition = c.p # not same node, save position to possibly return to
1642 c.selectPosition(p)
1643 c.pasteOutlineRetainingClones(s=s)
1644 if c.positionExists(oldPosition):
1645 # select if old position still valid
1646 c.selectPosition(oldPosition)
1647 else:
1648 oldPosition._childIndex = oldPosition._childIndex+1
1649 # Try again with childIndex incremented
1650 if c.positionExists(oldPosition):
1651 # additional try with higher childIndex
1652 c.selectPosition(oldPosition)
1653 return self._make_response()
1654 #@+node:felix.20210621233316.59: *5* server.redo
1655 def redo(self, param):
1656 """Undo last un-doable operation"""
1657 c = self._check_c()
1658 u = c.undoer
1659 if u.canRedo():
1660 u.redo()
1661 return self._make_response()
1662 #@+node:felix.20210621233316.60: *5* server.set_body
1663 def set_body(self, param):
1664 """
1665 Undoably set body text of a v node.
1666 (Only if new string is different from actual existing body string)
1667 """
1668 tag = 'set_body'
1669 c = self._check_c()
1670 gnx = param.get('gnx')
1671 body = param.get('body')
1672 u, wrapper = c.undoer, c.frame.body.wrapper
1673 if body is None: # pragma: no cover
1674 raise ServerError(f"{tag}: no body given")
1675 for p in c.all_positions():
1676 if p.v.gnx == gnx:
1677 if body==p.v.b:
1678 return self._make_response()
1679 # Just exit if there is no need to change at all.
1680 bunch = u.beforeChangeNodeContents(p)
1681 p.v.setBodyString(body)
1682 u.afterChangeNodeContents(p, "Body Text", bunch)
1683 if c.p == p:
1684 wrapper.setAllText(body)
1685 if not self.c.isChanged(): # pragma: no cover
1686 c.setChanged()
1687 if not p.v.isDirty(): # pragma: no cover
1688 p.setDirty()
1689 break
1690 # additional forced string setting
1691 if gnx:
1692 v = c.fileCommands.gnxDict.get(gnx) # vitalije
1693 if v:
1694 v.b = body
1695 return self._make_response()
1696 #@+node:felix.20210621233316.61: *5* server.set_current_position
1697 def set_current_position(self, param):
1698 """Select position p. Or try to get p with gnx if not found."""
1699 tag = "set_current_position"
1700 c = self._check_c()
1701 p = self._get_p(param)
1702 if p:
1703 if c.positionExists(p):
1704 # set this node as selection
1705 c.selectPosition(p)
1706 else:
1707 ap = param.get('ap')
1708 foundPNode = self._positionFromGnx(ap.get('gnx', ""))
1709 if foundPNode:
1710 c.selectPosition(foundPNode)
1711 else:
1712 print(
1713 f"{tag}: node does not exist! "
1714 f"ap was: {json.dumps(ap, cls=SetEncoder)}", flush=True)
1716 return self._make_response()
1717 #@+node:felix.20210621233316.62: *5* server.set_headline
1718 def set_headline(self, param):
1719 """
1720 Undoably set p.h, where p is c.p if package["ap"] is missing.
1721 """
1722 c = self._check_c()
1723 p = self._get_p(param)
1724 u = c.undoer
1725 h = param.get('name', '')
1726 bunch = u.beforeChangeNodeContents(p)
1727 p.h = h
1728 u.afterChangeNodeContents(p, 'Change Headline', bunch)
1729 return self._make_response()
1730 #@+node:felix.20210621233316.63: *5* server.set_selection
1731 def set_selection(self, param):
1732 """
1733 Set the selection range for p.b, where p is c.p if package["ap"] is missing.
1735 Set the selection in the wrapper if p == c.p
1737 Package has these keys:
1739 - "ap": An archived position for position p.
1740 - "start": The start of the selection.
1741 - "end": The end of the selection.
1742 - "active": The insert point. Must be either start or end.
1743 - "scroll": An optional scroll position.
1745 Selection points can be sent as {"col":int, "line" int} dict
1746 or as numbers directly for convenience.
1747 """
1748 c = self._check_c()
1749 p = self._get_p(param) # Will raise ServerError if p does not exist.
1750 v = p.v
1751 wrapper = c.frame.body.wrapper
1752 convert = g.convertRowColToPythonIndex
1753 start = param.get('start', 0)
1754 end = param.get('end', 0)
1755 active = param.get('insert', 0) # temp var to check if int.
1756 scroll = param.get('scroll', 0)
1757 # If sent as number, use 'as is'
1758 if isinstance(active, int):
1759 insert = active
1760 startSel = start
1761 endSel = end
1762 else:
1763 # otherwise convert from line+col data.
1764 insert = convert(
1765 v.b, active['line'], active['col'])
1766 startSel = convert(
1767 v.b, start['line'], start['col'])
1768 endSel = convert(
1769 v.b, end['line'], end['col'])
1770 # If it's the currently selected node set the wrapper's states too
1771 if p == c.p:
1772 wrapper.setSelectionRange(startSel, endSel, insert)
1773 wrapper.setYScrollPosition(scroll)
1774 # Always set vnode attrs.
1775 v.scrollBarSpot = scroll
1776 v.insertSpot = insert
1777 v.selectionStart = startSel
1778 v.selectionLength = abs(startSel - endSel)
1779 return self._make_response()
1780 #@+node:felix.20211114202046.1: *5* server.set_ua_member
1781 def set_ua_member(self, param):
1782 """
1783 Set a single member of a node's ua.
1784 """
1785 self._check_c()
1786 p = self._get_p(param)
1787 name = param.get('name')
1788 value = param.get('value', '')
1789 if not p.v.u:
1790 p.v.u = {} # assert at least an empty dict if null or non existent
1791 if name and isinstance(name, str):
1792 p.v.u[name] = value
1793 return self._make_response()
1794 #@+node:felix.20211114202058.1: *5* server.set_ua
1795 def set_ua(self, param):
1796 """
1797 Replace / set the whole user attribute dict of a node.
1798 """
1799 self._check_c()
1800 p = self._get_p(param)
1801 ua = param.get('ua', {})
1802 p.v.u = ua
1803 return self._make_response()
1804 #@+node:felix.20210621233316.64: *5* server.toggle_mark
1805 def toggle_mark(self, param):
1806 """
1807 Toggle the mark at position p.
1808 Try to keep selection, then return the selected node that remains.
1809 """
1810 c = self._check_c()
1811 p = self._get_p(param)
1812 if p == c.p:
1813 c.markHeadline()
1814 else:
1815 oldPosition = c.p
1816 c.selectPosition(p)
1817 c.markHeadline()
1818 if c.positionExists(oldPosition):
1819 c.selectPosition(oldPosition)
1820 # return selected node either ways
1821 return self._make_response()
1822 #@+node:felix.20210621233316.65: *5* server.mark_node
1823 def mark_node(self, param):
1824 """
1825 Mark a node.
1826 Try to keep selection, then return the selected node that remains.
1827 """
1828 # pylint: disable=no-else-return
1829 self._check_c()
1830 p = self._get_p(param)
1831 if p.isMarked():
1832 return self._make_response()
1833 else:
1834 return self.toggle_mark(param)
1836 #@+node:felix.20210621233316.66: *5* server.unmark_node
1837 def unmark_node(self, param):
1838 """
1839 Unmark a node.
1840 Try to keep selection, then return the selected node that remains.
1841 """
1842 # pylint: disable=no-else-return
1843 self._check_c()
1844 p = self._get_p(param)
1845 if not p.isMarked():
1846 return self._make_response()
1847 else:
1848 return self.toggle_mark(param)
1849 #@+node:felix.20210621233316.67: *5* server.undo
1850 def undo(self, param):
1851 """Undo last un-doable operation"""
1852 c = self._check_c()
1853 u = c.undoer
1854 if u.canUndo():
1855 u.undo()
1856 # Félix: Caller can get focus using other calls.
1857 return self._make_response()
1858 #@+node:felix.20210621233316.68: *4* server:server commands
1859 #@+node:felix.20210914230846.1: *5* server.get_version
1860 def get_version(self, param):
1861 """
1862 Return this server program name and version as a string representation
1863 along with the three version members as numbers 'major', 'minor' and 'patch'.
1864 """
1865 # uses the __version__ global constant and the v1, v2, v3 global version numbers
1866 result = {"version": __version__ , "major": v1, "minor": v2, "patch": v3}
1867 return self._make_minimal_response(result)
1868 #@+node:felix.20210818012827.1: *5* server.do_nothing
1869 def do_nothing(self, param):
1870 """Simply return states from _make_response"""
1871 return self._make_response()
1872 #@+node:felix.20210621233316.69: *5* server.set_ask_result
1873 def set_ask_result(self, param):
1874 """Got the result to an asked question/warning from client"""
1875 tag = "set_ask_result"
1876 result = param.get("result")
1877 if not result:
1878 raise ServerError(f"{tag}: no param result")
1879 g.app.externalFilesController.clientResult(result)
1880 return self._make_response()
1881 #@+node:felix.20210621233316.70: *5* server.set_config
1882 def set_config(self, param):
1883 """Got auto-reload's config from client"""
1884 self.leoServerConfig = param # PARAM IS THE CONFIG-DICT
1885 return self._make_response()
1886 #@+node:felix.20210621233316.71: *5* server.error
1887 def error(self, param):
1888 """For unit testing. Raise ServerError"""
1889 raise ServerError("error called")
1890 #@+node:felix.20210621233316.72: *5* server.get_all_leo_commands & helper
1891 def get_all_leo_commands(self, param):
1892 """Return a list of all commands that make sense for connected clients."""
1893 tag = 'get_all_leo_commands'
1894 # #173: Use the present commander to get commands created by @button and @command.
1895 c = self.c
1896 d = c.commandsDict if c else {} # keys are command names, values are functions.
1897 bad_names = self._bad_commands(c) # #92.
1898 good_names = self._good_commands()
1899 duplicates = set(bad_names).intersection(set(good_names))
1900 if duplicates: # pragma: no cover
1901 print(f"{tag}: duplicate command names...", flush=True)
1902 for z in sorted(duplicates):
1903 print(z, flush=True)
1904 result = []
1905 for command_name in sorted(d):
1906 func = d.get(command_name)
1907 if not func: # pragma: no cover
1908 print(f"{tag}: no func: {command_name!r}", flush=True)
1909 continue
1910 if command_name in bad_names: # #92.
1911 continue
1912 doc = func.__doc__ or ''
1913 result.append({
1914 "label": command_name, # Kebab-cased Command name to be called
1915 "detail": doc,
1916 })
1917 if self.log_flag: # pragma: no cover
1918 print(f"\n{tag}: {len(result)} leo commands\n", flush=True)
1919 g.printObj([z.get("label") for z in result], tag=tag)
1920 print('', flush=True)
1921 return self._make_minimal_response({"commands": result})
1922 #@+node:felix.20210621233316.73: *6* server._bad_commands
1923 def _bad_commands(self, c):
1924 """Return the list of command names that connected clients should ignore."""
1925 d = c.commandsDict if c else {} # keys are command names, values are functions.
1926 bad = []
1927 #
1928 # leoInteg #173: Remove only vim commands.
1929 for command_name in sorted(d):
1930 if command_name.startswith(':'):
1931 bad.append(command_name)
1932 #
1933 # Remove other commands.
1934 # This is a hand-curated list.
1935 bad_list = [
1936 'demangle-recent-files',
1937 'clean-main-spell-dict',
1938 'clean-persistence',
1939 'clean-recent-files',
1940 'clean-spellpyx',
1941 'clean-user-spell-dict',
1942 'clear-recent-files',
1943 'delete-first-icon',
1944 'delete-last-icon',
1945 'delete-node-icons',
1946 'insert-icon',
1947 'set-ua', # TODO : Should be easy to implement
1948 'export-headlines', # export TODO
1949 'export-jupyter-notebook', # export TODO
1950 'outline-to-cweb', # export TODO
1951 'outline-to-noweb', # export TODO
1952 'remove-sentinels', # import TODO
1954 'save-all',
1955 'save-file-as-zipped',
1956 'write-file-from-node',
1957 'edit-setting',
1958 'edit-shortcut',
1959 'goto-line',
1960 'pdb',
1961 'xdb',
1962 'compare-two-leo-files',
1963 'file-compare-two-leo-files',
1964 'edit-recent-files',
1965 'exit-leo',
1966 'help', # To do.
1967 'help-for-abbreviations',
1968 'help-for-autocompletion',
1969 'help-for-bindings',
1970 'help-for-command',
1971 'help-for-creating-external-files',
1972 'help-for-debugging-commands',
1973 'help-for-drag-and-drop',
1974 'help-for-dynamic-abbreviations',
1975 'help-for-find-commands',
1976 'help-for-keystroke',
1977 'help-for-minibuffer',
1978 'help-for-python',
1979 'help-for-regular-expressions',
1980 'help-for-scripting',
1981 'help-for-settings',
1982 'join-leo-irc', # Some online irc - parameters not working anymore
1984 'print-body',
1985 'print-cmd-docstrings',
1986 'print-expanded-body',
1987 'print-expanded-html',
1988 'print-html',
1989 'print-marked-bodies',
1990 'print-marked-html',
1991 'print-marked-nodes',
1992 'print-node',
1993 'print-sep',
1994 'print-tree-bodies',
1995 'print-tree-html',
1996 'print-tree-nodes',
1997 'print-window-state',
1998 'quit-leo',
1999 'reload-style-sheets',
2000 'save-buffers-kill-leo',
2001 'screen-capture-5sec',
2002 'screen-capture-now',
2003 'set-reference-file', # TODO : maybe offer this
2004 'show-style-sheet',
2005 'sort-recent-files',
2006 'view-lossage',
2008 # Buffers commands (Usage?)
2009 'buffer-append-to',
2010 'buffer-copy',
2011 'buffer-insert',
2012 'buffer-kill',
2013 'buffer-prepend-to',
2014 'buffer-switch-to',
2015 'buffers-list',
2016 'buffers-list-alphabetically',
2018 # Open specific files... (MAYBE MAKE AVAILABLE?)
2019 # 'ekr-projects',
2020 'leo-cheat-sheet', # These duplicates are useful.
2021 'leo-dist-leo',
2022 'leo-docs-leo',
2023 'leo-plugins-leo',
2024 'leo-py-leo',
2025 'leo-quickstart-leo',
2026 'leo-scripts-leo',
2027 'leo-unittest-leo',
2029 # 'scripts',
2030 'settings',
2032 'open-cheat-sheet-leo',
2033 'cheat-sheet-leo',
2034 'cheat-sheet',
2035 'open-desktop-integration-leo',
2036 'desktop-integration-leo',
2037 'open-leo-dist-leo',
2038 'leo-dist-leo',
2039 'open-leo-docs-leo',
2040 'leo-docs-leo',
2041 'open-leo-plugins-leo',
2042 'leo-plugins-leo',
2043 'open-leo-py-leo',
2044 'leo-py-leo',
2045 'open-leo-py-ref-leo',
2046 'leo-py-ref-leo',
2047 'open-leo-py',
2048 'open-leo-settings',
2049 'open-leo-settings-leo',
2050 'open-local-settings',
2051 'my-leo-settings',
2052 'open-my-leo-settings',
2053 'open-my-leo-settings-leo',
2054 'leo-settings'
2055 'open-quickstart-leo',
2056 'leo-quickstart-leo'
2057 'open-scripts-leo',
2058 'leo-scripts-leo'
2059 'open-unittest-leo',
2060 'leo-unittest-leo',
2062 # Open other places...
2063 'desktop-integration-leo',
2065 'open-offline-tutorial',
2066 'open-online-home',
2067 'open-online-toc',
2068 'open-online-tutorials',
2069 'open-online-videos',
2070 'open-recent-file',
2071 'open-theme-file',
2072 'open-url',
2073 'open-url-under-cursor',
2074 'open-users-guide',
2076 # Diffs - needs open file dialog
2077 'diff-and-open-leo-files',
2078 'diff-leo-files',
2080 # --- ORIGINAL BAD COMMANDS START HERE ---
2081 # Abbreviations...
2082 'abbrev-kill-all',
2083 'abbrev-list',
2084 'dabbrev-completion',
2085 'dabbrev-expands',
2087 # Autocompletion...
2088 'auto-complete',
2089 'auto-complete-force',
2090 'disable-autocompleter',
2091 'disable-calltips',
2092 'enable-autocompleter',
2093 'enable-calltips',
2095 # Debugger...
2096 'debug',
2097 'db-again',
2098 'db-b',
2099 'db-c',
2100 'db-h',
2101 'db-input',
2102 'db-l',
2103 'db-n',
2104 'db-q',
2105 'db-r',
2106 'db-s',
2107 'db-status',
2108 'db-w',
2110 # File operations...
2111 'directory-make',
2112 'directory-remove',
2113 'file-delete',
2114 'file-diff-files',
2115 'file-insert',
2116 #'file-new',
2117 #'file-open-by-name',
2119 # All others...
2120 'shell-command',
2121 'shell-command-on-region',
2122 'cheat-sheet',
2123 'dehoist', # Duplicates of de-hoist.
2124 #'find-clone-all',
2125 #'find-clone-all-flattened',
2126 #'find-clone-tag',
2127 #'find-all',
2128 'find-all-unique-regex',
2129 'find-character',
2130 'find-character-extend-selection',
2131 #'find-next',
2132 #'find-prev',
2133 'find-word',
2134 'find-word-in-line',
2136 'global-search',
2138 'isearch-backward',
2139 'isearch-backward-regexp',
2140 'isearch-forward',
2141 'isearch-forward-regexp',
2142 'isearch-with-present-options',
2144 #'replace',
2145 #'replace-all',
2146 'replace-current-character',
2147 #'replace-then-find',
2149 're-search-backward',
2150 're-search-forward',
2152 #'search-backward',
2153 #'search-forward',
2154 'search-return-to-origin',
2156 'set-find-everywhere',
2157 'set-find-node-only',
2158 'set-find-suboutline-only',
2159 'set-replace-string',
2160 'set-search-string',
2162 #'show-find-options',
2164 #'start-search',
2166 'toggle-find-collapses-nodes',
2167 #'toggle-find-ignore-case-option',
2168 #'toggle-find-in-body-option',
2169 #'toggle-find-in-headline-option',
2170 #'toggle-find-mark-changes-option',
2171 #'toggle-find-mark-finds-option',
2172 #'toggle-find-regex-option',
2173 #'toggle-find-word-option',
2174 'toggle-find-wrap-around-option',
2176 'word-search-backward',
2177 'word-search-forward',
2179 # Buttons...
2180 'delete-script-button-button',
2182 # Clicks...
2183 'click-click-box',
2184 'click-icon-box',
2185 'ctrl-click-at-cursor',
2186 'ctrl-click-icon',
2187 'double-click-icon-box',
2188 'right-click-icon',
2190 # Editors...
2191 'add-editor', 'editor-add',
2192 'delete-editor', 'editor-delete',
2193 'detach-editor-toggle',
2194 'detach-editor-toggle-max',
2196 # Focus...
2197 'cycle-editor-focus', 'editor-cycle-focus',
2198 'focus-to-body',
2199 'focus-to-find',
2200 'focus-to-log',
2201 'focus-to-minibuffer',
2202 'focus-to-nav',
2203 'focus-to-spell-tab',
2204 'focus-to-tree',
2206 'tab-cycle-next',
2207 'tab-cycle-previous',
2208 'tab-detach',
2210 # Headlines..
2211 'abort-edit-headline',
2212 'edit-headline',
2213 'end-edit-headline',
2215 # Layout and panes...
2216 'adoc',
2217 'adoc-with-preview',
2219 'contract-body-pane',
2220 'contract-log-pane',
2221 'contract-outline-pane',
2223 'edit-pane-csv',
2224 'edit-pane-test-open',
2225 'equal-sized-panes',
2226 'expand-log-pane',
2227 'expand-body-pane',
2228 'expand-outline-pane',
2230 'free-layout-context-menu',
2231 'free-layout-load',
2232 'free-layout-restore',
2233 'free-layout-zoom',
2235 'zoom-in',
2236 'zoom-out',
2238 # Log
2239 'clear-log',
2241 # Menus...
2242 'activate-cmds-menu',
2243 'activate-edit-menu',
2244 'activate-file-menu',
2245 'activate-help-menu',
2246 'activate-outline-menu',
2247 'activate-plugins-menu',
2248 'activate-window-menu',
2249 'context-menu-open',
2250 'menu-shortcut',
2252 # Modes...
2253 'clear-extend-mode',
2255 # Outline... (Commented off by Félix, Should work)
2256 #'contract-or-go-left',
2257 #'contract-node',
2258 #'contract-parent',
2260 # Scrolling...
2261 'scroll-down-half-page',
2262 'scroll-down-line',
2263 'scroll-down-page',
2264 'scroll-outline-down-line',
2265 'scroll-outline-down-page',
2266 'scroll-outline-left',
2267 'scroll-outline-right',
2268 'scroll-outline-up-line',
2269 'scroll-outline-up-page',
2270 'scroll-up-half-page',
2271 'scroll-up-line',
2272 'scroll-up-page',
2274 # Windows...
2275 'about-leo',
2277 'cascade-windows',
2278 'close-others',
2279 'close-window',
2281 'iconify-frame',
2283 'find-tab-hide',
2284 #'find-tab-open',
2286 'hide-body-dock',
2287 'hide-body-pane',
2288 'hide-invisibles',
2289 'hide-log-pane',
2290 'hide-outline-dock',
2291 'hide-outline-pane',
2292 'hide-tabs-dock',
2294 'minimize-all',
2296 'resize-to-screen',
2298 'show-body-dock',
2299 'show-hide-body-dock',
2300 'show-hide-outline-dock',
2301 'show-hide-render-dock',
2302 'show-hide-tabs-dock',
2303 'show-tabs-dock',
2304 'clean-diff',
2305 'cm-external-editor',
2307 'delete-@button-parse-json-button',
2308 'delete-trace-statements',
2310 'disable-idle-time-events',
2312 'enable-idle-time-events',
2313 'enter-quick-command-mode',
2314 'exit-named-mode',
2316 'F6-open-console',
2318 'flush-lines',
2319 'full-command',
2321 'get-child-headlines',
2323 'history',
2325 'insert-file-name',
2327 'justify-toggle-auto',
2329 'keep-lines',
2330 'keyboard-quit',
2332 'line-number',
2333 'line-numbering-toggle',
2334 'line-to-headline',
2336 'marked-list',
2338 'mode-help',
2340 'open-python-window',
2342 'open-with-idle',
2343 'open-with-open-office',
2344 'open-with-scite',
2345 'open-with-word',
2347 'recolor',
2348 'redraw',
2350 'repeat-complex-command',
2352 'session-clear',
2353 'session-create',
2354 'session-refresh',
2355 'session-restore',
2356 'session-snapshot-load',
2357 'session-snapshot-save',
2359 'set-colors',
2360 'set-command-state',
2361 'set-comment-column',
2362 'set-extend-mode',
2363 'set-fill-column',
2364 'set-fill-prefix',
2365 'set-font',
2366 'set-insert-state',
2367 'set-overwrite-state',
2368 'set-silent-mode',
2370 'show-buttons',
2371 'show-calltips',
2372 'show-calltips-force',
2373 'show-color-names',
2374 'show-color-wheel',
2375 'show-commands',
2376 'show-file-line',
2378 'show-focus',
2379 'show-fonts',
2381 'show-invisibles',
2382 'show-node-uas',
2383 'show-outline-dock',
2384 'show-plugin-handlers',
2385 'show-plugins-info',
2386 'show-settings',
2387 'show-settings-outline',
2388 'show-spell-info',
2389 'show-stats',
2390 'show-tips',
2392 'style-set-selected',
2394 'suspend',
2396 'toggle-abbrev-mode',
2397 'toggle-active-pane',
2398 'toggle-angle-brackets',
2399 'toggle-at-auto-at-edit',
2400 'toggle-autocompleter',
2401 'toggle-calltips',
2402 'toggle-case-region',
2403 'toggle-extend-mode',
2404 'toggle-idle-time-events',
2405 'toggle-input-state',
2406 'toggle-invisibles',
2407 'toggle-line-numbering-root',
2408 'toggle-sparse-move',
2409 'toggle-split-direction',
2411 'what-line',
2412 'eval',
2413 'eval-block',
2414 'eval-last',
2415 'eval-last-pretty',
2416 'eval-replace',
2418 'find-quick',
2419 'find-quick-changed',
2420 'find-quick-selected',
2421 'find-quick-test-failures',
2422 'find-quick-timeline',
2424 #'goto-next-history-node',
2425 #'goto-prev-history-node',
2427 'preview',
2428 'preview-body',
2429 'preview-expanded-body',
2430 'preview-expanded-html',
2431 'preview-html',
2432 'preview-marked-bodies',
2433 'preview-marked-html',
2434 'preview-marked-nodes',
2435 'preview-node',
2436 'preview-tree-bodies',
2437 'preview-tree-html',
2438 'preview-tree-nodes',
2440 'spell-add',
2441 'spell-as-you-type-next',
2442 'spell-as-you-type-toggle',
2443 'spell-as-you-type-undo',
2444 'spell-as-you-type-wrap',
2445 'spell-change',
2446 'spell-change-then-find',
2447 'spell-find',
2448 'spell-ignore',
2449 'spell-tab-hide',
2450 'spell-tab-open',
2452 #'tag-children',
2454 'todo-children-todo',
2455 'todo-dec-pri',
2456 'todo-find-todo',
2457 'todo-fix-datetime',
2458 'todo-inc-pri',
2460 'vr',
2461 'vr-contract',
2462 'vr-expand',
2463 'vr-hide',
2464 'vr-lock',
2465 'vr-pause-play-movie',
2466 'vr-show',
2467 'vr-toggle',
2468 'vr-unlock',
2469 'vr-update',
2470 'vr-zoom',
2472 'vs-create-tree',
2473 'vs-dump',
2474 'vs-reset',
2475 'vs-update',
2476 # Connected client's text editing commands should cover all of these...
2477 'add-comments',
2478 'add-space-to-lines',
2479 'add-tab-to-lines',
2480 'align-eq-signs',
2482 'back-char',
2483 'back-char-extend-selection',
2484 'back-page',
2485 'back-page-extend-selection',
2486 'back-paragraph',
2487 'back-paragraph-extend-selection',
2488 'back-sentence',
2489 'back-sentence-extend-selection',
2490 'back-to-home',
2491 'back-to-home-extend-selection',
2492 'back-to-indentation',
2493 'back-word',
2494 'back-word-extend-selection',
2495 'back-word-smart',
2496 'back-word-smart-extend-selection',
2497 'backward-delete-char',
2498 'backward-delete-word',
2499 'backward-delete-word-smart',
2500 'backward-find-character',
2501 'backward-find-character-extend-selection',
2502 'backward-kill-paragraph',
2503 'backward-kill-sentence',
2504 'backward-kill-word',
2505 'beginning-of-buffer',
2506 'beginning-of-buffer-extend-selection',
2507 'beginning-of-line',
2508 'beginning-of-line-extend-selection',
2510 'capitalize-word',
2511 'center-line',
2512 'center-region',
2513 'clean-all-blank-lines',
2514 'clean-all-lines',
2515 'clean-body',
2516 'clean-lines',
2517 'clear-kill-ring',
2518 'clear-selected-text',
2519 'convert-blanks',
2520 'convert-tabs',
2521 'copy-text',
2522 'cut-text',
2524 'delete-char',
2525 'delete-comments',
2526 'delete-indentation',
2527 'delete-spaces',
2528 'delete-word',
2529 'delete-word-smart',
2530 'downcase-region',
2531 'downcase-word',
2533 'end-of-buffer',
2534 'end-of-buffer-extend-selection',
2535 'end-of-line',
2536 'end-of-line-extend-selection',
2538 'exchange-point-mark',
2540 'extend-to-line',
2541 'extend-to-paragraph',
2542 'extend-to-sentence',
2543 'extend-to-word',
2545 'fill-paragraph',
2546 'fill-region',
2547 'fill-region-as-paragraph',
2549 'finish-of-line',
2550 'finish-of-line-extend-selection',
2552 'forward-char',
2553 'forward-char-extend-selection',
2554 'forward-end-word',
2555 'forward-end-word-extend-selection',
2556 'forward-page',
2557 'forward-page-extend-selection',
2558 'forward-paragraph',
2559 'forward-paragraph-extend-selection',
2560 'forward-sentence',
2561 'forward-sentence-extend-selection',
2562 'forward-word',
2563 'forward-word-extend-selection',
2564 'forward-word-smart',
2565 'forward-word-smart-extend-selection',
2567 'go-anywhere',
2568 'go-back',
2569 'go-forward',
2570 'goto-char',
2572 'indent-region',
2573 'indent-relative',
2574 'indent-rigidly',
2575 'indent-to-comment-column',
2577 'insert-hard-tab',
2578 'insert-newline',
2579 'insert-parentheses',
2580 'insert-soft-tab',
2582 'kill-line',
2583 'kill-paragraph',
2584 'kill-pylint',
2585 'kill-region',
2586 'kill-region-save',
2587 'kill-sentence',
2588 'kill-to-end-of-line',
2589 'kill-word',
2590 'kill-ws',
2592 'match-brackets',
2594 'move-lines-down',
2595 'move-lines-up',
2596 'move-past-close',
2597 'move-past-close-extend-selection',
2599 'newline-and-indent',
2600 'next-line',
2601 'next-line-extend-selection',
2602 'next-or-end-of-line',
2603 'next-or-end-of-line-extend-selection',
2605 'previous-line',
2606 'previous-line-extend-selection',
2607 'previous-or-beginning-of-line',
2608 'previous-or-beginning-of-line-extend-selection',
2610 'rectangle-clear',
2611 'rectangle-close',
2612 'rectangle-delete',
2613 'rectangle-kill',
2614 'rectangle-open',
2615 'rectangle-string',
2616 'rectangle-yank',
2618 'remove-blank-lines',
2619 'remove-newlines',
2620 'remove-space-from-lines',
2621 'remove-tab-from-lines',
2623 'reverse-region',
2624 'reverse-sort-lines',
2625 'reverse-sort-lines-ignoring-case',
2627 'paste-text',
2628 'pop-cursor',
2629 'push-cursor',
2631 'select-all',
2632 'select-next-trace-statement',
2633 'select-to-matching-bracket',
2635 'sort-columns',
2636 'sort-fields',
2637 'sort-lines',
2638 'sort-lines-ignoring-case',
2640 'split-defs',
2641 'split-line',
2643 'start-of-line',
2644 'start-of-line-extend-selection',
2646 'tabify',
2647 'transpose-chars',
2648 'transpose-lines',
2649 'transpose-words',
2651 'unformat-paragraph',
2652 'unindent-region',
2654 'untabify',
2656 'upcase-region',
2657 'upcase-word',
2658 'update-ref-file',
2660 'yank',
2661 'yank-pop',
2663 'zap-to-character',
2665 ]
2666 bad.extend(bad_list)
2667 result = list(sorted(bad))
2668 return result
2669 #@+node:felix.20210621233316.74: *6* server._good_commands
2670 def _good_commands(self):
2671 """Defined commands that should be available in a connected client"""
2672 good_list = [
2674 'contract-all',
2675 'contract-all-other-nodes',
2676 'clone-node',
2677 'copy-node',
2678 'copy-marked-nodes',
2679 'cut-node',
2681 'de-hoist',
2682 'delete-marked-nodes',
2683 'delete-node',
2684 # 'demangle-recent-files',
2685 'demote',
2686 'do-nothing',
2687 'expand-and-go-right',
2688 'expand-next-level',
2689 'expand-node',
2690 'expand-or-go-right',
2691 'expand-prev-level',
2692 'expand-to-level-1',
2693 'expand-to-level-2',
2694 'expand-to-level-3',
2695 'expand-to-level-4',
2696 'expand-to-level-5',
2697 'expand-to-level-6',
2698 'expand-to-level-7',
2699 'expand-to-level-8',
2700 'expand-to-level-9',
2701 'expand-all',
2702 'expand-all-subheads',
2703 'expand-ancestors-only',
2705 'find-next-clone',
2707 'goto-first-node',
2708 'goto-first-sibling',
2709 'goto-first-visible-node',
2710 'goto-last-node',
2711 'goto-last-sibling',
2712 'goto-last-visible-node',
2713 'goto-next-changed',
2714 'goto-next-clone',
2715 'goto-next-marked',
2716 'goto-next-node',
2717 'goto-next-sibling',
2718 'goto-next-visible',
2719 'goto-parent',
2720 'goto-prev-marked',
2721 'goto-prev-node',
2722 'goto-prev-sibling',
2723 'goto-prev-visible',
2725 'hoist',
2727 'insert-node',
2728 'insert-node-before',
2729 'insert-as-first-child',
2730 'insert-as-last-child',
2731 'insert-child',
2733 'mark',
2734 'mark-changed-items',
2735 'mark-first-parents',
2736 'mark-subheads',
2738 'move-marked-nodes',
2739 'move-outline-down',
2740 'move-outline-left',
2741 'move-outline-right',
2742 'move-outline-up',
2744 'paste-node',
2745 'paste-retaining-clones',
2746 'promote',
2747 'promote-bodies',
2748 'promote-headlines',
2750 'sort-children',
2751 'sort-siblings',
2753 'tangle',
2754 'tangle-all',
2755 'tangle-marked',
2757 'unmark-all',
2758 'unmark-first-parents',
2759 #'clean-main-spell-dict',
2760 #'clean-persistence',
2761 #'clean-recent-files',
2762 #'clean-spellpyx',
2763 #'clean-user-spell-dict',
2765 'clear-all-caches',
2766 'clear-all-hoists',
2767 'clear-all-uas',
2768 'clear-cache',
2769 'clear-node-uas',
2770 #'clear-recent-files',
2772 #'delete-first-icon', # ? maybe move to bad commands?
2773 #'delete-last-icon', # ? maybe move to bad commands?
2774 #'delete-node-icons', # ? maybe move to bad commands?
2776 'dump-caches',
2777 'dump-clone-parents',
2778 'dump-expanded',
2779 'dump-node',
2780 'dump-outline',
2782 #'insert-icon', # ? maybe move to bad commands?
2784 #'set-ua',
2786 'show-all-uas',
2787 'show-bindings',
2788 'show-clone-ancestors',
2789 'show-clone-parents',
2791 # Export files...
2792 #'export-headlines', # export
2793 #'export-jupyter-notebook', # export
2794 #'outline-to-cweb', # export
2795 #'outline-to-noweb', # export
2796 #'remove-sentinels', # import
2797 'typescript-to-py',
2799 # Import files... # done through import all
2800 'import-MORE-files',
2801 'import-file',
2802 'import-free-mind-files',
2803 'import-jupyter-notebook',
2804 'import-legacy-external-files',
2805 'import-mind-jet-files',
2806 'import-tabbed-files',
2807 'import-todo-text-files',
2808 'import-zim-folder',
2810 # Read outlines...
2811 'read-at-auto-nodes',
2812 'read-at-file-nodes',
2813 'read-at-shadow-nodes',
2814 'read-file-into-node',
2815 'read-outline-only',
2816 'read-ref-file',
2818 # Save Files.
2819 'file-save',
2820 'file-save-as',
2821 'file-save-by-name',
2822 'file-save-to',
2823 'save',
2824 'save-as',
2825 'save-file',
2826 'save-file-as',
2827 'save-file-by-name',
2828 'save-file-to',
2829 'save-to',
2831 # Write parts of outlines...
2832 'write-at-auto-nodes',
2833 'write-at-file-nodes',
2834 'write-at-shadow-nodes',
2835 'write-dirty-at-auto-nodes',
2836 'write-dirty-at-file-nodes',
2837 'write-dirty-at-shadow-nodes',
2838 'write-edited-recent-files',
2839 #'write-file-from-node',
2840 'write-missing-at-file-nodes',
2841 'write-outline-only',
2843 'clone-find-all',
2844 'clone-find-all-flattened',
2845 'clone-find-all-flattened-marked',
2846 'clone-find-all-marked',
2847 'clone-find-parents',
2848 'clone-find-tag',
2849 'clone-marked-nodes',
2850 'clone-node-to-last-node',
2852 'clone-to-at-spot',
2854 #'edit-setting',
2855 #'edit-shortcut',
2857 'execute-pytest',
2858 'execute-script',
2859 'extract',
2860 'extract-names',
2862 'goto-any-clone',
2863 'goto-global-line',
2864 #'goto-line',
2865 'git-diff', 'gd',
2867 'log-kill-listener', 'kill-log-listener',
2868 'log-listen', 'listen-to-log',
2870 'make-stub-files',
2872 #'pdb',
2874 'redo',
2875 'rst3',
2876 'run-all-unit-tests-externally',
2877 'run-all-unit-tests-locally',
2878 'run-marked-unit-tests-externally',
2879 'run-marked-unit-tests-locally',
2880 'run-selected-unit-tests-externally',
2881 'run-selected-unit-tests-locally',
2882 'run-tests',
2884 'undo',
2886 #'xdb',
2888 # Beautify, blacken, fstringify...
2889 'beautify-files',
2890 'beautify-files-diff',
2891 'blacken-files',
2892 'blacken-files-diff',
2893 #'diff-and-open-leo-files',
2894 'diff-beautify-files',
2895 'diff-fstringify-files',
2896 #'diff-leo-files',
2897 'diff-marked-nodes',
2898 'fstringify-files',
2899 'fstringify-files-diff',
2900 'fstringify-files-silent',
2901 'pretty-print-c',
2902 'silent-fstringify-files',
2904 # All other commands...
2905 'at-file-to-at-auto',
2907 'beautify-c',
2909 'cls',
2910 'c-to-python',
2911 'c-to-python-clean-docs',
2912 'check-derived-file',
2913 'check-outline',
2914 'code-to-rst',
2915 #'compare-two-leo-files',
2916 'convert-all-blanks',
2917 'convert-all-tabs',
2918 'count-children',
2919 'count-pages',
2920 'count-region',
2922 #'desktop-integration-leo',
2924 #'edit-recent-files',
2925 #'exit-leo',
2927 #'file-compare-two-leo-files',
2928 'find-def',
2929 'find-long-lines',
2930 'find-missing-docstrings',
2931 'flake8-files',
2932 'flatten-outline',
2933 'flatten-outline-to-node',
2934 'flatten-script',
2936 'gc-collect-garbage',
2937 'gc-dump-all-objects',
2938 'gc-dump-new-objects',
2939 'gc-dump-objects-verbose',
2940 'gc-show-summary',
2942 #'help', # To do.
2943 #'help-for-abbreviations',
2944 #'help-for-autocompletion',
2945 #'help-for-bindings',
2946 #'help-for-command',
2947 #'help-for-creating-external-files',
2948 #'help-for-debugging-commands',
2949 #'help-for-drag-and-drop',
2950 #'help-for-dynamic-abbreviations',
2951 #'help-for-find-commands',
2952 #'help-for-keystroke',
2953 #'help-for-minibuffer',
2954 #'help-for-python',
2955 #'help-for-regular-expressions',
2956 #'help-for-scripting',
2957 #'help-for-settings',
2959 'insert-body-time', # ?
2960 'insert-headline-time',
2961 'insert-jupyter-toc',
2962 'insert-markdown-toc',
2964 'find-var',
2966 #'join-leo-irc',
2967 'join-node-above',
2968 'join-node-below',
2969 'join-selection-to-node-below',
2971 'move-lines-to-next-node',
2973 'new',
2975 'open-outline',
2977 'parse-body',
2978 'parse-json',
2979 'pandoc',
2980 'pandoc-with-preview',
2981 'paste-as-template',
2983 #'print-body',
2984 #'print-cmd-docstrings',
2985 #'print-expanded-body',
2986 #'print-expanded-html',
2987 #'print-html',
2988 #'print-marked-bodies',
2989 #'print-marked-html',
2990 #'print-marked-nodes',
2991 #'print-node',
2992 #'print-sep',
2993 #'print-tree-bodies',
2994 #'print-tree-html',
2995 #'print-tree-nodes',
2996 #'print-window-state',
2998 'pyflakes',
2999 'pylint',
3000 'pylint-kill',
3001 'python-to-coffeescript',
3003 #'quit-leo',
3005 'reformat-body',
3006 'reformat-paragraph',
3007 'refresh-from-disk',
3008 'reload-settings',
3009 #'reload-style-sheets',
3010 'revert',
3012 #'save-buffers-kill-leo',
3013 #'screen-capture-5sec',
3014 #'screen-capture-now',
3015 'script-button', # ?
3016 #'set-reference-file',
3017 #'show-style-sheet',
3018 #'sort-recent-files',
3019 'sphinx',
3020 'sphinx-with-preview',
3021 'style-reload', # ?
3023 'untangle',
3024 'untangle-all',
3025 'untangle-marked',
3027 #'view-lossage', # ?
3029 'weave',
3031 # Dubious commands (to do)...
3032 'act-on-node',
3034 'cfa',
3035 'cfam',
3036 'cff',
3037 'cffm',
3038 'cft',
3040 #'buffer-append-to',
3041 #'buffer-copy',
3042 #'buffer-insert',
3043 #'buffer-kill',
3044 #'buffer-prepend-to',
3045 #'buffer-switch-to',
3046 #'buffers-list',
3047 #'buffers-list-alphabetically',
3049 'chapter-back',
3050 'chapter-next',
3051 'chapter-select',
3052 'chapter-select-main',
3053 'create-def-list', # ?
3054 ]
3055 return good_list
3056 #@+node:felix.20210621233316.75: *5* server.get_all_server_commands & helpers
3057 def get_all_server_commands(self, param):
3058 """
3059 Public server method:
3060 Return the names of all callable public methods of the server.
3061 """
3062 tag = 'get_all_server_commands'
3063 names = self._get_all_server_commands()
3064 if self.log_flag: # pragma: no cover
3065 print(f"\n{tag}: {len(names)} server commands\n", flush=True)
3066 g.printObj(names, tag=tag)
3067 print('', flush=True)
3068 return self._make_response({"server-commands": names})
3069 #@+node:felix.20210914231602.1: *6* _get_all_server_commands
3070 def _get_all_server_commands(self):
3071 """
3072 Private server method:
3073 Return the names of all callable public methods of the server.
3074 (Methods that do not start with an underscore '_')
3075 """
3076 members = inspect.getmembers(self, inspect.ismethod)
3077 return sorted([name for (name, value) in members if not name.startswith('_')])
3078 #@+node:felix.20210621233316.76: *5* server.init_connection
3079 def _init_connection(self, web_socket): # pragma: no cover (tested in client).
3080 """Begin the connection."""
3081 global connectionsTotal
3082 if connectionsTotal == 1:
3083 # First connection, so "Master client" setup
3084 self.web_socket = web_socket
3085 self.loop = asyncio.get_event_loop()
3086 else:
3087 # already exist, so "spectator-clients" setup
3088 pass # nothing for now
3089 #@+node:felix.20210621233316.77: *5* server.shut_down
3090 def shut_down(self, param):
3091 """Shut down the server."""
3092 tag = 'shut_down'
3093 n = len(g.app.commanders())
3094 if n: # pragma: no cover
3095 raise ServerError(f"{tag}: {n} open outlines")
3096 raise TerminateServer("client requested shut down")
3097 #@+node:felix.20210621233316.78: *3* server:server utils
3098 #@+node:felix.20210621233316.79: *4* server._ap_to_p
3099 def _ap_to_p(self, ap):
3100 """
3101 Convert ap (archived position, a dict) to a valid Leo position.
3103 Return False on any kind of error to support calls to invalid positions
3104 after a document has been closed of switched and interface interaction
3105 in the client generated incoming calls to 'getters' already sent. (for the
3106 now inaccessible leo document commander.)
3107 """
3108 tag = '_ap_to_p'
3109 c = self._check_c()
3110 gnx_d = c.fileCommands.gnxDict
3111 try:
3112 outer_stack = ap.get('stack')
3113 if outer_stack is None: # pragma: no cover.
3114 raise ServerError(f"{tag}: no stack in ap: {ap!r}")
3115 if not isinstance(outer_stack, (list, tuple)): # pragma: no cover.
3116 raise ServerError(f"{tag}: stack must be tuple or list: {outer_stack}")
3117 #
3118 def d_to_childIndex_v (d):
3119 """Helper: return childIndex and v from d ["childIndex"] and d["gnx"]."""
3120 childIndex = d.get('childIndex')
3121 if childIndex is None: # pragma: no cover.
3122 raise ServerError(f"{tag}: no childIndex in {d}")
3123 try:
3124 childIndex = int(childIndex)
3125 except Exception: # pragma: no cover.
3126 raise ServerError(f"{tag}: bad childIndex: {childIndex!r}")
3127 gnx = d.get('gnx')
3128 if gnx is None: # pragma: no cover.
3129 raise ServerError(f"{tag}: no gnx in {d}.")
3130 v = gnx_d.get(gnx)
3131 if v is None: # pragma: no cover.
3132 raise ServerError(f"{tag}: gnx not found: {gnx!r}")
3133 return childIndex, v
3134 #
3135 # Compute p.childIndex and p.v.
3136 childIndex, v = d_to_childIndex_v(ap)
3137 #
3138 # Create p.stack.
3139 stack = []
3140 for stack_d in outer_stack:
3141 stack_childIndex, stack_v = d_to_childIndex_v(stack_d)
3142 stack.append((stack_v, stack_childIndex))
3143 #
3144 # Make p and check p.
3145 p = Position(v, childIndex, stack)
3146 if not c.positionExists(p): # pragma: no cover.
3147 raise ServerError(f"{tag}: p does not exist in {c.shortFileName()}")
3148 except Exception:
3149 if self.log_flag or traces:
3150 print(
3151 f"{tag}: Bad ap: {ap!r}\n"
3152 # f"{tag}: position: {p!r}\n"
3153 f"{tag}: v {v!r} childIndex: {childIndex!r}\n"
3154 f"{tag}: stack: {stack!r}", flush=True)
3155 return False # Return false on any error so caller can react
3156 return p
3157 #@+node:felix.20210621233316.80: *4* server._check_c
3158 def _check_c(self):
3159 """Return self.c or raise ServerError if self.c is None."""
3160 tag = '_check_c'
3161 c = self.c
3162 if not c: # pragma: no cover
3163 raise ServerError(f"{tag}: no open commander")
3164 return c
3165 #@+node:felix.20210621233316.81: *4* server._check_outline
3166 def _check_outline(self, c):
3167 """Check self.c for consistency."""
3168 # Check that all positions exist.
3169 self._check_outline_positions(c)
3170 # Test round-tripping.
3171 self._test_round_trip_positions(c)
3172 #@+node:felix.20210621233316.82: *4* server._check_outline_positions
3173 def _check_outline_positions(self, c):
3174 """Verify that all positions in c exist."""
3175 tag = '_check_outline_positions'
3176 for p in c.all_positions(copy=False):
3177 if not c.positionExists(p): # pragma: no cover
3178 message = f"{tag}: position {p!r} does not exist in {c.shortFileName()}"
3179 print(message, flush=True)
3180 self._dump_position(p)
3181 raise ServerError(message)
3182 #@+node:felix.20210621233316.84: *4* server._do_leo_command_by_name
3183 def _do_leo_command_by_name(self, command_name, param):
3184 """
3185 Generic call to a command in Leo's Commands class or any subcommander class.
3187 The param["ap"] position is to be selected before having the command run,
3188 while the param["keep"] parameter specifies wether the original position
3189 should be re-selected afterward.
3191 TODO: The whole of those operations is to be undoable as one undo step.
3193 command_name: the name of a Leo command (a kebab-cased string).
3194 param["ap"]: an archived position.
3195 param["keep"]: preserve the current selection, if possible.
3197 """
3198 tag = '_do_leo_command_by_name'
3199 c = self._check_c()
3201 if command_name in self.bad_commands_list: # pragma: no cover
3202 raise ServerError(f"{tag}: disallowed command: {command_name!r}")
3204 keepSelection = False # Set default, optional component of param
3205 if "keep" in param:
3206 keepSelection = param["keep"]
3208 func = c.commandsDict.get(command_name) # Getting from kebab-cased 'Command Name'
3209 if not func: # pragma: no cover
3210 raise ServerError(f"{tag}: Leo command not found: {command_name!r}")
3212 p = self._get_p(param)
3213 try:
3214 if p == c.p:
3215 value = func(event={"c":c}) # no need for re-selection
3216 else:
3217 old_p = c.p # preserve old position
3218 c.selectPosition(p) # set position upon which to perform the command
3219 value = func(event={"c":c})
3220 if keepSelection and c.positionExists(old_p):
3221 # Only if 'keep' old position was set, and old_p still exists
3222 c.selectPosition(old_p)
3223 except Exception as e:
3224 print(f"_do_leo_command Recovered from Error {e!s}", flush=True)
3225 return self._make_response() # Return empty on error
3226 #
3227 # Tag along a possible return value with info sent back by _make_response
3228 if self._is_jsonable(value):
3229 return self._make_response({"return-value": value})
3230 return self._make_response()
3231 #@+node:ekr.20210722184932.1: *4* server._do_leo_function_by_name
3232 def _do_leo_function_by_name(self, function_name, param):
3233 """
3234 Generic call to a method in Leo's Commands class or any subcommander class.
3236 The param["ap"] position is to be selected before having the command run,
3237 while the param["keep"] parameter specifies wether the original position
3238 should be re-selected afterward.
3240 TODO: The whole of those operations is to be undoable as one undo step.
3242 command: the name of a method
3243 param["ap"]: an archived position.
3244 param["keep"]: preserve the current selection, if possible.
3246 """
3247 tag = '_do_leo_function_by_name'
3248 c = self._check_c()
3250 keepSelection = False # Set default, optional component of param
3251 if "keep" in param:
3252 keepSelection = param["keep"]
3254 func = self._get_commander_method(function_name) # GET FUNC
3255 if not func: # pragma: no cover
3256 raise ServerError(f"{tag}: Leo command not found: {function_name!r}")
3258 p = self._get_p(param)
3259 try:
3260 if p == c.p:
3261 value = func(event={"c":c}) # no need for re-selection
3262 else:
3263 old_p = c.p # preserve old position
3264 c.selectPosition(p) # set position upon which to perform the command
3265 value = func(event={"c":c})
3266 if keepSelection and c.positionExists(old_p):
3267 # Only if 'keep' old position was set, and old_p still exists
3268 c.selectPosition(old_p)
3269 except Exception as e:
3270 print(f"_do_leo_command Recovered from Error {e!s}", flush=True)
3271 return self._make_response() # Return empty on error
3272 #
3273 # Tag along a possible return value with info sent back by _make_response
3274 if self._is_jsonable(value):
3275 return self._make_response({"return-value": value})
3276 return self._make_response()
3277 #@+node:felix.20210621233316.85: *4* server._do_message
3278 def _do_message(self, d):
3279 """
3280 Handle d, a python dict representing the incoming request.
3281 The d dict must have the three (3) following keys:
3283 "id": A positive integer.
3285 "action": A string, which is either:
3286 - The name of public method of this class, prefixed with '!'.
3287 - The name of a Leo command, prefixed with '-'
3288 - The name of a method of a Leo class, without prefix.
3290 "param": A dict to be passed to the called "action" method.
3291 (Passed to the public method, or the _do_leo_command. Often contains ap, text & keep)
3293 Return a dict, created by _make_response or _make_minimal_response
3294 that contains at least an 'id' key.
3296 """
3297 global traces
3298 tag = '_do_message'
3299 trace, verbose = 'request' in traces, 'verbose' in traces
3301 # Require "id" and "action" keys
3302 id_ = d.get("id")
3303 if id_ is None: # pragma: no cover
3304 raise ServerError(f"{tag}: no id")
3305 action = d.get("action")
3306 if action is None: # pragma: no cover
3307 raise ServerError(f"{tag}: no action")
3309 # TODO : make/force always an object from the client connected.
3310 param = d.get('param', {}) # Can be none or a string
3311 # Set log flag.
3312 if param:
3313 self.log_flag = param.get("log")
3314 pass
3315 else:
3316 param = {}
3318 # Handle traces.
3319 if trace and verbose: # pragma: no cover
3320 g.printObj(d, tag=f"request {id_}")
3321 print('', flush=True)
3322 elif trace: # pragma: no cover
3323 keys = sorted(param.keys())
3324 if action == '!set_config':
3325 keys_s = f"({len(keys)} keys)"
3326 elif len(keys) > 5:
3327 keys_s = '\n ' + '\n '.join(keys)
3328 else:
3329 keys_s = ', '.join(keys)
3330 print(f" request {id_:<4} {action:<30} {keys_s}", flush=True)
3332 # Set the current_id and action ivars for _make_response.
3333 self.current_id = id_
3334 self.action = action
3336 # Execute the requested action.
3337 if action[0] == "!":
3338 action = action[1:] # Remove exclamation point "!"
3339 func = self._do_server_command # Server has this method.
3340 elif action[0] == '-':
3341 action = action[1:] # Remove dash "-"
3342 func = self._do_leo_command_by_name # It's a command name.
3343 else:
3344 func = self._do_leo_function_by_name # It's the name of a method in some commander.
3345 result = func(action, param)
3346 if result is None: # pragma: no cover
3347 raise ServerError(f"{tag}: no response: {action!r}")
3348 return result
3349 #@+node:felix.20210621233316.86: *4* server._do_server_command
3350 def _do_server_command(self, action, param):
3351 tag = '_do_server_command'
3352 # Disallow hidden methods.
3353 if action.startswith('_'): # pragma: no cover
3354 raise ServerError(f"{tag}: action starts with '_': {action!r}")
3355 # Find and execute the server method.
3356 func = getattr(self, action, None)
3357 if not func:
3358 raise ServerError(f"{tag}: action not found: {action!r}") # pragma: no cover
3359 if not callable(func):
3360 raise ServerError(f"{tag}: not callable: {func!r}") # pragma: no cover
3361 return func(param)
3362 #@+node:felix.20210621233316.87: *4* server._dump_*
3363 def _dump_outline(self, c): # pragma: no cover
3364 """Dump the outline."""
3365 tag = '_dump_outline'
3366 print(f"{tag}: {c.shortFileName()}...\n", flush=True)
3367 for p in c.all_positions():
3368 self._dump_position(p)
3369 print('', flush=True)
3371 def _dump_position(self, p): # pragma: no cover
3372 level_s = ' ' * 2 * p.level()
3373 print(f"{level_s}{p.childIndex():2} {p.v.gnx} {p.h}", flush=True)
3374 #@+node:felix.20210624160812.1: *4* server._emit_signon
3375 def _emit_signon(self):
3376 """Simulate the Initial Leo Log Entry"""
3377 tag = 'emit_signon'
3378 if self.loop:
3379 g.app.computeSignon()
3380 signon = []
3381 for z in (g.app.signon, g.app.signon1):
3382 for z2 in z.split('\n'):
3383 signon.append(z2.strip())
3384 g.es("\n".join(signon))
3385 else:
3386 raise ServerError(f"{tag}: no loop ready for emit_signon")
3387 #@+node:felix.20210625230236.1: *4* server._get_commander_method
3388 def _get_commander_method(self, command):
3389 """ Return the given method (p_command) in the Commands class or subcommanders."""
3390 # First, try the commands class.
3391 c = self._check_c()
3392 func = getattr(c, command, None)
3393 if func:
3394 return func
3395 # Otherwise, search all subcommanders for the method.
3396 table = ( # This table comes from c.initObjectIvars.
3397 'abbrevCommands',
3398 'bufferCommands',
3399 'chapterCommands',
3400 'controlCommands',
3401 'convertCommands',
3402 'debugCommands',
3403 'editCommands',
3404 'editFileCommands',
3405 'evalController',
3406 'gotoCommands',
3407 'helpCommands',
3408 'keyHandler',
3409 'keyHandlerCommands',
3410 'killBufferCommands',
3411 'leoCommands',
3412 'leoTestManager',
3413 'macroCommands',
3414 'miniBufferWidget',
3415 'printingController',
3416 'queryReplaceCommands',
3417 'rectangleCommands',
3418 'searchCommands',
3419 'spellCommands',
3420 'vimCommands', # Not likely to be useful.
3421 )
3422 for ivar in table:
3423 subcommander = getattr(c, ivar, None)
3424 if subcommander:
3425 func = getattr(subcommander, command, None)
3426 if func:
3427 return func
3428 return None
3429 #@+node:felix.20210621233316.91: *4* server._get_focus
3430 def _get_focus(self):
3431 """Server helper method to get the focused panel name string"""
3432 tag = '_get_focus'
3433 try:
3434 w = g.app.gui.get_focus()
3435 focus = g.app.gui.widget_name(w)
3436 except Exception as e:
3437 raise ServerError(f"{tag}: exception trying to get the focused widget: {e}")
3438 return focus
3439 #@+node:felix.20210621233316.90: *4* server._get_p
3440 def _get_p(self, param, strict = False):
3441 """
3442 Return _ap_to_p(param["ap"]) or c.p.,
3443 or False if the strict flag is set
3444 """
3445 tag = '_get_ap'
3446 c = self.c
3447 if not c: # pragma: no cover
3448 raise ServerError(f"{tag}: no c")
3450 ap = param.get("ap")
3451 if ap:
3452 p = self._ap_to_p(ap) # Convertion
3453 if p:
3454 if not c.positionExists(p): # pragma: no cover
3455 raise ServerError(f"{tag}: position does not exist. ap: {ap!r}")
3456 return p # Return the position
3457 if strict:
3458 return False
3459 # Fallback to c.p
3460 if not c.p: # pragma: no cover
3461 raise ServerError(f"{tag}: no c.p")
3463 return c.p
3464 #@+node:felix.20210621233316.92: *4* server._get_position_d
3465 def _get_position_d(self, p):
3466 """
3467 Return a python dict that is adding
3468 graphical representation data and flags
3469 to the base 'ap' dict from _p_to_ap.
3470 (To be used by the connected client GUI.)
3471 """
3472 d = self._p_to_ap(p)
3473 d['headline'] = p.h
3474 d['level'] = p.level()
3475 if p.v.u:
3476 if g.leoServer.leoServerConfig and g.leoServer.leoServerConfig.get("uAsBoolean", False):
3477 # uAsBoolean is 'thruthy'
3478 d['u'] = True
3479 else:
3480 # Normal output if no options set
3481 d['u'] = p.v.u
3482 if bool(p.b):
3483 d['hasBody'] = True
3484 if p.hasChildren():
3485 d['hasChildren'] = True
3486 if p.isCloned():
3487 d['cloned'] = True
3488 if p.isDirty():
3489 d['dirty'] = True
3490 if p.isExpanded():
3491 d['expanded'] = True
3492 if p.isMarked():
3493 d['marked'] = True
3494 if p.isAnyAtFileNode():
3495 d['atFile'] = True
3496 if p == self.c.p:
3497 d['selected'] = True
3498 return d
3499 #@+node:felix.20210705211625.1: *4* server._is_jsonable
3500 def _is_jsonable(self, x):
3501 try:
3502 json.dumps(x, cls=SetEncoder)
3503 return True
3504 except (TypeError, OverflowError):
3505 return False
3506 #@+node:felix.20210621233316.94: *4* server._make_minimal_response
3507 def _make_minimal_response(self, package=None):
3508 """
3509 Return a json string representing a response dict.
3511 The 'package' kwarg, if present, must be a python dict describing a
3512 response. package may be an empty dict or None.
3514 The 'p' kwarg, if present, must be a position.
3516 First, this method creates a response (a python dict) containing all
3517 the keys in the 'package' dict.
3519 Then it adds 'id' to the package.
3521 Finally, this method returns the json string corresponding to the
3522 response.
3523 """
3524 if package is None:
3525 package = {}
3527 # Always add id.
3528 package ["id"] = self.current_id
3530 return json.dumps(package, separators=(',', ':'), cls=SetEncoder)
3531 #@+node:felix.20210621233316.93: *4* server._make_response
3532 def _make_response(self, package=None):
3533 """
3534 Return a json string representing a response dict.
3536 The 'package' kwarg, if present, must be a python dict describing a
3537 response. package may be an empty dict or None.
3539 The 'p' kwarg, if present, must be a position.
3541 First, this method creates a response (a python dict) containing all
3542 the keys in the 'package' dict, with the following added keys:
3544 - "id": The incoming id.
3545 - "commander": A dict describing self.c.
3546 - "node": None, or an archived position describing self.c.p.
3548 Finally, this method returns the json string corresponding to the
3549 response.
3550 """
3551 global traces
3552 tag = '_make_response'
3553 trace = self.log_flag or 'response' in traces
3554 verbose = 'verbose' in traces
3555 c = self.c # It is valid for c to be None.
3556 if package is None:
3557 package = {}
3558 p = package.get("p")
3559 if p:
3560 del package ["p"]
3561 # Raise an *internal* error if checks fail.
3562 if isinstance(package, str): # pragma: no cover
3563 raise InternalServerError(f"{tag}: bad package kwarg: {package!r}")
3564 if p and not isinstance(p, Position): # pragma: no cover
3565 raise InternalServerError(f"{tag}: bad p kwarg: {p!r}")
3566 if p and not c: # pragma: no cover
3567 raise InternalServerError(f"{tag}: p but not c")
3568 if p and not c.positionExists(p): # pragma: no cover
3569 raise InternalServerError(f"{tag}: p does not exist: {p!r}")
3570 if c and not c.p: # pragma: no cover
3571 raise InternalServerError(f"{tag}: empty c.p")
3573 # Always add id
3574 package ["id"] = self.current_id
3576 # The following keys are relevant only if there is an open commander.
3577 if c:
3578 # Allow commands, especially _get_redraw_d, to specify p!
3579 p = p or c.p
3580 package ["commander"] = {
3581 "changed": c.isChanged(),
3582 "fileName": c.fileName(), # Can be None for new files.
3583 }
3584 # Add all the node data, including:
3585 # - "node": self._p_to_ap(p) # Contains p.gnx, p.childIndex and p.stack.
3586 # - All the *cheap* redraw data for p.
3587 redraw_d = self._get_position_d(p)
3588 package ["node"] = redraw_d
3590 # Handle traces.
3591 if trace and verbose: # pragma: no cover
3592 g.printObj(package, tag=f"response {self.current_id}")
3593 print('', flush=True)
3594 elif trace: # pragma: no cover
3595 keys = sorted(package.keys())
3596 keys_s = ', '.join(keys)
3597 print(f"response {self.current_id:<4} {keys_s}", flush=True)
3599 return json.dumps(package, separators=(',', ':'), cls=SetEncoder)
3600 #@+node:felix.20210621233316.95: *4* server._p_to_ap
3601 def _p_to_ap(self, p):
3602 """
3603 * From Leo plugin leoflexx.py *
3605 Convert Leo position p to a serializable archived position.
3607 This returns only position-related data.
3608 get_position_data returns all data needed to redraw the screen.
3609 """
3610 self._check_c()
3611 stack = [{'gnx': v.gnx, 'childIndex': childIndex}
3612 for (v, childIndex) in p.stack]
3613 return {
3614 'childIndex': p._childIndex,
3615 'gnx': p.v.gnx,
3616 'stack': stack,
3617 }
3618 #@+node:felix.20210621233316.96: *4* server._positionFromGnx
3619 def _positionFromGnx(self, gnx):
3620 """Return first p node with this gnx or false"""
3621 c = self._check_c()
3622 for p in c.all_unique_positions():
3623 if p.v.gnx == gnx:
3624 return p
3625 return False
3626 #@+node:felix.20210622232409.1: *4* server._send_async_output & helper
3627 def _send_async_output(self, package, toAll = False):
3628 """
3629 Send data asynchronously to the client
3630 """
3631 tag = "send async output"
3632 jsonPackage = json.dumps(package, separators=(',', ':'), cls=SetEncoder)
3633 if "async" not in package:
3634 InternalServerError(f"\n{tag}: async member missing in package {jsonPackage} \n")
3635 if self.loop:
3636 self.loop.create_task(self._async_output(jsonPackage, toAll))
3637 else:
3638 InternalServerError(f"\n{tag}: loop not ready {jsonPackage} \n")
3639 #@+node:felix.20210621233316.89: *5* server._async_output
3640 async def _async_output(self, json, toAll = False): # pragma: no cover (tested in server)
3641 """Output json string to the web_socket"""
3642 global connectionsTotal
3643 tag = '_async_output'
3644 outputBytes = bytes(json, 'utf-8')
3645 if toAll:
3646 if connectionsPool: # asyncio.wait doesn't accept an empty list
3647 await asyncio.wait([asyncio.create_task(client.send(outputBytes)) for client in connectionsPool])
3648 else:
3649 g.trace(f"{tag}: no web socket. json: {json!r}")
3650 else:
3651 if self.web_socket:
3652 await self.web_socket.send(outputBytes)
3653 else:
3654 g.trace(f"{tag}: no web socket. json: {json!r}")
3655 #@+node:felix.20210621233316.97: *4* server._test_round_trip_positions
3656 def _test_round_trip_positions(self, c): # pragma: no cover (tested in client).
3657 """Test the round tripping of p_to_ap and ap_to_p."""
3658 tag = '_test_round_trip_positions'
3659 for p in c.all_unique_positions():
3660 ap = self._p_to_ap(p)
3661 p2 = self._ap_to_p(ap)
3662 if p != p2:
3663 self._dump_outline(c)
3664 raise ServerError(f"{tag}: round-trip failed: ap: {ap!r}, p: {p!r}, p2: {p2!r}")
3665 #@+node:felix.20210625002950.1: *4* server._yieldAllRootChildren
3666 def _yieldAllRootChildren(self):
3667 """Return all root children P nodes"""
3668 c = self._check_c()
3669 p = c.rootPosition()
3670 while p:
3671 yield p
3672 p.moveToNext()
3674 #@-others
3675#@+node:felix.20210621233316.105: ** function: main & helpers
3676def main(): # pragma: no cover (tested in client)
3677 """python script for leo integration via leoBridge"""
3678 global websockets
3679 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile
3680 if not websockets:
3681 print('websockets not found')
3682 print('pip install websockets')
3683 return
3685 #@+others
3686 #@+node:felix.20210807214524.1: *3* function: cancel_tasks
3687 def cancel_tasks(to_cancel, loop):
3688 if not to_cancel:
3689 return
3691 for task in to_cancel:
3692 task.cancel()
3694 loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
3696 for task in to_cancel:
3697 if task.cancelled():
3698 continue
3699 if task.exception() is not None:
3700 loop.call_exception_handler(
3701 {
3702 "message": "unhandled exception during asyncio.run() shutdown",
3703 "exception": task.exception(),
3704 "task": task,
3705 }
3706 )
3707 #@+node:ekr.20210825115746.1: *3* function: center_tk_frame
3708 def center_tk_frame(top):
3709 """Center the top-level Frame."""
3710 # https://stackoverflow.com/questions/3352918
3711 top.update_idletasks()
3712 screen_width = top.winfo_screenwidth()
3713 screen_height = top.winfo_screenheight()
3714 size = tuple(int(_) for _ in top.geometry().split('+')[0].split('x'))
3715 x = screen_width/2 - size[0]/2
3716 y = screen_height/2 - size[1]/2
3717 top.geometry("+%d+%d" % (x, y))
3718 #@+node:felix.20210804130751.1: *3* function: close_server
3719 def close_Server():
3720 """
3721 Close the server by stopping the loop
3722 """
3723 print('Closing Leo Server', flush=True)
3724 if loop.is_running():
3725 loop.stop()
3726 else:
3727 print('Loop was not running', flush=True)
3728 #@+node:ekr.20210825172913.1: *3* function: general_yes_no_dialog & helpers
3729 def general_yes_no_dialog(
3730 c,
3731 title, # Not used.
3732 message=None, # Must exist.
3733 yesMessage="&Yes", # Not used.
3734 noMessage="&No", # Not used.
3735 yesToAllMessage=None, # Not used.
3736 defaultButton="Yes", # Not used
3737 cancelMessage=None, # Not used.
3738 ):
3739 """
3740 Monkey-patched implementation of LeoQtGui.runAskYesNoCancelDialog
3741 offering *only* Yes/No buttons.
3743 This will fallback to a tk implementation if the qt library is unavailable.
3745 This raises a dialog and return either 'yes' or 'no'.
3746 """
3747 #@+others # define all helper functions.
3748 #@+node:ekr.20210801175921.1: *4* function: tk_runAskYesNoCancelDialog & helpers
3749 def tk_runAskYesNoCancelDialog(c):
3750 """
3751 Tk version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons.
3752 """
3753 if g.unitTesting:
3754 return None
3755 root = top = val = None # Non-locals
3756 #@+others # define helper functions
3757 #@+node:ekr.20210801180311.4: *5* function: create_yes_no_frame
3758 def create_yes_no_frame(message, top):
3759 """Create the dialog's frame."""
3760 frame = Tk.Frame(top)
3761 frame.pack(side="top", expand=1, fill="both")
3762 label = Tk.Label(frame, text=message, bg="white")
3763 label.pack(pady=10)
3764 # Create buttons.
3765 f = Tk.Frame(top)
3766 f.pack(side="top", padx=30)
3767 b = Tk.Button(f, width=6, text="Yes", bd=4, underline=0, command=yesButton)
3768 b.pack(side="left", padx=5, pady=10)
3769 b = Tk.Button(f, width=6, text="No", bd=2, underline=0, command=noButton)
3770 b.pack(side="left", padx=5, pady=10)
3771 #@+node:ekr.20210801180311.5: *5* function: callbacks
3772 def noButton(event=None):
3773 """Do default click action in ok button."""
3774 nonlocal val
3775 print(f"Not saved: {c.fileName()}")
3776 val = "no"
3777 top.destroy()
3779 def yesButton(event=None):
3780 """Do default click action in ok button."""
3781 nonlocal val
3782 print(f"Saved: {c.fileName()}")
3783 val = "yes"
3784 top.destroy()
3785 #@-others
3786 root = Tk.Tk()
3787 root.withdraw()
3788 root.update()
3790 top = Tk.Toplevel(root)
3791 top.title("Saved changed outline?")
3792 create_yes_no_frame(message, top)
3793 top.bind("<Return>", yesButton)
3794 top.bind("y", yesButton)
3795 top.bind("Y", yesButton)
3796 top.bind("n", noButton)
3797 top.bind("N", noButton)
3798 top.lift()
3800 center_tk_frame(top)
3802 top.grab_set() # Make the dialog a modal dialog.
3804 root.update()
3805 root.wait_window(top)
3807 top.destroy()
3808 root.destroy()
3809 return val
3810 #@+node:ekr.20210825170952.1: *4* function: qt_runAskYesNoCancelDialog
3811 def qt_runAskYesNoCancelDialog(c):
3812 """
3813 Qt version of LeoQtGui.runAskYesNoCancelDialog, with *only* Yes/No buttons.
3814 """
3815 if g.unitTesting:
3816 return None
3817 dialog = QtWidgets.QMessageBox(None)
3818 dialog.setIcon(Information.Warning)
3819 dialog.setWindowTitle("Saved changed outline?")
3820 if message:
3821 dialog.setText(message)
3822 # Creation order determines returned value.
3823 yes = dialog.addButton(yesMessage, ButtonRole.YesRole)
3824 dialog.addButton(noMessage, ButtonRole.NoRole)
3825 dialog.setDefaultButton(yes)
3826 # Set the Leo icon.
3827 core_dir = os.path.dirname(__file__)
3828 icon_path = os.path.join(core_dir, "..", "Icons", "leoApp.ico")
3829 if os.path.exists(icon_path):
3830 pixmap = QtGui.QPixmap()
3831 pixmap.load(icon_path)
3832 icon = QtGui.QIcon(pixmap)
3833 dialog.setWindowIcon(icon)
3834 # None of these grabs focus from the console window.
3835 dialog.raise_()
3836 dialog.setFocus()
3837 app.processEvents() # type:ignore
3838 # val is the same as the creation order.
3839 # Tested with both Qt6 and Qt5.
3840 val = dialog.exec() if isQt6 else dialog.exec_()
3841 if val == 0:
3842 print(f"Saved: {c.fileName()}")
3843 return 'yes'
3844 print(f"Not saved: {c.fileName()}")
3845 return 'no'
3846 #@-others
3847 try:
3848 # Careful: raise the Tk dialog if there are errors in the Qt code.
3849 if 1: # Prefer Qt.
3850 from leo.core.leoQt import isQt6, QtGui, QtWidgets
3851 from leo.core.leoQt import ButtonRole, Information
3852 if QtGui and QtWidgets:
3853 app = QtWidgets.QApplication([])
3854 assert app
3855 val = qt_runAskYesNoCancelDialog(c)
3856 assert val in ('yes', 'no')
3857 return val
3858 except Exception:
3859 pass
3860 return tk_runAskYesNoCancelDialog(c)
3861 #@+node:felix.20210621233316.107: *3* function: get_args
3862 def get_args(): # pragma: no cover
3863 """
3864 Get arguments from the command line and sets them globally.
3865 """
3866 global wsHost, wsPort, wsLimit, wsPersist, wsSkipDirty, argFile, traces
3868 def leo_file(s):
3869 if os.path.exists(s):
3870 return s
3871 print(f"\nNot a .leo file: {s!r}")
3872 sys.exit(1)
3874 description = ''.join([
3875 " leoserver.py\n",
3876 " ------------\n",
3877 " Offers single or multiple concurrent websockets\n",
3878 " for JSON based remote-procedure-calls\n",
3879 " to a shared instance of leo.core.leoBridge\n",
3880 " \n",
3881 " Clients may be written in any language:\n",
3882 " - leo.core.leoclient is an example client written in python.\n",
3883 " - leoInteg (https://github.com/boltex/leointeg) is written in typescript.\n"
3884 ])
3885 # usage = 'leoserver.py [-a <address>] [-p <port>] [-l <limit>] [-f <file>] [--dirty] [--persist]'
3886 usage = 'python leo.core.leoserver [options...]'
3887 trace_s = 'request,response,verbose'
3888 valid_traces = [z.strip() for z in trace_s.split(',')]
3889 parser = argparse.ArgumentParser(description=description, usage=usage,
3890 formatter_class=argparse.RawTextHelpFormatter)
3891 add = parser.add_argument
3892 add('-a', '--address', dest='wsHost', type=str, default=wsHost, metavar='STR',
3893 help='server address. Defaults to ' + str(wsHost))
3894 add('-p', '--port', dest='wsPort', type=int, default=wsPort, metavar='N',
3895 help='port number. Defaults to ' + str(wsPort))
3896 add('-l', '--limit', dest='wsLimit', type=int, default=wsLimit, metavar='N',
3897 help='maximum number of clients. Defaults to '+ str(wsLimit))
3898 add('-f', '--file', dest='argFile', type=leo_file, metavar='PATH',
3899 help='open a .leo file at startup')
3900 add('--persist', dest='wsPersist', action='store_true',
3901 help='do not quit when last client disconnects')
3902 add('-d', '--dirty', dest='wsSkipDirty', action='store_true',
3903 help='do not warn about dirty files when quitting')
3904 add('--trace', dest='traces', type=str, metavar='STRINGS',
3905 help=f"comma-separated list of {trace_s}")
3906 add('-v', '--version', dest='v', action='store_true',
3907 help='show version and exit')
3908 # Parse
3909 args = parser.parse_args()
3910 # Handle the args and set them up globally
3911 wsHost = args.wsHost
3912 wsPort = args.wsPort
3913 wsLimit = args.wsLimit
3914 wsPersist = bool(args.wsPersist)
3915 wsSkipDirty = bool(args.wsSkipDirty)
3916 argFile = args.argFile
3917 if args.traces:
3918 ok = True
3919 for z in args.traces.split(','):
3920 if z in valid_traces:
3921 traces.append(z)
3922 else:
3923 ok = False
3924 print(f"Ignoring invalid --trace value: {z!r}", flush=True)
3925 if not ok:
3926 print(f"Valid traces are: {','.join(valid_traces)}", flush=True)
3927 print(f"--trace={','.join(traces)}", flush=True)
3928 if args.v:
3929 print(__version__)
3930 sys.exit(0)
3931 # Sanitize limit.
3932 if wsLimit < 1:
3933 wsLimit = 1
3934 #@+node:felix.20210803174312.1: *3* function: notify_clients
3935 async def notify_clients(action, excludedConn = None):
3936 global connectionsTotal
3937 if connectionsPool: # asyncio.wait doesn't accept an empty list
3938 opened = bool(controller.c) # c can be none if no files opened
3939 m = json.dumps({
3940 "async": "refresh",
3941 "action": action,
3942 "opened": opened,
3943 }, separators=(',', ':'), cls=SetEncoder)
3944 clientSetCopy = connectionsPool.copy()
3945 if excludedConn:
3946 clientSetCopy.discard(excludedConn)
3947 if clientSetCopy:
3948 # if still at least one to notify
3949 await asyncio.wait([asyncio.create_task(client.send(m)) for client in clientSetCopy])
3951 #@+node:felix.20210803174312.2: *3* function: register_client
3952 async def register_client(websocket):
3953 global connectionsTotal
3954 connectionsPool.add(websocket)
3955 await notify_clients("unregister", websocket)
3956 #@+node:felix.20210807160828.1: *3* function: save_dirty
3957 def save_dirty():
3958 """
3959 Ask the user about dirty files if any remained opened.
3960 """
3961 # Monkey-patch the dialog method first.
3962 g.app.gui.runAskYesNoCancelDialog = general_yes_no_dialog
3963 # Loop all commanders and 'close' them for dirty check
3964 commanders = g.app.commanders()
3965 for commander in commanders:
3966 if commander.isChanged() and commander.fileName():
3967 commander.close() # Patched 'ask' methods will open dialog
3968 #@+node:felix.20210803174312.3: *3* function: unregister_client
3969 async def unregister_client(websocket):
3970 global connectionsTotal
3971 connectionsPool.remove(websocket)
3972 await notify_clients("unregister")
3973 #@+node:felix.20210621233316.106: *3* function: ws_handler (server)
3974 async def ws_handler(websocket, path):
3975 """
3976 The web socket handler: server.ws_server.
3978 It must be a coroutine accepting two arguments: a WebSocketServerProtocol and the request URI.
3979 """
3980 global connectionsTotal, wsLimit
3981 tag = 'server'
3982 trace = False
3983 verbose = False
3984 connected = False
3986 try:
3987 # Websocket connection startup
3988 if connectionsTotal >= wsLimit:
3989 print(f"{tag}: User Refused, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True)
3990 await websocket.close(1001)
3991 return
3992 connected = True # local variable
3993 connectionsTotal += 1 # global variable
3994 print(f"{tag}: User Connected, Total: {connectionsTotal}, Limit: {wsLimit}", flush=True)
3995 # If first connection set it as the main client connection
3996 controller._init_connection(websocket)
3997 await register_client(websocket)
3998 # Start by sending empty as 'ok'.
3999 n = 0
4000 await websocket.send(controller._make_response())
4001 controller._emit_signon()
4003 # Websocket connection message handling loop
4004 async for json_message in websocket:
4005 try:
4006 n += 1
4007 d = None
4008 d = json.loads(json_message)
4009 if trace and verbose:
4010 print(f"{tag}: got: {d}", flush=True)
4011 elif trace:
4012 print(f"{tag}: got: {d}", flush=True)
4013 answer = controller._do_message(d)
4014 except TerminateServer as e:
4015 # pylint: disable=no-value-for-parameter,unexpected-keyword-arg
4016 raise websockets.exceptions.ConnectionClosed(code=1000, reason=e)
4017 except ServerError as e:
4018 data = f"{d}" if d else f"json syntax error: {json_message!r}"
4019 error = f"{tag}: ServerError: {e}...\n{tag}: {data}"
4020 print("", flush=True)
4021 print(error, flush=True)
4022 print("", flush=True)
4023 package = {
4024 "id": controller.current_id,
4025 "action": controller.action,
4026 "request": data,
4027 "ServerError": f"{e}",
4028 }
4029 answer = json.dumps(package, separators=(',', ':'), cls=SetEncoder)
4030 except InternalServerError as e: # pragma: no cover
4031 print(f"{tag}: InternalServerError {e}", flush=True)
4032 break
4033 except Exception as e: # pragma: no cover
4034 print(f"{tag}: Unexpected Exception! {e}", flush=True)
4035 g.print_exception()
4036 print('', flush=True)
4037 break
4038 await websocket.send(answer)
4040 # If not a 'getter' send refresh signal to other clients
4041 if controller.action[0:5] != "!get_" and controller.action != "!do_nothing":
4042 await notify_clients(controller.action, websocket)
4044 except websockets.exceptions.ConnectionClosedError as e: # pragma: no cover
4045 print(f"{tag}: connection closed error: {e}")
4046 except websockets.exceptions.ConnectionClosed as e:
4047 print(f"{tag}: connection closed: {e}")
4048 finally:
4049 if connected:
4050 connectionsTotal -= 1
4051 await unregister_client(websocket)
4052 print(f"{tag} connection finished. Total: {connectionsTotal}, Limit: {wsLimit}")
4053 # Check for persistence flag if all connections are closed
4054 if connectionsTotal == 0 and not wsPersist:
4055 print("Shutting down leoserver")
4056 # Preemptive closing of tasks
4057 for task in asyncio.all_tasks():
4058 task.cancel()
4059 close_Server() # Stops the run_forever loop
4060 #@-others
4062 # Make the first real line of output more visible.
4063 print("", flush=True)
4065 # Sets sHost, wsPort, wsLimit, wsPersist, wsSkipDirty fileArg and traces
4066 get_args() # Set global values from the command line arguments
4067 print("Starting LeoBridge... (Launch with -h for help)", flush=True)
4069 # Open leoBridge.
4070 controller = LeoServer() # Single instance of LeoServer, i.e., an instance of leoBridge
4071 if argFile:
4072 # Open specified file argument
4073 try:
4074 print(f"Opening file: {argFile}", flush=True)
4075 controller.open_file({"filename":argFile})
4076 except Exception:
4077 print("Opening file failed", flush=True)
4079 # Start the server.
4080 loop = asyncio.get_event_loop()
4082 try:
4083 try:
4084 server = websockets.serve(ws_handler, wsHost, wsPort, max_size=None) # pylint: disable=no-member
4085 realtime_server = loop.run_until_complete(server)
4086 except OSError as e:
4087 print(e)
4088 print("Trying with IPv4 Family", flush=True)
4089 server = websockets.serve(ws_handler, wsHost, wsPort, family=socket.AF_INET, max_size=None) # pylint: disable=no-member
4090 realtime_server = loop.run_until_complete(server)
4092 signon = SERVER_STARTED_TOKEN + f" at {wsHost} on port: {wsPort}.\n"
4093 if wsPersist:
4094 signon = signon + "Persistent server\n"
4095 if wsSkipDirty:
4096 signon = signon + "No prompt about dirty file(s) when closing server\n"
4097 if wsLimit > 1:
4098 signon = signon + f"Total client limit is {wsLimit}.\n"
4099 signon = signon + "Ctrl+c to break"
4100 print(signon, flush=True)
4101 loop.run_forever()
4103 except KeyboardInterrupt:
4104 print("Process interrupted", flush=True)
4106 finally:
4107 # Execution continues here after server is interupted (e.g. with ctrl+c)
4108 realtime_server.close()
4109 if not wsSkipDirty:
4110 print("Checking for changed commanders...", flush=True)
4111 save_dirty()
4112 cancel_tasks(asyncio.all_tasks(loop), loop)
4113 loop.run_until_complete(loop.shutdown_asyncgens())
4114 loop.close()
4115 asyncio.set_event_loop(None)
4116 print("Stopped leobridge server", flush=True)
4117#@-others
4118if __name__ == '__main__':
4119 # pytest will *not* execute this code.
4120 main()
4121#@-leo