mui 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +158 -0
- data/CHANGELOG.md +349 -0
- data/exe/mui +1 -2
- data/lib/mui/autocmd.rb +66 -0
- data/lib/mui/buffer.rb +275 -0
- data/lib/mui/buffer_word_cache.rb +131 -0
- data/lib/mui/buffer_word_completer.rb +77 -0
- data/lib/mui/color_manager.rb +136 -0
- data/lib/mui/color_scheme.rb +63 -0
- data/lib/mui/command_completer.rb +21 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_line.rb +137 -0
- data/lib/mui/command_registry.rb +25 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +56 -0
- data/lib/mui/editor.rb +319 -0
- data/lib/mui/error.rb +29 -0
- data/lib/mui/file_completer.rb +51 -0
- data/lib/mui/floating_window.rb +161 -0
- data/lib/mui/handler_result.rb +101 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +26 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +105 -0
- data/lib/mui/input.rb +17 -0
- data/lib/mui/insert_completion_renderer.rb +92 -0
- data/lib/mui/insert_completion_state.rb +77 -0
- data/lib/mui/job.rb +81 -0
- data/lib/mui/job_manager.rb +113 -0
- data/lib/mui/key_code.rb +30 -0
- data/lib/mui/key_handler/base.rb +100 -0
- data/lib/mui/key_handler/command_mode.rb +443 -0
- data/lib/mui/key_handler/insert_mode.rb +354 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +579 -0
- data/lib/mui/key_handler/operators/base_operator.rb +134 -0
- data/lib/mui/key_handler/operators/change_operator.rb +179 -0
- data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
- data/lib/mui/key_handler/operators/paste_operator.rb +113 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +188 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +397 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -0
- data/lib/mui/layout/calculator.rb +15 -0
- data/lib/mui/layout/leaf_node.rb +33 -0
- data/lib/mui/layout/node.rb +29 -0
- data/lib/mui/layout/split_node.rb +132 -0
- data/lib/mui/line_renderer.rb +122 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +185 -0
- data/lib/mui/motion.rb +139 -0
- data/lib/mui/plugin.rb +35 -0
- data/lib/mui/plugin_manager.rb +106 -0
- data/lib/mui/register.rb +110 -0
- data/lib/mui/screen.rb +85 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +88 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +74 -0
- data/lib/mui/syntax/lexer_base.rb +106 -0
- data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
- data/lib/mui/syntax/token.rb +42 -0
- data/lib/mui/syntax/token_cache.rb +91 -0
- data/lib/mui/tab_bar_renderer.rb +87 -0
- data/lib/mui/tab_manager.rb +96 -0
- data/lib/mui/tab_page.rb +35 -0
- data/lib/mui/terminal_adapter/base.rb +92 -0
- data/lib/mui/terminal_adapter/curses.rb +162 -0
- data/lib/mui/terminal_adapter.rb +4 -0
- data/lib/mui/themes/default.rb +315 -0
- data/lib/mui/undo_manager.rb +83 -0
- data/lib/mui/undoable_action.rb +175 -0
- data/lib/mui/unicode_width.rb +100 -0
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +158 -0
- data/lib/mui/window_manager.rb +249 -0
- data/lib/mui.rb +156 -2
- metadata +98 -3
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
# Handles key inputs in Insert mode
|
|
6
|
+
class InsertMode < Base
|
|
7
|
+
def initialize(mode_manager, buffer, undo_manager: nil, group_started: false)
|
|
8
|
+
super(mode_manager, buffer)
|
|
9
|
+
@undo_manager = undo_manager
|
|
10
|
+
# Start undo group unless already started (e.g., by change operator)
|
|
11
|
+
@undo_manager&.begin_group unless group_started
|
|
12
|
+
# Build word cache for fast completion (use active window's buffer)
|
|
13
|
+
@word_cache = BufferWordCache.new(self.buffer)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def handle(key)
|
|
17
|
+
# Check plugin keymaps first
|
|
18
|
+
plugin_result = check_plugin_keymap(key, :insert)
|
|
19
|
+
return plugin_result if plugin_result
|
|
20
|
+
|
|
21
|
+
case key
|
|
22
|
+
when KeyCode::ESCAPE
|
|
23
|
+
handle_escape
|
|
24
|
+
when Curses::KEY_LEFT
|
|
25
|
+
handle_move_left
|
|
26
|
+
when Curses::KEY_RIGHT
|
|
27
|
+
handle_move_right
|
|
28
|
+
when Curses::KEY_UP
|
|
29
|
+
completion_active? ? handle_completion_previous : handle_move_up
|
|
30
|
+
when Curses::KEY_DOWN
|
|
31
|
+
completion_active? ? handle_completion_next : handle_move_down
|
|
32
|
+
when KeyCode::CTRL_P
|
|
33
|
+
completion_active? ? handle_completion_previous : handle_buffer_completion
|
|
34
|
+
when KeyCode::CTRL_N
|
|
35
|
+
completion_active? ? handle_completion_next : handle_buffer_completion
|
|
36
|
+
when KeyCode::TAB
|
|
37
|
+
completion_active? ? handle_completion_confirm : handle_tab
|
|
38
|
+
when KeyCode::BACKSPACE, Curses::KEY_BACKSPACE
|
|
39
|
+
handle_backspace
|
|
40
|
+
when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
|
|
41
|
+
handle_enter
|
|
42
|
+
else
|
|
43
|
+
handle_character_input(key)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_plugin_keymap(key, mode_symbol)
|
|
48
|
+
return nil unless @mode_manager&.editor
|
|
49
|
+
|
|
50
|
+
key_str = convert_key_to_string(key)
|
|
51
|
+
return nil unless key_str
|
|
52
|
+
|
|
53
|
+
plugin_handler = Mui.config.keymaps[mode_symbol]&.[](key_str)
|
|
54
|
+
return nil unless plugin_handler
|
|
55
|
+
|
|
56
|
+
context = CommandContext.new(
|
|
57
|
+
editor: @mode_manager.editor,
|
|
58
|
+
buffer:,
|
|
59
|
+
window:
|
|
60
|
+
)
|
|
61
|
+
handler_result = plugin_handler.call(context)
|
|
62
|
+
|
|
63
|
+
# If handler returns nil/false, let built-in handle it
|
|
64
|
+
return nil unless handler_result
|
|
65
|
+
|
|
66
|
+
# Return a valid result to indicate the key was handled
|
|
67
|
+
handler_result.is_a?(HandlerResult::InsertModeResult) ? handler_result : result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def convert_key_to_string(key)
|
|
71
|
+
return key if key.is_a?(String)
|
|
72
|
+
|
|
73
|
+
# Handle special Curses keys
|
|
74
|
+
case key
|
|
75
|
+
when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
|
|
76
|
+
"\r"
|
|
77
|
+
else
|
|
78
|
+
key.chr
|
|
79
|
+
end
|
|
80
|
+
rescue RangeError
|
|
81
|
+
# Key code out of char range (e.g., special function keys)
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def handle_escape
|
|
88
|
+
# Cancel completion if active
|
|
89
|
+
editor.insert_completion_state.reset if completion_active?
|
|
90
|
+
|
|
91
|
+
@undo_manager&.end_group
|
|
92
|
+
# Remove trailing whitespace from current line if it's whitespace-only (Vim behavior)
|
|
93
|
+
stripped = strip_trailing_whitespace_if_empty_line
|
|
94
|
+
# Move cursor back one position unless we just stripped whitespace (cursor already at 0)
|
|
95
|
+
self.cursor_col = cursor_col - 1 if cursor_col.positive? && !stripped
|
|
96
|
+
result(mode: Mode::NORMAL)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def strip_trailing_whitespace_if_empty_line
|
|
100
|
+
line = buffer.line(cursor_row)
|
|
101
|
+
return false unless line.match?(/\A[ \t]+\z/)
|
|
102
|
+
|
|
103
|
+
# Line contains only whitespace, clear it
|
|
104
|
+
line.length.times { buffer.delete_char(cursor_row, 0) }
|
|
105
|
+
self.cursor_col = 0
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def handle_move_left
|
|
110
|
+
self.cursor_col = cursor_col - 1 if cursor_col.positive?
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_move_right
|
|
115
|
+
self.cursor_col = cursor_col + 1 if cursor_col < current_line_length
|
|
116
|
+
result
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_move_up
|
|
120
|
+
window.move_up
|
|
121
|
+
result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle_move_down
|
|
125
|
+
window.move_down
|
|
126
|
+
result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def handle_backspace
|
|
130
|
+
if cursor_col.positive?
|
|
131
|
+
self.cursor_col = cursor_col - 1
|
|
132
|
+
buffer.delete_char(cursor_row, cursor_col)
|
|
133
|
+
# Update completion list after backspace
|
|
134
|
+
update_completion_list if completion_active?
|
|
135
|
+
elsif cursor_row.positive?
|
|
136
|
+
join_with_previous_line
|
|
137
|
+
editor.insert_completion_state.reset if completion_active?
|
|
138
|
+
end
|
|
139
|
+
result
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def join_with_previous_line
|
|
143
|
+
prev_line_len = buffer.line(cursor_row - 1).length
|
|
144
|
+
buffer.join_lines(cursor_row - 1)
|
|
145
|
+
self.cursor_row = cursor_row - 1
|
|
146
|
+
self.cursor_col = prev_line_len
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def handle_enter
|
|
150
|
+
# Get indent from current line before splitting
|
|
151
|
+
current_line = buffer.line(cursor_row)
|
|
152
|
+
indent = extract_indent(current_line)
|
|
153
|
+
|
|
154
|
+
buffer.split_line(cursor_row, cursor_col)
|
|
155
|
+
self.cursor_row = cursor_row + 1
|
|
156
|
+
|
|
157
|
+
# Insert indent at the beginning of the new line
|
|
158
|
+
if indent && !indent.empty?
|
|
159
|
+
indent.each_char.with_index do |char, i|
|
|
160
|
+
buffer.insert_char(cursor_row, i, char)
|
|
161
|
+
end
|
|
162
|
+
self.cursor_col = indent.length
|
|
163
|
+
else
|
|
164
|
+
self.cursor_col = 0
|
|
165
|
+
end
|
|
166
|
+
result
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def extract_indent(line)
|
|
170
|
+
match = line.match(/\A([ \t]*)/)
|
|
171
|
+
match ? match[1] : ""
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def handle_character_input(key)
|
|
175
|
+
char = extract_printable_char(key)
|
|
176
|
+
if char
|
|
177
|
+
buffer.insert_char(cursor_row, cursor_col, char)
|
|
178
|
+
self.cursor_col = cursor_col + 1
|
|
179
|
+
|
|
180
|
+
# Update completion list if active
|
|
181
|
+
if completion_active? && word_char?(char)
|
|
182
|
+
update_completion_list
|
|
183
|
+
elsif completion_active?
|
|
184
|
+
# Non-word character typed, close completion and add completed word to cache
|
|
185
|
+
add_current_word_to_cache
|
|
186
|
+
editor.insert_completion_state.reset
|
|
187
|
+
trigger_completion_for(char)
|
|
188
|
+
else
|
|
189
|
+
# Non-word char means previous word is complete, add to cache
|
|
190
|
+
add_current_word_to_cache unless word_char?(char)
|
|
191
|
+
# Trigger completion for certain characters
|
|
192
|
+
trigger_completion_for(char)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
result
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def add_current_word_to_cache
|
|
199
|
+
# Get the word that just ended (before current cursor)
|
|
200
|
+
line = buffer.line(cursor_row)
|
|
201
|
+
return if cursor_col < 2
|
|
202
|
+
|
|
203
|
+
# Find the word that just ended
|
|
204
|
+
end_col = cursor_col - 1
|
|
205
|
+
start_col = end_col
|
|
206
|
+
start_col -= 1 while start_col.positive? && word_char?(line[start_col - 1])
|
|
207
|
+
|
|
208
|
+
word = line[start_col...end_col]
|
|
209
|
+
@word_cache.add_word(word) if word && word.length >= BufferWordCache::MIN_WORD_LENGTH
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def update_completion_list
|
|
213
|
+
return unless editor
|
|
214
|
+
|
|
215
|
+
new_prefix = @word_cache.prefix_at(cursor_row, cursor_col)
|
|
216
|
+
|
|
217
|
+
if new_prefix.empty?
|
|
218
|
+
editor.insert_completion_state.reset
|
|
219
|
+
return
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
editor.insert_completion_state.update_prefix(new_prefix)
|
|
223
|
+
|
|
224
|
+
# Close completion if no matches remain
|
|
225
|
+
editor.insert_completion_state.reset unless editor.insert_completion_state.active?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def trigger_completion_for(char)
|
|
229
|
+
return unless editor
|
|
230
|
+
|
|
231
|
+
# LSP completion triggers
|
|
232
|
+
if %w[. @].include?(char) || (char == ":" && previous_char == ":")
|
|
233
|
+
editor.trigger_autocmd(:InsertCompletion)
|
|
234
|
+
return
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Buffer word completion - trigger after typing word characters
|
|
238
|
+
trigger_buffer_completion_if_needed(min_prefix: 1) if word_char?(char)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def trigger_buffer_completion_if_needed(min_prefix: 3)
|
|
242
|
+
prefix = @word_cache.prefix_at(cursor_row, cursor_col)
|
|
243
|
+
|
|
244
|
+
return if prefix.length < min_prefix
|
|
245
|
+
|
|
246
|
+
# Mark current row as dirty to exclude word at cursor
|
|
247
|
+
@word_cache.mark_dirty(cursor_row)
|
|
248
|
+
candidates = @word_cache.complete(prefix, cursor_row, cursor_col)
|
|
249
|
+
return if candidates.empty?
|
|
250
|
+
|
|
251
|
+
# Format candidates for InsertCompletionState
|
|
252
|
+
items = candidates.map do |word|
|
|
253
|
+
{
|
|
254
|
+
label: word,
|
|
255
|
+
insert_text: word
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
editor.start_insert_completion(items, prefix:)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def word_char?(char)
|
|
263
|
+
char&.match?(/\w/)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def previous_char
|
|
267
|
+
return nil if cursor_col < 2
|
|
268
|
+
|
|
269
|
+
buffer.line(cursor_row)[cursor_col - 2]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def handle_buffer_completion
|
|
273
|
+
return result unless editor
|
|
274
|
+
|
|
275
|
+
# For manual trigger (Ctrl+N/P), allow 1+ character prefix
|
|
276
|
+
trigger_buffer_completion_if_needed(min_prefix: 1)
|
|
277
|
+
result
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def handle_tab
|
|
281
|
+
buffer.insert_char(cursor_row, cursor_col, "\t")
|
|
282
|
+
self.cursor_col = cursor_col + 1
|
|
283
|
+
result
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def completion_active?
|
|
287
|
+
editor&.insert_completion_active?
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def handle_completion_next
|
|
291
|
+
editor.insert_completion_state.select_next
|
|
292
|
+
result
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def handle_completion_previous
|
|
296
|
+
editor.insert_completion_state.select_previous
|
|
297
|
+
result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def handle_completion_confirm
|
|
301
|
+
state = editor.insert_completion_state
|
|
302
|
+
return result unless state.current_item
|
|
303
|
+
|
|
304
|
+
insert_text = state.insert_text
|
|
305
|
+
text_edit_range = state.text_edit_range
|
|
306
|
+
|
|
307
|
+
if text_edit_range
|
|
308
|
+
# Use textEdit range for precise replacement
|
|
309
|
+
apply_text_edit(insert_text, text_edit_range)
|
|
310
|
+
else
|
|
311
|
+
# Fallback to prefix-based replacement
|
|
312
|
+
apply_prefix_replacement(insert_text, state.prefix)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
state.reset
|
|
316
|
+
result
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def apply_text_edit(insert_text, range)
|
|
320
|
+
start_char = range.dig(:start, :character) || range.dig("start", "character")
|
|
321
|
+
end_char = range.dig(:end, :character) || range.dig("end", "character")
|
|
322
|
+
|
|
323
|
+
# Delete from start to end
|
|
324
|
+
delete_count = end_char - start_char
|
|
325
|
+
self.cursor_col = start_char
|
|
326
|
+
delete_count.times { buffer.delete_char(cursor_row, cursor_col) }
|
|
327
|
+
|
|
328
|
+
# Insert new text
|
|
329
|
+
insert_text.each_char do |c|
|
|
330
|
+
buffer.insert_char(cursor_row, cursor_col, c)
|
|
331
|
+
self.cursor_col = cursor_col + 1
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def apply_prefix_replacement(insert_text, prefix)
|
|
336
|
+
# Delete prefix
|
|
337
|
+
prefix.length.times do
|
|
338
|
+
self.cursor_col = cursor_col - 1
|
|
339
|
+
buffer.delete_char(cursor_row, cursor_col)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Insert completion text
|
|
343
|
+
insert_text.each_char do |c|
|
|
344
|
+
buffer.insert_char(cursor_row, cursor_col, c)
|
|
345
|
+
self.cursor_col = cursor_col + 1
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def result(mode: nil, message: nil, quit: false)
|
|
350
|
+
HandlerResult::InsertModeResult.new(mode:, message:, quit:)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
module Motions
|
|
6
|
+
# Shared motion handling for NormalMode and VisualMode
|
|
7
|
+
# Provides common handle_* methods that delegate to Motion module
|
|
8
|
+
module MotionHandler
|
|
9
|
+
def handle_word_forward
|
|
10
|
+
apply_motion(Motion.word_forward(buffer, cursor_row, cursor_col))
|
|
11
|
+
result
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def handle_word_backward
|
|
15
|
+
apply_motion(Motion.word_backward(buffer, cursor_row, cursor_col))
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def handle_word_end
|
|
20
|
+
apply_motion(Motion.word_end(buffer, cursor_row, cursor_col))
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handle_line_start
|
|
25
|
+
apply_motion(Motion.line_start(buffer, cursor_row, cursor_col))
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle_first_non_blank
|
|
30
|
+
apply_motion(Motion.first_non_blank(buffer, cursor_row, cursor_col))
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handle_line_end
|
|
35
|
+
apply_motion(Motion.line_end(buffer, cursor_row, cursor_col))
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def handle_file_end
|
|
40
|
+
apply_motion(Motion.file_end(buffer, cursor_row, cursor_col))
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def apply_motion(motion_result)
|
|
47
|
+
return unless motion_result
|
|
48
|
+
|
|
49
|
+
self.cursor_row = motion_result[:row]
|
|
50
|
+
self.cursor_col = motion_result[:col]
|
|
51
|
+
window.clamp_cursor_to_line(buffer)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|