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.
Files changed (72) hide show
  1. suplemon/__init__.py +0 -0
  2. suplemon/__main__.py +3 -0
  3. suplemon/cli.py +65 -0
  4. suplemon/config/defaults.json +153 -0
  5. suplemon/config/keymap.json +63 -0
  6. suplemon/config.py +324 -0
  7. suplemon/cursor.py +135 -0
  8. suplemon/editor.py +568 -0
  9. suplemon/file.py +193 -0
  10. suplemon/help.py +327 -0
  11. suplemon/helpers.py +104 -0
  12. suplemon/hex2xterm.py +23 -0
  13. suplemon/key_mappings.py +239 -0
  14. suplemon/lexer.py +88 -0
  15. suplemon/line.py +51 -0
  16. suplemon/linelight/__init__.py +0 -0
  17. suplemon/linelight/color_map.py +9 -0
  18. suplemon/linelight/css.py +21 -0
  19. suplemon/linelight/diff.py +17 -0
  20. suplemon/linelight/html.py +19 -0
  21. suplemon/linelight/js.py +21 -0
  22. suplemon/linelight/json.py +15 -0
  23. suplemon/linelight/lua.py +10 -0
  24. suplemon/linelight/md.py +19 -0
  25. suplemon/linelight/php.py +27 -0
  26. suplemon/linelight/py.py +27 -0
  27. suplemon/logger.py +90 -0
  28. suplemon/main.py +771 -0
  29. suplemon/module_loader.py +98 -0
  30. suplemon/modules/application_state.py +84 -0
  31. suplemon/modules/autocomplete.py +114 -0
  32. suplemon/modules/autodocstring.py +142 -0
  33. suplemon/modules/battery.py +118 -0
  34. suplemon/modules/bulk_delete.py +75 -0
  35. suplemon/modules/clock.py +22 -0
  36. suplemon/modules/comment.py +59 -0
  37. suplemon/modules/config.py +20 -0
  38. suplemon/modules/date.py +22 -0
  39. suplemon/modules/diff.py +42 -0
  40. suplemon/modules/eval.py +43 -0
  41. suplemon/modules/hostname.py +34 -0
  42. suplemon/modules/input_test.py +23 -0
  43. suplemon/modules/keymap.py +20 -0
  44. suplemon/modules/linter.py +274 -0
  45. suplemon/modules/lower.py +21 -0
  46. suplemon/modules/lstrip.py +20 -0
  47. suplemon/modules/paste.py +49 -0
  48. suplemon/modules/reload.py +16 -0
  49. suplemon/modules/replace_all.py +23 -0
  50. suplemon/modules/reverse.py +22 -0
  51. suplemon/modules/rstrip.py +19 -0
  52. suplemon/modules/save.py +16 -0
  53. suplemon/modules/save_all.py +19 -0
  54. suplemon/modules/sort_lines.py +41 -0
  55. suplemon/modules/strip.py +19 -0
  56. suplemon/modules/system_clipboard.py +121 -0
  57. suplemon/modules/tabstospaces.py +18 -0
  58. suplemon/modules/toggle_whitespace.py +18 -0
  59. suplemon/modules/upper.py +21 -0
  60. suplemon/prompt.py +319 -0
  61. suplemon/suplemon_module.py +210 -0
  62. suplemon/themes/8colors.tmTheme +179 -0
  63. suplemon/themes/monokai.tmTheme +284 -0
  64. suplemon/themes.py +201 -0
  65. suplemon/ui.py +656 -0
  66. suplemon/viewer.py +1004 -0
  67. suplemon_editor-0.3.0.dist-info/METADATA +515 -0
  68. suplemon_editor-0.3.0.dist-info/RECORD +72 -0
  69. suplemon_editor-0.3.0.dist-info/WHEEL +5 -0
  70. suplemon_editor-0.3.0.dist-info/entry_points.txt +3 -0
  71. suplemon_editor-0.3.0.dist-info/licenses/LICENSE +21 -0
  72. suplemon_editor-0.3.0.dist-info/top_level.txt +1 -0
