suplemon-editor 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- suplemon/__init__.py +0 -0
- suplemon/__main__.py +3 -0
- suplemon/cli.py +65 -0
- suplemon/config/defaults.json +153 -0
- suplemon/config/keymap.json +63 -0
- suplemon/config.py +324 -0
- suplemon/cursor.py +135 -0
- suplemon/editor.py +568 -0
- suplemon/file.py +193 -0
- suplemon/help.py +327 -0
- suplemon/helpers.py +104 -0
- suplemon/hex2xterm.py +23 -0
- suplemon/key_mappings.py +239 -0
- suplemon/lexer.py +88 -0
- suplemon/line.py +51 -0
- suplemon/linelight/__init__.py +0 -0
- suplemon/linelight/color_map.py +9 -0
- suplemon/linelight/css.py +21 -0
- suplemon/linelight/diff.py +17 -0
- suplemon/linelight/html.py +19 -0
- suplemon/linelight/js.py +21 -0
- suplemon/linelight/json.py +15 -0
- suplemon/linelight/lua.py +10 -0
- suplemon/linelight/md.py +19 -0
- suplemon/linelight/php.py +27 -0
- suplemon/linelight/py.py +27 -0
- suplemon/logger.py +90 -0
- suplemon/main.py +771 -0
- suplemon/module_loader.py +98 -0
- suplemon/modules/application_state.py +84 -0
- suplemon/modules/autocomplete.py +114 -0
- suplemon/modules/autodocstring.py +142 -0
- suplemon/modules/battery.py +118 -0
- suplemon/modules/bulk_delete.py +75 -0
- suplemon/modules/clock.py +22 -0
- suplemon/modules/comment.py +59 -0
- suplemon/modules/config.py +20 -0
- suplemon/modules/date.py +22 -0
- suplemon/modules/diff.py +42 -0
- suplemon/modules/eval.py +43 -0
- suplemon/modules/hostname.py +34 -0
- suplemon/modules/input_test.py +23 -0
- suplemon/modules/keymap.py +20 -0
- suplemon/modules/linter.py +274 -0
- suplemon/modules/lower.py +21 -0
- suplemon/modules/lstrip.py +20 -0
- suplemon/modules/paste.py +49 -0
- suplemon/modules/reload.py +16 -0
- suplemon/modules/replace_all.py +23 -0
- suplemon/modules/reverse.py +22 -0
- suplemon/modules/rstrip.py +19 -0
- suplemon/modules/save.py +16 -0
- suplemon/modules/save_all.py +19 -0
- suplemon/modules/sort_lines.py +41 -0
- suplemon/modules/strip.py +19 -0
- suplemon/modules/system_clipboard.py +121 -0
- suplemon/modules/tabstospaces.py +18 -0
- suplemon/modules/toggle_whitespace.py +18 -0
- suplemon/modules/upper.py +21 -0
- suplemon/prompt.py +319 -0
- suplemon/suplemon_module.py +210 -0
- suplemon/themes/8colors.tmTheme +179 -0
- suplemon/themes/monokai.tmTheme +284 -0
- suplemon/themes.py +201 -0
- suplemon/ui.py +656 -0
- suplemon/viewer.py +1004 -0
- suplemon_editor-0.3.0.dist-info/METADATA +515 -0
- suplemon_editor-0.3.0.dist-info/RECORD +72 -0
- suplemon_editor-0.3.0.dist-info/WHEEL +5 -0
- suplemon_editor-0.3.0.dist-info/entry_points.txt +3 -0
- suplemon_editor-0.3.0.dist-info/licenses/LICENSE +21 -0
- suplemon_editor-0.3.0.dist-info/top_level.txt +1 -0
suplemon/main.py
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
The main class that starts and runs Suplemon.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from . import ui
|
|
12
|
+
from . import module_loader
|
|
13
|
+
from . import themes
|
|
14
|
+
from . import helpers
|
|
15
|
+
|
|
16
|
+
from .file import File
|
|
17
|
+
from .logger import logger
|
|
18
|
+
from .config import Config
|
|
19
|
+
from .editor import Editor
|
|
20
|
+
|
|
21
|
+
__version__ = "0.3.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class App:
|
|
25
|
+
def __init__(self, filenames=None, config_file=None, log_level=None):
|
|
26
|
+
"""
|
|
27
|
+
Handle App initialization
|
|
28
|
+
|
|
29
|
+
:param list filenames: Names of files to load initially
|
|
30
|
+
:param str filenames[*]: Path to a file to load
|
|
31
|
+
"""
|
|
32
|
+
self.version = __version__
|
|
33
|
+
self.inited = False
|
|
34
|
+
self.running = False
|
|
35
|
+
self.debug = True
|
|
36
|
+
self.log_level = log_level
|
|
37
|
+
self.block_rendering = False
|
|
38
|
+
|
|
39
|
+
# Set default variables
|
|
40
|
+
self.path = os.path.dirname(os.path.realpath(__file__))
|
|
41
|
+
self.files = []
|
|
42
|
+
self.current_file = 0
|
|
43
|
+
self.status_msg = ""
|
|
44
|
+
self.last_input = None
|
|
45
|
+
self.global_buffer = []
|
|
46
|
+
self.event_bindings = {}
|
|
47
|
+
|
|
48
|
+
self.config = None
|
|
49
|
+
self.ui = None
|
|
50
|
+
self.modules = None
|
|
51
|
+
self.themes = None
|
|
52
|
+
|
|
53
|
+
# Maximum amount of inputs to process at once
|
|
54
|
+
self.max_input = 100
|
|
55
|
+
|
|
56
|
+
# Save filenames for later
|
|
57
|
+
self.filenames = filenames
|
|
58
|
+
|
|
59
|
+
# Save config file path for later
|
|
60
|
+
self.config_file = config_file
|
|
61
|
+
|
|
62
|
+
# Define core operations
|
|
63
|
+
self.operations = {
|
|
64
|
+
"help": self.help,
|
|
65
|
+
"save_file": self.save_file,
|
|
66
|
+
"run_command": self.query_command,
|
|
67
|
+
"go_to": self.go_to,
|
|
68
|
+
"open": self.open,
|
|
69
|
+
"close_file": self.close_file,
|
|
70
|
+
"new_file": self.new_file,
|
|
71
|
+
"exit": self.ask_exit,
|
|
72
|
+
"ask_exit": self.ask_exit,
|
|
73
|
+
"prev_file": self.prev_file,
|
|
74
|
+
"next_file": self.next_file,
|
|
75
|
+
"save_file_as": self.save_file_as,
|
|
76
|
+
"reload_file": self.reload_file,
|
|
77
|
+
"toggle_mouse": self.toggle_mouse,
|
|
78
|
+
"toggle_fullscreen": self.toggle_fullscreen,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Bind our logger
|
|
82
|
+
self.logger = logger
|
|
83
|
+
self.logger.debug("Starting Suplemon...")
|
|
84
|
+
|
|
85
|
+
def init(self):
|
|
86
|
+
"""Initialize the app."""
|
|
87
|
+
# Load core components
|
|
88
|
+
self.config = Config(self)
|
|
89
|
+
if self.config_file:
|
|
90
|
+
self.config.set_path(self.config_file)
|
|
91
|
+
if not self.config.init():
|
|
92
|
+
# Can't run without config
|
|
93
|
+
return False
|
|
94
|
+
self.config.load()
|
|
95
|
+
|
|
96
|
+
# Configure logger
|
|
97
|
+
self.debug = self.config["app"]["debug"]
|
|
98
|
+
debug_level = self.config["app"]["debug_level"]
|
|
99
|
+
if self.log_level is not None:
|
|
100
|
+
debug_level = self.log_level
|
|
101
|
+
self.logger.debug("Setting debug_level to {0}.".format(debug_level))
|
|
102
|
+
self.logger.setLevel(debug_level)
|
|
103
|
+
[handler.setLevel(debug_level) for handler in self.logger.handlers]
|
|
104
|
+
|
|
105
|
+
# Load user interface
|
|
106
|
+
self.ui = ui.UI(self)
|
|
107
|
+
self.ui.init()
|
|
108
|
+
|
|
109
|
+
# Load extension modules
|
|
110
|
+
self.modules = module_loader.ModuleLoader(self)
|
|
111
|
+
self.modules.load()
|
|
112
|
+
|
|
113
|
+
# Load default module configs
|
|
114
|
+
self.config.load_module_configs()
|
|
115
|
+
|
|
116
|
+
# Load themes
|
|
117
|
+
self.themes = themes.ThemeLoader(self)
|
|
118
|
+
|
|
119
|
+
# Indicate that initialization is complete
|
|
120
|
+
self.inited = True
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def exit(self):
|
|
125
|
+
"""Stop the main loop and exit."""
|
|
126
|
+
self.trigger_event_before("app_exit")
|
|
127
|
+
self.running = False
|
|
128
|
+
|
|
129
|
+
def run(self):
|
|
130
|
+
"""Run the app via the ui wrapper."""
|
|
131
|
+
self.ui.run(self.run_wrapped)
|
|
132
|
+
|
|
133
|
+
def run_wrapped(self, *args):
|
|
134
|
+
"""Actually run the app and start mainloop.
|
|
135
|
+
|
|
136
|
+
This shouldn't be called directly. Instead it's passed to the UI wich
|
|
137
|
+
calls it in a safe curses wrapper.
|
|
138
|
+
|
|
139
|
+
:param *args: Not used. Takes any args the wrapper might pass in.
|
|
140
|
+
"""
|
|
141
|
+
# Load ui and files etc
|
|
142
|
+
self.load()
|
|
143
|
+
# Initial render
|
|
144
|
+
self.get_editor().refresh()
|
|
145
|
+
self.ui.refresh()
|
|
146
|
+
# Start mainloop
|
|
147
|
+
self.main_loop()
|
|
148
|
+
self.trigger_event_after("app_exit")
|
|
149
|
+
# Unload ui
|
|
150
|
+
self.ui.unload()
|
|
151
|
+
|
|
152
|
+
def load(self):
|
|
153
|
+
"""Load the app.
|
|
154
|
+
|
|
155
|
+
Load the UI, open files in self.filenames and finally trigger
|
|
156
|
+
the 'app_loaded' event.
|
|
157
|
+
"""
|
|
158
|
+
self.ui.load()
|
|
159
|
+
if sys.version_info < (3, 8):
|
|
160
|
+
ver = ".".join(map(str, sys.version_info[0:2]))
|
|
161
|
+
self.logger.warning("Running Suplemon with Python {version} "
|
|
162
|
+
"isn't officially supported. Please use "
|
|
163
|
+
"Python 3.8 or higher."
|
|
164
|
+
.format(version=ver))
|
|
165
|
+
self.load_files()
|
|
166
|
+
self.running = True
|
|
167
|
+
self.trigger_event_after("app_loaded")
|
|
168
|
+
|
|
169
|
+
def on_input(self, event):
|
|
170
|
+
# Handle the input or give it to the editor
|
|
171
|
+
if not self.handle_input(event):
|
|
172
|
+
# Pass the input to the editor component
|
|
173
|
+
self.get_editor().handle_input(event)
|
|
174
|
+
|
|
175
|
+
def main_loop(self):
|
|
176
|
+
"""Run the terminal IO loop until exit() is called."""
|
|
177
|
+
while self.running:
|
|
178
|
+
# Update ui before refreshing it
|
|
179
|
+
self.ui.update()
|
|
180
|
+
self.block_rendering = True
|
|
181
|
+
got_input = False
|
|
182
|
+
|
|
183
|
+
# Run through max 100 inputs (so the view is updated at least every 100 characters)
|
|
184
|
+
for i in range(self.max_input):
|
|
185
|
+
event = self.ui.get_input(False) # non-blocking
|
|
186
|
+
|
|
187
|
+
if not event:
|
|
188
|
+
break # no more inputs to process at this time
|
|
189
|
+
|
|
190
|
+
got_input = True
|
|
191
|
+
self.on_input(event)
|
|
192
|
+
|
|
193
|
+
if not got_input:
|
|
194
|
+
# Wait for input, since there were none already available
|
|
195
|
+
event = self.ui.get_input(True) # blocking
|
|
196
|
+
|
|
197
|
+
if event:
|
|
198
|
+
got_input = True
|
|
199
|
+
self.on_input(event) # PERF: Up to 30% processing time
|
|
200
|
+
|
|
201
|
+
self.block_rendering = False
|
|
202
|
+
|
|
203
|
+
self.trigger_event_after("mainloop")
|
|
204
|
+
# Rendering happens here
|
|
205
|
+
# TODO: Optimize performance. Can make up 45% of processing time in the loop.
|
|
206
|
+
self.get_editor().refresh()
|
|
207
|
+
self.ui.refresh()
|
|
208
|
+
|
|
209
|
+
def get_status(self):
|
|
210
|
+
"""Get the current status message.
|
|
211
|
+
|
|
212
|
+
:return: Current status message.
|
|
213
|
+
:rtype: str
|
|
214
|
+
"""
|
|
215
|
+
return self.status_msg
|
|
216
|
+
|
|
217
|
+
def get_file_index(self, file_obj):
|
|
218
|
+
"""Return the index of file_obj in the file list.
|
|
219
|
+
|
|
220
|
+
:param file_obj: File instance.
|
|
221
|
+
:return: Index of file_obj.
|
|
222
|
+
:rtype: int
|
|
223
|
+
"""
|
|
224
|
+
return self.files.index(file_obj)
|
|
225
|
+
|
|
226
|
+
def get_key_bindings(self):
|
|
227
|
+
"""Return the list of key bindings."""
|
|
228
|
+
return self.config.key_bindings
|
|
229
|
+
|
|
230
|
+
def get_event_bindings(self):
|
|
231
|
+
"""Return the dict of event bindings."""
|
|
232
|
+
return self.event_bindings
|
|
233
|
+
|
|
234
|
+
def set_key_binding(self, key, operation):
|
|
235
|
+
"""Bind a key to an operation.
|
|
236
|
+
|
|
237
|
+
Bind operation to be run when key is pressed.
|
|
238
|
+
|
|
239
|
+
:param key: What key or key combination to bind.
|
|
240
|
+
:param str operation: Which operation to run.
|
|
241
|
+
"""
|
|
242
|
+
self.config.keymap.prepend({"keys": [key], "command": operation})
|
|
243
|
+
|
|
244
|
+
def set_event_binding(self, event, when, callback):
|
|
245
|
+
"""Bind a callback to be run before or after an event.
|
|
246
|
+
|
|
247
|
+
Bind callback to run before or after event occurs. The when parameter
|
|
248
|
+
should be 'before' or 'after'. If using 'before' the callback can
|
|
249
|
+
inhibit running the event if it returns True
|
|
250
|
+
|
|
251
|
+
:param str event: Event to bind to.
|
|
252
|
+
:param str when: String with 'before' or 'after'.
|
|
253
|
+
:param callback: Callback to bind.
|
|
254
|
+
"""
|
|
255
|
+
event_bindings = self.get_event_bindings()
|
|
256
|
+
if when not in event_bindings.keys():
|
|
257
|
+
event_bindings[when] = {}
|
|
258
|
+
if event in event_bindings[when].keys():
|
|
259
|
+
event_bindings[when][event].append(callback)
|
|
260
|
+
else:
|
|
261
|
+
event_bindings[when][event] = [callback]
|
|
262
|
+
|
|
263
|
+
def set_status(self, status):
|
|
264
|
+
"""Set app status message.
|
|
265
|
+
|
|
266
|
+
:param str status: Status message to show in status bar.
|
|
267
|
+
"""
|
|
268
|
+
self.status_msg = str(status)
|
|
269
|
+
|
|
270
|
+
def unsaved_changes(self):
|
|
271
|
+
"""Return True if there are unsaved changes in any file."""
|
|
272
|
+
for f in self.files:
|
|
273
|
+
if f.is_changed():
|
|
274
|
+
return True
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
def unsaved_files(self):
|
|
278
|
+
"""Return the files that have unsaved changes.
|
|
279
|
+
|
|
280
|
+
:return: List of files with unsaved changes.
|
|
281
|
+
:rtype: list
|
|
282
|
+
"""
|
|
283
|
+
return [f for f in self.files if f.is_changed()]
|
|
284
|
+
|
|
285
|
+
def reload_config(self):
|
|
286
|
+
"""Reload configuration."""
|
|
287
|
+
self.config.reload()
|
|
288
|
+
for f in self.files:
|
|
289
|
+
self.setup_editor(f.editor)
|
|
290
|
+
self.trigger_event_after("config_loaded")
|
|
291
|
+
self.ui.resize()
|
|
292
|
+
self.ui.refresh()
|
|
293
|
+
|
|
294
|
+
def handle_input(self, event):
|
|
295
|
+
"""Handle an input event.
|
|
296
|
+
|
|
297
|
+
Runs relevant actions based on the event received.
|
|
298
|
+
|
|
299
|
+
:param event: An event instance.
|
|
300
|
+
:return: Boolean indicating if the event was handled.
|
|
301
|
+
:rtype: boolean
|
|
302
|
+
"""
|
|
303
|
+
if not event:
|
|
304
|
+
return False
|
|
305
|
+
self.last_input = event
|
|
306
|
+
if event.type == "key":
|
|
307
|
+
return self.handle_key(event)
|
|
308
|
+
elif event.type == "mouse":
|
|
309
|
+
return self.handle_mouse(event)
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
def handle_key(self, event):
|
|
313
|
+
"""Handle a key input event.
|
|
314
|
+
|
|
315
|
+
:param event: Event instance.
|
|
316
|
+
:return: Boolean indicating if event was handled.
|
|
317
|
+
:rtype: boolean
|
|
318
|
+
"""
|
|
319
|
+
key_bindings = self.get_key_bindings()
|
|
320
|
+
|
|
321
|
+
operation = None
|
|
322
|
+
if event.key_name in key_bindings.keys():
|
|
323
|
+
operation = key_bindings[event.key_name]
|
|
324
|
+
elif event.key_code in key_bindings.keys():
|
|
325
|
+
operation = key_bindings[event.key_code]
|
|
326
|
+
|
|
327
|
+
if operation in self.operations.keys():
|
|
328
|
+
self.run_operation(operation)
|
|
329
|
+
return True
|
|
330
|
+
elif operation in self.modules.modules.keys():
|
|
331
|
+
self.run_module(operation)
|
|
332
|
+
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
def handle_mouse(self, event):
|
|
336
|
+
"""Handle a mouse input event.
|
|
337
|
+
|
|
338
|
+
:param event: Event instance.
|
|
339
|
+
:return: Boolean indicating if event was handled.
|
|
340
|
+
:rtype: boolean
|
|
341
|
+
"""
|
|
342
|
+
editor = self.get_editor()
|
|
343
|
+
if event.mouse_code == 1: # Left mouse button release
|
|
344
|
+
editor.set_single_cursor(event.mouse_pos)
|
|
345
|
+
elif event.mouse_code == 4096: # Right mouse button release
|
|
346
|
+
editor.add_cursor(event.mouse_pos)
|
|
347
|
+
elif event.mouse_code == 524288: # Wheel up
|
|
348
|
+
editor.jump_up()
|
|
349
|
+
elif event.mouse_code == 134217728: # Wheel down(and unfortunately left button drag)
|
|
350
|
+
editor.jump_down()
|
|
351
|
+
else:
|
|
352
|
+
return False
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
###########################################################################
|
|
356
|
+
# User Interactions
|
|
357
|
+
###########################################################################
|
|
358
|
+
|
|
359
|
+
def help(self):
|
|
360
|
+
"""Toggle the help document.
|
|
361
|
+
- If current document is help, close it.
|
|
362
|
+
- Otherwise, if help is open, switch to it.
|
|
363
|
+
- Otherwise, open a new file with help text.
|
|
364
|
+
"""
|
|
365
|
+
if self.get_file().is_help:
|
|
366
|
+
self.close_file()
|
|
367
|
+
else:
|
|
368
|
+
idx = next((i for i, f in enumerate(self.files) if f.is_help), -1)
|
|
369
|
+
if idx == -1:
|
|
370
|
+
f = self.default_file()
|
|
371
|
+
from . import help
|
|
372
|
+
f.set_data(help.help_text)
|
|
373
|
+
f.is_help = True
|
|
374
|
+
self.files.append(f)
|
|
375
|
+
idx = self.last_file_index()
|
|
376
|
+
self.switch_to_file(idx)
|
|
377
|
+
|
|
378
|
+
def new_file(self, path=None):
|
|
379
|
+
"""Open a new empty file.
|
|
380
|
+
|
|
381
|
+
Open a new file and optionally set it's path.
|
|
382
|
+
|
|
383
|
+
:param str path: Optional. Path for file.
|
|
384
|
+
"""
|
|
385
|
+
new_file = self.default_file()
|
|
386
|
+
if path:
|
|
387
|
+
new_file.set_path(path)
|
|
388
|
+
self.files.append(new_file)
|
|
389
|
+
self.current_file = self.last_file_index()
|
|
390
|
+
return new_file
|
|
391
|
+
|
|
392
|
+
def exit_question(self):
|
|
393
|
+
"""Build the exit confirmation, naming what would be lost.
|
|
394
|
+
|
|
395
|
+
A bare "Exit?" doesn't say that unsaved work is about to be
|
|
396
|
+
discarded, which is the one thing the user needs to know.
|
|
397
|
+
|
|
398
|
+
:return: Question to put to the user.
|
|
399
|
+
:rtype: str
|
|
400
|
+
"""
|
|
401
|
+
unsaved = self.unsaved_files()
|
|
402
|
+
if len(unsaved) == 1:
|
|
403
|
+
return "Discard unsaved changes to '{0}' and exit?".format(unsaved[0].get_name())
|
|
404
|
+
return "Discard unsaved changes to {0} files and exit?".format(len(unsaved))
|
|
405
|
+
|
|
406
|
+
def ask_exit(self):
|
|
407
|
+
"""Exit if no unsaved changes, else make sure the user really wants to exit."""
|
|
408
|
+
if self.unsaved_changes():
|
|
409
|
+
yes = self.ui.query_bool(self.exit_question())
|
|
410
|
+
if yes:
|
|
411
|
+
self.exit()
|
|
412
|
+
return True
|
|
413
|
+
return False
|
|
414
|
+
self.exit()
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
def switch_to_file(self, index):
|
|
418
|
+
"""Load a default file if no files specified."""
|
|
419
|
+
self.current_file = index
|
|
420
|
+
|
|
421
|
+
def next_file(self):
|
|
422
|
+
"""Switch to next file."""
|
|
423
|
+
if len(self.files) < 2:
|
|
424
|
+
return
|
|
425
|
+
cur = self.current_file
|
|
426
|
+
cur += 1
|
|
427
|
+
if cur > len(self.files)-1:
|
|
428
|
+
cur = 0
|
|
429
|
+
self.switch_to_file(cur)
|
|
430
|
+
|
|
431
|
+
def prev_file(self):
|
|
432
|
+
"""Switch to previous file."""
|
|
433
|
+
if len(self.files) < 2:
|
|
434
|
+
return
|
|
435
|
+
cur = self.current_file
|
|
436
|
+
cur -= 1
|
|
437
|
+
if cur < 0:
|
|
438
|
+
cur = len(self.files)-1
|
|
439
|
+
self.switch_to_file(cur)
|
|
440
|
+
|
|
441
|
+
def go_to(self):
|
|
442
|
+
"""Go to a line or a file (or a line in a specific file with 'name:lineno')."""
|
|
443
|
+
input_str = self.ui.query("Go to:")
|
|
444
|
+
lineno = None
|
|
445
|
+
fname = None
|
|
446
|
+
if input_str is False:
|
|
447
|
+
return False
|
|
448
|
+
if input_str.find(":") != -1:
|
|
449
|
+
parts = input_str.split(":")
|
|
450
|
+
fname = parts[0]
|
|
451
|
+
lineno = parts[1]
|
|
452
|
+
file_index = self.find_file(fname)
|
|
453
|
+
if file_index != -1:
|
|
454
|
+
self.switch_to_file(file_index)
|
|
455
|
+
try:
|
|
456
|
+
input_str = int(lineno)
|
|
457
|
+
self.get_editor().go_to_pos(input_str)
|
|
458
|
+
except ValueError:
|
|
459
|
+
pass
|
|
460
|
+
else:
|
|
461
|
+
try:
|
|
462
|
+
line_no = int(input_str)
|
|
463
|
+
self.get_editor().go_to_pos(line_no)
|
|
464
|
+
except ValueError:
|
|
465
|
+
file_index = self.find_file(input_str)
|
|
466
|
+
if file_index != -1:
|
|
467
|
+
self.switch_to_file(file_index)
|
|
468
|
+
|
|
469
|
+
def find_file(self, s):
|
|
470
|
+
"""Return index of file matching string."""
|
|
471
|
+
# REFACTOR: Move to a helper function or implement in a module
|
|
472
|
+
|
|
473
|
+
# Case insensitive matching
|
|
474
|
+
s = s.lower()
|
|
475
|
+
|
|
476
|
+
# First match files beginning with s
|
|
477
|
+
for i, file in enumerate(self.files):
|
|
478
|
+
if file.name.lower().startswith(s):
|
|
479
|
+
return i
|
|
480
|
+
|
|
481
|
+
# Then match any files that contain s
|
|
482
|
+
for i, file in enumerate(self.files):
|
|
483
|
+
if s in file.name.lower():
|
|
484
|
+
return i
|
|
485
|
+
|
|
486
|
+
return -1
|
|
487
|
+
|
|
488
|
+
def run_command(self, data):
|
|
489
|
+
"""Run editor commands."""
|
|
490
|
+
parts = data.split(" ")
|
|
491
|
+
cmd = parts[0].lower()
|
|
492
|
+
if cmd in self.operations.keys():
|
|
493
|
+
return self.run_operation(cmd)
|
|
494
|
+
|
|
495
|
+
args = " ".join(parts[1:])
|
|
496
|
+
self.logger.debug("Looking for command '{0}'".format(cmd))
|
|
497
|
+
if cmd in self.modules.modules.keys():
|
|
498
|
+
self.logger.debug("Trying to run command '{0}'".format(cmd))
|
|
499
|
+
self.get_editor().store_action_state(cmd)
|
|
500
|
+
if not self.run_module(cmd, args):
|
|
501
|
+
return False
|
|
502
|
+
else:
|
|
503
|
+
self.set_status("Command '{0}' not found.".format(cmd))
|
|
504
|
+
return False
|
|
505
|
+
return True
|
|
506
|
+
|
|
507
|
+
def run_module(self, module_name, args=""):
|
|
508
|
+
try:
|
|
509
|
+
self.modules.modules[module_name].run(self, self.get_editor(), args)
|
|
510
|
+
return True
|
|
511
|
+
except Exception:
|
|
512
|
+
# Catch any error when running a module just incase
|
|
513
|
+
self.set_status("Running command failed!")
|
|
514
|
+
self.logger.exception("Running command failed!")
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
def run_operation(self, operation):
|
|
518
|
+
"""Run an app core operation."""
|
|
519
|
+
# Support arbitrary callables. TODO: deprecate
|
|
520
|
+
if hasattr(operation, "__call__"):
|
|
521
|
+
return operation()
|
|
522
|
+
|
|
523
|
+
if operation in self.operations.keys():
|
|
524
|
+
cancel = self.trigger_event_before(operation)
|
|
525
|
+
if not cancel:
|
|
526
|
+
result = self.operations[operation]()
|
|
527
|
+
self.trigger_event_after(operation)
|
|
528
|
+
return result
|
|
529
|
+
elif operation in self.modules.modules.keys():
|
|
530
|
+
cancel = self.trigger_event_before(operation)
|
|
531
|
+
if not cancel:
|
|
532
|
+
result = self.modules.modules[operation].run(self, self.get_editor(), "")
|
|
533
|
+
self.trigger_event_after(operation)
|
|
534
|
+
return result
|
|
535
|
+
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
def trigger_event(self, event, when):
|
|
539
|
+
"""Triggers event and runs registered callbacks."""
|
|
540
|
+
status = False
|
|
541
|
+
bindings = self.get_event_bindings()
|
|
542
|
+
if when not in bindings.keys():
|
|
543
|
+
return False
|
|
544
|
+
if event in bindings[when].keys():
|
|
545
|
+
callbacks = bindings[when][event]
|
|
546
|
+
for cb in callbacks:
|
|
547
|
+
try:
|
|
548
|
+
val = cb(event)
|
|
549
|
+
except Exception:
|
|
550
|
+
# Catch all errors in callbacks just incase
|
|
551
|
+
self.logger.error("Failed running callback: {0}".format(cb), exc_info=True)
|
|
552
|
+
continue
|
|
553
|
+
if val:
|
|
554
|
+
status = True
|
|
555
|
+
return status
|
|
556
|
+
|
|
557
|
+
def trigger_event_before(self, event):
|
|
558
|
+
return self.trigger_event(event, "before")
|
|
559
|
+
|
|
560
|
+
def trigger_event_after(self, event):
|
|
561
|
+
return self.trigger_event(event, "after")
|
|
562
|
+
|
|
563
|
+
def toggle_fullscreen(self):
|
|
564
|
+
"""Toggle full screen editor."""
|
|
565
|
+
display = self.config["display"]
|
|
566
|
+
show_indicators = not display["show_top_bar"]
|
|
567
|
+
display["show_top_bar"] = show_indicators
|
|
568
|
+
display["show_bottom_bar"] = show_indicators
|
|
569
|
+
display["show_legend"] = show_indicators
|
|
570
|
+
# Virtual curses windows need to be resized
|
|
571
|
+
self.ui.resize()
|
|
572
|
+
|
|
573
|
+
def toggle_mouse(self):
|
|
574
|
+
"""Toggle mouse support."""
|
|
575
|
+
# Invert the boolean
|
|
576
|
+
self.config["editor"]["use_mouse"] = not self.config["editor"]["use_mouse"]
|
|
577
|
+
self.ui.setup_mouse()
|
|
578
|
+
if self.config["editor"]["use_mouse"]:
|
|
579
|
+
self.set_status("Mouse enabled")
|
|
580
|
+
else:
|
|
581
|
+
self.set_status("Mouse disabled")
|
|
582
|
+
|
|
583
|
+
def query_command(self):
|
|
584
|
+
"""Run editor commands."""
|
|
585
|
+
modules = self.modules.modules.items()
|
|
586
|
+
|
|
587
|
+
# Get built in operations
|
|
588
|
+
completions = [oper for oper in self.operations.keys()]
|
|
589
|
+
# Add runnable modules
|
|
590
|
+
completions += [name for name, m in modules if m.is_runnable()]
|
|
591
|
+
|
|
592
|
+
data = self.ui.query_autocmp("Command:", completions=sorted(completions))
|
|
593
|
+
if not data:
|
|
594
|
+
return False
|
|
595
|
+
self.run_command(data)
|
|
596
|
+
|
|
597
|
+
###########################################################################
|
|
598
|
+
# Editor operations
|
|
599
|
+
###########################################################################
|
|
600
|
+
|
|
601
|
+
def new_editor(self):
|
|
602
|
+
"""Create a new editor instance."""
|
|
603
|
+
editor = Editor(self, self.ui.editor_win)
|
|
604
|
+
self.setup_editor(editor)
|
|
605
|
+
return editor
|
|
606
|
+
|
|
607
|
+
def get_editor(self):
|
|
608
|
+
"""Return the current editor."""
|
|
609
|
+
return self.files[self.current_file].editor
|
|
610
|
+
|
|
611
|
+
def setup_editor(self, editor):
|
|
612
|
+
"""Setup an editor instance with configuration."""
|
|
613
|
+
config = self.config["editor"]
|
|
614
|
+
editor.set_config(config)
|
|
615
|
+
editor.init()
|
|
616
|
+
|
|
617
|
+
###########################################################################
|
|
618
|
+
# File operations
|
|
619
|
+
###########################################################################
|
|
620
|
+
|
|
621
|
+
def open(self):
|
|
622
|
+
"""Ask for file name and try to open it."""
|
|
623
|
+
input_name = self.ui.query_file("Open file:")
|
|
624
|
+
if not input_name:
|
|
625
|
+
return False
|
|
626
|
+
name_row_col = helpers.get_filename_cursor_pos(input_name)
|
|
627
|
+
name = name_row_col["name"]
|
|
628
|
+
exists = self.file_is_open(name)
|
|
629
|
+
if exists:
|
|
630
|
+
self.switch_to_file(self.files.index(exists))
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
if not self.open_file(**name_row_col):
|
|
634
|
+
self.set_status("Failed to load '{0}'".format(name))
|
|
635
|
+
return False
|
|
636
|
+
self.switch_to_file(self.last_file_index())
|
|
637
|
+
return True
|
|
638
|
+
|
|
639
|
+
def close_file(self):
|
|
640
|
+
"""Close current file if user confirms action."""
|
|
641
|
+
if self.get_file().is_changed():
|
|
642
|
+
if not self.ui.query_bool("Close file?"):
|
|
643
|
+
return False
|
|
644
|
+
self.files.pop(self.current_file)
|
|
645
|
+
if not len(self.files):
|
|
646
|
+
self.new_file()
|
|
647
|
+
return False
|
|
648
|
+
if self.current_file == len(self.files):
|
|
649
|
+
self.current_file -= 1
|
|
650
|
+
|
|
651
|
+
def save_file(self, file=False, overwrite=False):
|
|
652
|
+
"""Save current file."""
|
|
653
|
+
f = file or self.get_file()
|
|
654
|
+
# Make sure the file has a name
|
|
655
|
+
if not f.get_name():
|
|
656
|
+
return self.save_file_as(f)
|
|
657
|
+
# Warn if the file has changed on disk
|
|
658
|
+
if not overwrite and f.is_changed_on_disk():
|
|
659
|
+
if not self.ui.query_bool("The file was modified since you opened it, save anyway?"):
|
|
660
|
+
return False
|
|
661
|
+
# Save the file
|
|
662
|
+
if f.save():
|
|
663
|
+
self.set_status("Saved [{0}] '{1}'".format(helpers.curr_time_sec(), f.name))
|
|
664
|
+
if f.path() == self.config.path() or f.path() == self.config.keymap_path():
|
|
665
|
+
self.reload_config()
|
|
666
|
+
return True
|
|
667
|
+
self.set_status("Couldn't write to '{0}'".format(f.name))
|
|
668
|
+
return False
|
|
669
|
+
|
|
670
|
+
def save_file_as(self, file=False):
|
|
671
|
+
"""Save current file."""
|
|
672
|
+
f = file or self.get_file()
|
|
673
|
+
name_input = self.ui.query_file("Save as:", f.name)
|
|
674
|
+
if not name_input:
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
target_dir, name = helpers.parse_path(name_input)
|
|
678
|
+
full_path = os.path.join(target_dir, name)
|
|
679
|
+
|
|
680
|
+
if os.path.exists(full_path):
|
|
681
|
+
if not self.ui.query_bool("A file or directory with that name already exists. Overwrite it?"):
|
|
682
|
+
return False
|
|
683
|
+
if target_dir and not os.path.exists(target_dir):
|
|
684
|
+
if self.ui.query_bool("The path doesn't exist, do you want to create it?"):
|
|
685
|
+
self.logger.debug("Creating missing folders in save path.")
|
|
686
|
+
try:
|
|
687
|
+
os.makedirs(target_dir)
|
|
688
|
+
except OSError:
|
|
689
|
+
self.logger.exception("Couldn't create directories!")
|
|
690
|
+
self.ui.set_status("Couldn't create directories!")
|
|
691
|
+
return False
|
|
692
|
+
else:
|
|
693
|
+
return False
|
|
694
|
+
f.set_path(full_path)
|
|
695
|
+
# We can just overwrite the file since the user already confirmed
|
|
696
|
+
return self.save_file(f, overwrite=True)
|
|
697
|
+
|
|
698
|
+
def reload_file(self):
|
|
699
|
+
"""Reload the current file."""
|
|
700
|
+
if self.ui.query_bool("Reload '{0}'?".format(self.get_file().name)):
|
|
701
|
+
if self.get_file().reload():
|
|
702
|
+
return True
|
|
703
|
+
return False
|
|
704
|
+
|
|
705
|
+
def get_files(self):
|
|
706
|
+
"""Return list of open files."""
|
|
707
|
+
return self.files
|
|
708
|
+
|
|
709
|
+
def get_file(self):
|
|
710
|
+
"""Return the current file."""
|
|
711
|
+
return self.files[self.current_file]
|
|
712
|
+
|
|
713
|
+
def last_file_index(self):
|
|
714
|
+
"""Get index of last file."""
|
|
715
|
+
cur = len(self.files)-1
|
|
716
|
+
return cur
|
|
717
|
+
|
|
718
|
+
def current_file_index(self):
|
|
719
|
+
"""Get index of current file."""
|
|
720
|
+
return self.current_file
|
|
721
|
+
|
|
722
|
+
def open_file(self, name=None, row=0, col=0):
|
|
723
|
+
"""Open a file."""
|
|
724
|
+
file = File(self)
|
|
725
|
+
file.set_path(name)
|
|
726
|
+
file.set_editor(self.new_editor())
|
|
727
|
+
if not file.load():
|
|
728
|
+
return False
|
|
729
|
+
file.get_editor().set_single_cursor((col, row))
|
|
730
|
+
file.get_editor().scroll_to_line(row)
|
|
731
|
+
self.files.append(file)
|
|
732
|
+
return True
|
|
733
|
+
|
|
734
|
+
def load_files(self):
|
|
735
|
+
"""Try to load all files specified in arguments."""
|
|
736
|
+
if self.filenames:
|
|
737
|
+
for item in self.filenames:
|
|
738
|
+
name_row_col = helpers.get_filename_cursor_pos(item)
|
|
739
|
+
name = name_row_col["name"]
|
|
740
|
+
if os.path.isdir(name):
|
|
741
|
+
continue
|
|
742
|
+
# Avoid opening duplicate files
|
|
743
|
+
if self.file_is_open(name):
|
|
744
|
+
continue
|
|
745
|
+
if not self.open_file(**name_row_col):
|
|
746
|
+
self.new_file(name)
|
|
747
|
+
# If nothing was loaded
|
|
748
|
+
if not self.files:
|
|
749
|
+
self.load_default()
|
|
750
|
+
|
|
751
|
+
def file_is_open(self, path):
|
|
752
|
+
"""Check if file is open. Returns a File object or False."""
|
|
753
|
+
for file in self.files:
|
|
754
|
+
if file.path() == os.path.abspath(path):
|
|
755
|
+
return file
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
def load_default(self):
|
|
759
|
+
"""Load a default file if no files specified."""
|
|
760
|
+
file = self.default_file()
|
|
761
|
+
self.files.append(file)
|
|
762
|
+
|
|
763
|
+
def default_file(self):
|
|
764
|
+
"""Create the default file."""
|
|
765
|
+
file = File(self)
|
|
766
|
+
file.set_editor(self.new_editor())
|
|
767
|
+
# Specify contents to avoid appearing as modified
|
|
768
|
+
file.set_data("")
|
|
769
|
+
# Set markdown as the default file type
|
|
770
|
+
file.editor.set_file_extension("md")
|
|
771
|
+
return file
|