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/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")
|