suplemon/cursor.py ADDED
@@ -0,0 +1,135 @@
1
+ # -*- encoding: utf-8
2
+ """
3
+ Cursor object for storing cursor data.
4
+ """
5
+
6
+
7
+ class Cursor:
8
+ def __init__(self, x=0, y=0):
9
+ """Initialize Cursor.
10
+
11
+ :param x: Cursor x coordinate or x,y tuple. Defaults to 0.
12
+ :param y: Cursor y coordinate. Defaults to 0.
13
+ """
14
+ # Handle coords as a tuple
15
+ if isinstance(x, tuple) or isinstance(x, list):
16
+ x, y = x
17
+ self.x = x
18
+ self.y = y
19
+ # Handle coords from existing Cursor
20
+ elif isinstance(x, Cursor): # Handle arguments as a cursor
21
+ self.x = x.x
22
+ self.y = x.y
23
+ # Handle coords as plain ints
24
+ else:
25
+ self.x = x
26
+ self.y = y
27
+ # Store the desired x position and
28
+ # use it if the line is long enough
29
+ self.persistent_x = self.x
30
+
31
+ def get_x(self):
32
+ """Return the x coordinate of the cursor.
33
+
34
+ :return: Cursor x coordinate.
35
+ :rtype: int
36
+ """
37
+ return self.x
38
+
39
+ def get_y(self):
40
+ """Return the y coordinate of the cursor.
41
+
42
+ :return: Cursor y coordinate.
43
+ :rtype: int
44
+ """
45
+ return self.y
46
+
47
+ def set_x(self, x):
48
+ """Set the x coordinate of the cursor."""
49
+ self.x = x
50
+ self.persistent_x = x
51
+
52
+ def set_y(self, y):
53
+ """Set the y coordinate of the cursor."""
54
+ self.y = y
55
+
56
+ def move_left(self, delta=1):
57
+ """Move the cursor left by delta steps.
58
+
59
+ :param int delta: How much to move. Defaults to 1.
60
+ """
61
+ self.x -= delta
62
+ if self.x < 0:
63
+ self.x = 0
64
+ self.persistent_x = self.x
65
+ return
66
+
67
+ def move_right(self, delta=1):
68
+ """Move the cursor right by delta steps.
69
+
70
+ :param int delta: How much to move. Defaults to 1.
71
+ """
72
+ self.x += delta
73
+ # Check in case of negative values
74
+ if self.x < 0:
75
+ self.x = 0
76
+ self.persistent_x = self.x
77
+ return
78
+
79
+ def move_up(self, delta=1):
80
+ """Move the cursor up by delta steps.
81
+
82
+ :param int delta: How much to move. Defaults to 1.
83
+ """
84
+ self.y -= delta
85
+ if self.y < 0:
86
+ self.y = 0
87
+ return
88
+
89
+ def move_down(self, delta=1):
90
+ """Move the cursor down by delta steps.
91
+
92
+ :param int delta: How much to move. Defaults to 1.
93
+ """
94
+ self.y += delta
95
+ return
96
+
97
+ def __getitem__(self, i):
98
+ # TODO: Deprecate in favor of proper access methods.
99
+ """Get coordinates with list indices.
100
+
101
+ :param i: 0 or 1 for x or y
102
+ :return: x or y coordinate of cursor.
103
+ :rtype: int
104
+ """
105
+ if i == 0:
106
+ return self.x
107
+ elif i == 1:
108
+ return self.y
109
+
110
+ def __eq__(self, item):
111
+ """Check cursor for equality."""
112
+ if isinstance(item, Cursor):
113
+ if item.x == self.x and item.y == self.y:
114
+ return True
115
+ return False
116
+
117
+ def __ne__(self, item):
118
+ """Check cursor for unequality."""
119
+ if isinstance(item, Cursor):
120
+ if item.x != self.x or item.y != self.x:
121
+ return False
122
+
123
+ def __str__(self):
124
+ return "Cursor({x},{y})".format(x=self.x, y=self.y)
125
+
126
+ def __repr__(self):
127
+ return self.__str__()
128
+
129
+ def tuple(self):
130
+ """Return the cursor as a tuple.
131
+
132
+ :return: Tuple with x and y coordinates of cursor.
133
+ :rtype: tuple
134
+ """
135
+ return (self.x, self.y)
suplemon/editor.py ADDED
@@ -0,0 +1,568 @@
1
+ # -*- encoding: utf-8
2
+ """
3
+ Editor class for extending viewer with text editing features.
4
+ """
5
+
6
+
7
+ from . import helpers
8
+
9
+ from .line import Line
10
+ from .cursor import Cursor
11
+ from .viewer import Viewer
12
+
13
+
14
+ class State:
15
+ """Store editor state for undo/redo."""
16
+ def __init__(self, editor=None):
17
+ self.cursors = [Cursor()]
18
+ self.lines = [Line()]
19
+ self.y_scroll = 0
20
+ self.x_scroll = 0
21
+ self.last_find = ""
22
+ if editor is not None:
23
+ self.store(editor)
24
+
25
+ def store(self, editor):
26
+ """Store the state of editor instance."""
27
+ self.cursors = [cursor.tuple() for cursor in editor.cursors]
28
+ self.lines = [line.data for line in editor.lines]
29
+ self.y_scroll = editor.y_scroll
30
+ self.x_scroll = editor.x_scroll
31
+ self.last_find = editor.last_find
32
+
33
+ def restore(self, editor):
34
+ """Restore stored state into the editor instance."""
35
+ editor.cursors = [Cursor(cursor) for cursor in self.cursors]
36
+ editor.lines = [Line(line) for line in self.lines]
37
+ editor.y_scroll = self.y_scroll
38
+ editor.x_scroll = self.x_scroll
39
+ editor.last_find = self.last_find
40
+
41
+
42
+ class Editor(Viewer):
43
+ """Extends Viewer with editing capabilities."""
44
+ def __init__(self, app, window):
45
+ """Initialize the editor.
46
+
47
+ Args:
48
+ app: The Suplemon main instance.
49
+ window: A window object to use for the ui.
50
+ """
51
+ Viewer.__init__(self, app, window)
52
+
53
+ # History of editor states for undo/redo
54
+ self.history = [State()]
55
+ # Current state index of the editor
56
+ self.current_state = 0
57
+ # Last editor action that was used (for undo/redo)
58
+ self.last_action = None
59
+
60
+ def init(self):
61
+ Viewer.init(self)
62
+ operations = {
63
+ "backspace": self.backspace, # Backspace
64
+ "delete": self.delete, # Delete
65
+ "insert": self.insert, # Insert
66
+ "enter": self.enter, # Enter
67
+ "tab": self.tab, # Tab
68
+ "untab": self.untab, # Shift + Tab
69
+ "escape": self.escape, # Escape
70
+ "single_selection": self.single_selection, # Escape
71
+ "clear_last_find": self.clear_last_find, # Escape
72
+ "new_cursor_up": self.new_cursor_up, # Alt + Up
73
+ "new_cursor_down": self.new_cursor_down, # Alt + Down
74
+ "new_cursor_left": self.new_cursor_left, # Alt + Left
75
+ "new_cursor_right": self.new_cursor_right, # Alt + Right
76
+ "page_up": self.page_up, # Page Up
77
+ "page_down": self.page_down, # Page Down
78
+ "push_up": self.push_up, # Alt + Page Up
79
+ "push_down": self.push_down, # Alt + Page Down
80
+ "undo": self.undo, # F5
81
+ "redo": self.redo, # F6
82
+ "toggle_line_nums": self.toggle_line_nums, # F9
83
+ "toggle_line_ends": self.toggle_line_ends, # F10
84
+ "toggle_highlight": self.toggle_highlight, # F11
85
+ "copy": self.copy, # Ctrl + C
86
+ "cut": self.cut, # Ctrl + X
87
+ "duplicate_line": self.duplicate_line, # Ctrl + W
88
+ }
89
+ for key in operations.keys():
90
+ self.operations[key] = operations[key]
91
+
92
+ def set_buffer(self, buffer):
93
+ """Sets local or global buffer depending on config."""
94
+ if self.app.config["editor"]["use_global_buffer"]:
95
+ self.app.global_buffer = buffer
96
+ else:
97
+ self.buffer = buffer
98
+
99
+ def set_data(self, data):
100
+ """Set the editor text contents."""
101
+ Viewer.set_data(self, data)
102
+ buffer = self.get_buffer() # TODO: check this
103
+ if len(buffer) > 1:
104
+ self.store_state()
105
+ else:
106
+ state = State()
107
+ state.store(self)
108
+ self.history[0] = state
109
+
110
+ def store_action_state(self, action, state=None):
111
+ """Store the editor state if a new action is taken."""
112
+ if self.last_action != action:
113
+ self.last_action = action
114
+ self.store_state(state)
115
+ else:
116
+ # FIXME: This if is here just for safety.
117
+ # FIXME: current_state might be wrong ;.<
118
+ if self.current_state < len(self.history)-1:
119
+ self.history[self.current_state].store(self)
120
+
121
+ def store_state(self, state=None, action=None):
122
+ """Store the current editor state for undo/redo."""
123
+ if state is None:
124
+ state = State()
125
+ state.store(self)
126
+ if len(self.history) > 1:
127
+ if self.current_state < len(self.history)-1:
128
+ # Drop the redo branch, but keep the state being edited from.
129
+ # Slicing at current_state dropped that state too, so undoing
130
+ # after an undo followed by an edit skipped a step.
131
+ self.history = self.history[:self.current_state+1]
132
+
133
+ self.history.append(state)
134
+ self.current_state = len(self.history)-1
135
+
136
+ if len(self.history) > self.config["max_history"]:
137
+ self.history.pop(0)
138
+
139
+ def restore_state(self, index=None):
140
+ """Restore an editor state."""
141
+ if len(self.history) <= 1:
142
+ return False
143
+ if index is None:
144
+ index = self.current_state-1
145
+
146
+ if index < 0 or index >= len(self.history):
147
+ return False
148
+
149
+ # if self.current_state < len(self.history):
150
+ # self.current_state = self.current_state-1
151
+
152
+ state = self.history[index]
153
+ state.restore(self)
154
+ self.current_state = index
155
+
156
+ def handle_input(self, event):
157
+ done = Viewer.handle_input(self, event)
158
+ if not done:
159
+ if event.is_typeable:
160
+ if isinstance(event.key_code, str):
161
+ self.type(event.key_code)
162
+ elif event.key_name:
163
+ self.type(event.key_name)
164
+ return True
165
+ return False
166
+
167
+ def undo(self):
168
+ """Undo the last command or change."""
169
+ self.last_action = "undo"
170
+ self.restore_state()
171
+
172
+ def redo(self):
173
+ """Redo the last command or change."""
174
+ self.last_action = "redo"
175
+ if self.current_state == len(self.history)-1:
176
+ return False
177
+ index = self.current_state+1
178
+ self.restore_state(index)
179
+
180
+ #
181
+ # Cursor operations
182
+ #
183
+
184
+ def new_cursor_up(self):
185
+ """Add a new cursor one line up."""
186
+ x = self.get_cursor().x
187
+ cursor = self.get_first_cursor()
188
+ if cursor.y == 0:
189
+ return
190
+ new = Cursor(x, cursor.y-1)
191
+ self.cursors.append(new)
192
+ self.move_cursors()
193
+ self.scroll_up()
194
+
195
+ def new_cursor_down(self):
196
+ """Add a new cursor one line down."""
197
+ x = self.get_cursor().x
198
+ cursor = self.get_last_cursor()
199
+ if cursor.y == len(self.lines)-1:
200
+ return
201
+ new = Cursor(x, cursor.y+1)
202
+ self.cursors.append(new)
203
+ self.move_cursors()
204
+ self.scroll_down()
205
+
206
+ def new_cursor_left(self):
207
+ """Add a new cursor one character left."""
208
+ new = []
209
+ for cursor in self.cursors:
210
+ if cursor.x == 0:
211
+ continue
212
+ new.append(Cursor(cursor.x-1, cursor.y))
213
+ for c in new:
214
+ self.cursors.append(c)
215
+ self.move_cursors()
216
+ self.scroll_up()
217
+
218
+ def new_cursor_right(self):
219
+ """Add a new cursor one character right."""
220
+ new = []
221
+ for cursor in self.cursors:
222
+ if cursor.x+1 > len(self.lines[cursor.y]):
223
+ continue
224
+ new.append(Cursor(cursor.x+1, cursor.y))
225
+ for c in new:
226
+ self.cursors.append(c)
227
+ self.move_cursors()
228
+ self.scroll_down()
229
+
230
+ def escape(self):
231
+ """Handle escape key.
232
+
233
+ Wrapper for clear_last_find and single_selection."""
234
+ self.clear_last_find()
235
+ self.single_selection()
236
+
237
+ def clear_last_find(self):
238
+ """Removes last_find so a new auto-find can be initiated."""
239
+ self.last_find = ""
240
+
241
+ def single_selection(self):
242
+ """Removes all cursors except primary cursor."""
243
+ self.cursors = [self.cursors[0]]
244
+ self.move_cursors()
245
+
246
+ #
247
+ # Text editing operations
248
+ #
249
+
250
+ def replace_all(self, what, replacement):
251
+ """Replaces what with replacement on each line."""
252
+ for line in self.lines:
253
+ data = line.get_data()
254
+ new = data.replace(what, replacement)
255
+ line.set_data(new)
256
+ self.move_cursors()
257
+
258
+ def delete(self):
259
+ """Delete the next character."""
260
+ for cursor in self.cursors:
261
+ if len(self.lines)-1 < cursor.y:
262
+ # If we've run out of lines
263
+ break
264
+ line = self.lines[cursor.y]
265
+ # if we have more than 1 line
266
+ # and we're at the end of the current line
267
+ # and we're not on the last line
268
+ if len(self.lines) > 1 and cursor.x == len(line) and cursor.y != len(self.lines) - 1:
269
+ data = self.lines[cursor.y].get_data()
270
+ self.lines.pop(cursor.y)
271
+ self.lines[cursor.y].set_data(data+self.lines[cursor.y])
272
+ # Reposition cursors from line below into correct positions on current line
273
+ line_cursors = self.get_cursors_on_line(cursor.y+1)
274
+ for c in line_cursors:
275
+ c.move_right(len(data))
276
+ c.move_up()
277
+ self.move_y_cursors(cursor.y, -1)
278
+ else:
279
+ start = line[:cursor.x]
280
+ end = line[cursor.x+1:]
281
+ self.lines[cursor.y].set_data(start+end)
282
+ self.move_x_cursors(cursor.y, cursor.x, -1)
283
+ self.move_cursors()
284
+ # Add a restore point if previous action != delete
285
+ self.store_action_state("delete")
286
+
287
+ def backspace(self):
288
+ """Delete the previous character."""
289
+ curs = reversed(sorted(self.cursors, key=lambda c: (c[1], c[0])))
290
+ # Iterate through all cursors from bottom to top
291
+ for cursor in curs:
292
+ line_no = cursor.y
293
+ # If we're at the beginning of file don't do anything
294
+ if cursor.x == 0 and cursor.y == 0:
295
+ continue
296
+ # If were operating at the beginning of a line
297
+ if cursor.x == 0 and cursor.y != 0:
298
+ curr_line = self.lines.pop(line_no)
299
+ prev_line = self.lines[line_no-1]
300
+ length = len(prev_line) # Get the length of previous line
301
+
302
+ # Add the current line to the previous one
303
+ new_data = self.lines[cursor.y-1] + curr_line
304
+ self.lines[cursor.y-1].set_data(new_data)
305
+
306
+ # Get all cursors on current line
307
+ line_cursors = self.get_cursors_on_line(line_no)
308
+
309
+ for line_cursor in line_cursors: # Move the cursors
310
+ line_cursor.move_up()
311
+ # Add the length of previous line to each x coordinate
312
+ # so that their relative positions
313
+ line_cursor.move_right(length)
314
+ # Move all cursors below up one line
315
+ # (since a line was removed above them)
316
+ self.move_y_cursors(cursor.y, -1)
317
+ # Handle all other cases
318
+ else:
319
+ curr_line = self.lines[line_no]
320
+ # Remove one character by default
321
+ del_n_chars = 1
322
+ # Check if we should unindent
323
+ if self.config["backspace_unindent"]:
324
+ # Check if we can unindent, and that it's actually whitespace
325
+ # We don't do this for hard tabs since they're just a single character
326
+ if not self.config["hard_tabs"]:
327
+ indent = self.config["tab_width"]
328
+ if cursor.x >= indent:
329
+ if curr_line[cursor.x-indent:cursor.x] == indent*" ":
330
+ # Remove an indents worth of whitespace
331
+ del_n_chars = indent
332
+ # Slice characters out of the line
333
+ start = curr_line[:cursor.x-del_n_chars]
334
+ end = curr_line[cursor.x:]
335
+ # Store the new line
336
+ self.lines[line_no].set_data(start+end)
337
+ # Move the operating curser back the deleted amount
338
+ cursor.move_left(del_n_chars)
339
+ # Do the same to the rest
340
+ self.move_x_cursors(line_no, cursor.x, -1*del_n_chars)
341
+ # Ensure we keep the view scrolled
342
+ self.move_cursors()
343
+ self.scroll_up()
344
+ # Add a restore point if previous action != backspace
345
+ self.store_action_state("backspace")
346
+
347
+ def enter(self):
348
+ """Insert a new line at each cursor."""
349
+ # We sort the cursors, and loop through them from last to first
350
+ # That way we avoid messing with
351
+ # the relative positions of the higher cursors
352
+ curs = sorted(self.cursors, key=lambda c: (c[1], c[0]))
353
+ curs = reversed(curs)
354
+ for cursor in curs:
355
+ # The current line this cursor is on
356
+ line = self.lines[cursor.y]
357
+
358
+ # Start of the line
359
+ start = line[:cursor.x]
360
+
361
+ # End of the line
362
+ end = line[cursor.x:]
363
+
364
+ # Leave the beginning of the line
365
+ self.lines[cursor.y].set_data(start)
366
+ wspace = ""
367
+ if self.config["auto_indent_newline"]:
368
+ wspace = helpers.leading_whitespace(self.lines[cursor.y])
369
+ self.lines.insert(cursor.y+1, Line(wspace+end))
370
+ self.move_y_cursors(cursor.y, 1)
371
+ cursor.set_x(len(wspace))
372
+ cursor.move_down()
373
+ self.move_cursors()
374
+ self.scroll_down()
375
+ # Add a restore point if previous action != enter
376
+ self.store_action_state("enter")
377
+
378
+ def insert(self):
379
+ """Insert buffer data at cursor(s)."""
380
+ cur = self.get_cursor()
381
+ buffer = list(self.get_buffer())
382
+
383
+ # If we have more than one cursor
384
+ # Or one cursor and one line
385
+ if len(self.cursors) > 1 or len(buffer) == 1:
386
+ # If the cursor count is more than the buffer length extend
387
+ # the buffer until it's at least as long as the cursor count
388
+ while len(buffer) < len(self.cursors):
389
+ buffer.extend(buffer)
390
+ curs = sorted(self.cursors, key=lambda c: (c[1], c[0]))
391
+ for cursor in curs:
392
+ line = self.lines[cursor.y]
393
+ buf = buffer[0]
394
+ line = line[:cursor.x] + buf + line[cursor.x:]
395
+ self.lines[cursor.y].set_data(line)
396
+ buffer.pop(0)
397
+ self.move_x_cursors(cursor.y, cursor.x-1, len(buf))
398
+ # If we have one cursor and multiple lines
399
+ else:
400
+ for buf in buffer:
401
+ y = cur[1]
402
+ if y < 0:
403
+ y = 0
404
+ self.lines.insert(y, Line(buf))
405
+ self.move_y_cursors(cur[1]-1, 1)
406
+ self.move_cursors()
407
+ self.scroll_down()
408
+ # Add a restore point if previous action != insert
409
+ self.store_action_state("insert")
410
+
411
+ def insert_lines_at(self, lines, at):
412
+ rev_lines = reversed(lines)
413
+ for line in rev_lines:
414
+ self.lines.insert(at, Line(line))
415
+ self.move_y_cursors(at, len(lines))
416
+
417
+ def push_up(self):
418
+ """Move current lines up by one line."""
419
+ used_y = []
420
+ curs = sorted(self.cursors, key=lambda c: (c[1], c[0]))
421
+ for cursor in curs:
422
+ if cursor.y in used_y:
423
+ continue
424
+ used_y.append(cursor.y)
425
+ if cursor.y == 0:
426
+ break
427
+ old = self.lines[cursor.y-1]
428
+ self.lines[cursor.y-1] = self.lines[cursor.y]
429
+ self.lines[cursor.y] = old
430
+ self.move_cursors((0, -1))
431
+ self.scroll_up()
432
+ # Add a restore point if previous action != push_up
433
+ self.store_action_state("push_up")
434
+
435
+ def push_down(self):
436
+ """Move current lines down by one line."""
437
+ used_y = []
438
+ curs = reversed(sorted(self.cursors, key=lambda c: (c[1], c[0])))
439
+ for cursor in curs:
440
+ if cursor.y in used_y:
441
+ continue
442
+ if cursor.y >= len(self.lines)-1:
443
+ break
444
+ used_y.append(cursor.y)
445
+ old = self.lines[cursor.y+1]
446
+ self.lines[cursor.y+1] = self.lines[cursor.y]
447
+ self.lines[cursor.y] = old
448
+
449
+ self.move_cursors((0, 1))
450
+ self.scroll_down()
451
+ # Add a restore point if previous action != push_down
452
+ self.store_action_state("push_down")
453
+
454
+ def tab(self):
455
+ """Indent lines."""
456
+ # Add a restore point if previous action != tab
457
+ self.store_action_state("tab")
458
+ if not self.config["hard_tabs"]:
459
+ self.type(" "*self.config["tab_width"])
460
+ else:
461
+ self.type("\t")
462
+
463
+ def untab(self):
464
+ """Unindent lines."""
465
+ linenums = []
466
+ # String to compare tabs to
467
+ tab = " "*self.config["tab_width"]
468
+ if self.config["hard_tabs"]:
469
+ tab = "\t"
470
+ width = len(tab)
471
+ for cursor in self.cursors:
472
+ line = self.lines[cursor.y]
473
+ if cursor.y in linenums:
474
+ cursor.x = helpers.whitespace(line)
475
+ continue
476
+ elif line[:width] == tab:
477
+ line = Line(line[width:])
478
+ self.lines[cursor.y] = line
479
+ cursor.x = helpers.whitespace(line)
480
+ linenums.append(cursor.y)
481
+ # Add a restore point if previous action != untab
482
+ self.store_action_state("untab")
483
+
484
+ def copy(self):
485
+ """Copy lines to buffer."""
486
+ # Store cut lines in buffer
487
+ copy_buffer = []
488
+ # Get all lines with cursors on them
489
+ line_nums = self.get_lines_with_cursors()
490
+
491
+ for i in range(len(line_nums)):
492
+ # Get the line
493
+ line = self.lines[line_nums[i]]
494
+ # Put it in our temporary buffer
495
+ copy_buffer.append(line.get_data())
496
+ self.set_buffer(copy_buffer)
497
+ self.store_action_state("copy")
498
+
499
+ def cut(self):
500
+ """Cut lines to buffer."""
501
+ # Store cut lines in buffer
502
+ cut_buffer = []
503
+ # Get all lines with cursors on them
504
+ line_nums = self.get_lines_with_cursors()
505
+ # Sort from last to first (invert order)
506
+ line_nums = line_nums[::-1]
507
+ for i in range(len(line_nums)): # Iterate from last to first
508
+ # Make sure we don't completely remove the last line
509
+ if len(self.lines) == 1:
510
+ cut_buffer.append(self.lines[0])
511
+ self.lines[0] = Line()
512
+ break
513
+ # Get the current line
514
+ line_no = line_nums[i]
515
+ # Get and remove the line
516
+ line = self.lines.pop(line_no)
517
+ # Put it in our temporary buffer
518
+ cut_buffer.append(line)
519
+ # Move all cursors below the current line up
520
+ self.move_y_cursors(line_no, -1)
521
+ self.move_cursors() # Make sure cursors are in valid places
522
+ # Reverse the buffer to get correct order and store it
523
+ self.set_buffer(cut_buffer[::-1])
524
+ self.store_action_state("cut")
525
+
526
+ def type(self, data):
527
+ """Insert data at each cursor position."""
528
+ for cursor in self.cursors:
529
+ self.type_at_cursor(cursor, data)
530
+ self.move_cursors()
531
+ # Add a restore point if previous action != type
532
+ self.store_action_state("type")
533
+
534
+ def type_at_cursor(self, cursor, data):
535
+ """Insert data at specified cursor."""
536
+ line = self.lines[cursor.y]
537
+ start = line[:cursor.x]
538
+ end = line[cursor.x:]
539
+ self.lines[cursor.y].set_data(start + data + end)
540
+ self.move_x_cursors(cursor.y, cursor.x, len(data))
541
+ cursor.move_right(len(data))
542
+
543
+ def go_to_pos(self, line_no, col=0):
544
+ """Move primary cursor to line_no, col=0."""
545
+ if line_no < 0:
546
+ line_no = len(self.lines)-1
547
+ else:
548
+ line_no = line_no-1
549
+
550
+ self.store_state()
551
+ cur = self.get_cursor()
552
+ if col is not None:
553
+ cur.x = col
554
+ cur.y = line_no
555
+ if cur.y >= len(self.lines):
556
+ cur.y = len(self.lines)-1
557
+ self.scroll_to_line(cur.y)
558
+ self.move_cursors()
559
+
560
+ def duplicate_line(self):
561
+ """Copy current line and add it below as a new line."""
562
+ curs = sorted(self.cursors, key=lambda c: (c.y, c.x))
563
+ for cursor in curs:
564
+ line = Line(self.lines[cursor.y])
565
+ self.lines.insert(cursor.y+1, line)
566
+ self.move_y_cursors(cursor.y, 1)
567
+ self.move_cursors()
568
+ self.store_action_state("duplicate_line")