mui 0.2.0 → 0.4.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 +18 -10
- data/CHANGELOG.md +162 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +314 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +155 -0
- data/lib/mui/color_manager.rb +140 -6
- data/lib/mui/color_scheme.rb +1 -0
- data/lib/mui/command_completer.rb +11 -2
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +32 -2
- data/lib/mui/command_registry.rb +21 -2
- data/lib/mui/config.rb +3 -1
- data/lib/mui/editor.rb +90 -2
- data/lib/mui/floating_window.rb +53 -1
- data/lib/mui/handler_result.rb +13 -7
- data/lib/mui/highlighters/search_highlighter.rb +2 -1
- data/lib/mui/highlighters/syntax_highlighter.rb +4 -1
- data/lib/mui/insert_completion_state.rb +15 -2
- data/lib/mui/key_handler/base.rb +87 -0
- data/lib/mui/key_handler/command_mode.rb +68 -0
- data/lib/mui/key_handler/insert_mode.rb +10 -41
- data/lib/mui/key_handler/normal_mode.rb +24 -51
- data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
- data/lib/mui/key_handler/search_mode.rb +10 -7
- data/lib/mui/key_handler/visual_mode.rb +15 -10
- data/lib/mui/key_notation_parser.rb +159 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -0
- data/lib/mui/line_renderer.rb +52 -1
- data/lib/mui/mode_manager.rb +3 -2
- data/lib/mui/screen.rb +30 -6
- data/lib/mui/search_state.rb +74 -27
- data/lib/mui/syntax/language_detector.rb +33 -1
- data/lib/mui/syntax/lexers/c_lexer.rb +2 -0
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +207 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +219 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +3 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +150 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +225 -0
- data/lib/mui/terminal_adapter/base.rb +21 -0
- data/lib/mui/terminal_adapter/curses.rb +37 -11
- data/lib/mui/themes/default.rb +263 -132
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +105 -39
- data/lib/mui/window_manager.rb +7 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +15 -0
- metadata +26 -3
|
@@ -15,6 +15,10 @@ module Mui
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def handle(key)
|
|
18
|
+
# Check plugin keymaps first
|
|
19
|
+
plugin_result = check_plugin_keymap(key, :command)
|
|
20
|
+
return plugin_result if plugin_result
|
|
21
|
+
|
|
18
22
|
case key
|
|
19
23
|
when KeyCode::ESCAPE
|
|
20
24
|
handle_escape
|
|
@@ -30,6 +34,10 @@ module Mui
|
|
|
30
34
|
handle_cursor_left
|
|
31
35
|
when Curses::KEY_RIGHT
|
|
32
36
|
handle_cursor_right
|
|
37
|
+
when Curses::KEY_UP
|
|
38
|
+
handle_history_up
|
|
39
|
+
when Curses::KEY_DOWN
|
|
40
|
+
handle_history_down
|
|
33
41
|
else
|
|
34
42
|
handle_character_input(key)
|
|
35
43
|
end
|
|
@@ -82,6 +90,18 @@ module Mui
|
|
|
82
90
|
result
|
|
83
91
|
end
|
|
84
92
|
|
|
93
|
+
def handle_history_up
|
|
94
|
+
@completion_state.reset
|
|
95
|
+
@command_line.history_previous
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_history_down
|
|
100
|
+
@completion_state.reset
|
|
101
|
+
@command_line.history_next
|
|
102
|
+
result
|
|
103
|
+
end
|
|
104
|
+
|
|
85
105
|
def update_completion
|
|
86
106
|
context = @command_line.completion_context
|
|
87
107
|
unless context
|
|
@@ -176,6 +196,10 @@ module Mui
|
|
|
176
196
|
handle_tab_move(command_result[:position])
|
|
177
197
|
when :goto_line
|
|
178
198
|
handle_goto_line(command_result[:line_number])
|
|
199
|
+
when :shell_command
|
|
200
|
+
handle_shell_command(command_result[:command])
|
|
201
|
+
when :shell_command_error
|
|
202
|
+
result(message: command_result[:message])
|
|
179
203
|
when :unknown
|
|
180
204
|
# Check plugin commands before reporting unknown
|
|
181
205
|
plugin_result = try_plugin_command(command_result[:command])
|
|
@@ -264,7 +288,9 @@ module Mui
|
|
|
264
288
|
|
|
265
289
|
def open_new_buffer(path)
|
|
266
290
|
new_buffer = create_buffer_from_path(path)
|
|
291
|
+
new_buffer.undo_manager = UndoManager.new
|
|
267
292
|
window.buffer = new_buffer
|
|
293
|
+
|
|
268
294
|
result(message: "\"#{path}\" opened")
|
|
269
295
|
rescue SystemCallError => e
|
|
270
296
|
result(message: "Error: #{e.message}")
|
|
@@ -303,6 +329,7 @@ module Mui
|
|
|
303
329
|
def handle_split_horizontal(path = nil)
|
|
304
330
|
with_window_manager do |wm|
|
|
305
331
|
buffer = path ? create_buffer_from_path(path) : nil
|
|
332
|
+
buffer&.undo_manager = UndoManager.new
|
|
306
333
|
wm.split_horizontal(buffer)
|
|
307
334
|
result
|
|
308
335
|
end
|
|
@@ -311,6 +338,7 @@ module Mui
|
|
|
311
338
|
def handle_split_vertical(path = nil)
|
|
312
339
|
with_window_manager do |wm|
|
|
313
340
|
buffer = path ? create_buffer_from_path(path) : nil
|
|
341
|
+
buffer&.undo_manager = UndoManager.new
|
|
314
342
|
wm.split_vertical(buffer)
|
|
315
343
|
result
|
|
316
344
|
end
|
|
@@ -352,6 +380,7 @@ module Mui
|
|
|
352
380
|
with_tab_manager do |tm|
|
|
353
381
|
new_tab = tm.add
|
|
354
382
|
buffer = path ? create_buffer_from_path(path) : Buffer.new
|
|
383
|
+
buffer.undo_manager = UndoManager.new
|
|
355
384
|
new_tab.window_manager.add_window(buffer)
|
|
356
385
|
result
|
|
357
386
|
end
|
|
@@ -424,6 +453,45 @@ module Mui
|
|
|
424
453
|
result
|
|
425
454
|
end
|
|
426
455
|
|
|
456
|
+
def handle_shell_command(cmd)
|
|
457
|
+
return result(message: "Shell commands not available") unless @mode_manager&.editor
|
|
458
|
+
|
|
459
|
+
context = CommandContext.new(
|
|
460
|
+
editor: @mode_manager.editor,
|
|
461
|
+
buffer:,
|
|
462
|
+
window:
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
context.run_shell_command(cmd, on_complete: lambda { |job|
|
|
466
|
+
display_shell_result(job, cmd)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
result(message: "Running: #{cmd}")
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def display_shell_result(job, cmd)
|
|
473
|
+
return unless @mode_manager&.editor
|
|
474
|
+
|
|
475
|
+
output = build_shell_output(job, cmd)
|
|
476
|
+
@mode_manager.editor.update_or_create_scratch_buffer("[Shell Output]", output)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def build_shell_output(job, cmd)
|
|
480
|
+
res = job.result
|
|
481
|
+
lines = ["$ #{cmd}", ""]
|
|
482
|
+
|
|
483
|
+
lines << res[:stdout].chomp unless res[:stdout].empty?
|
|
484
|
+
|
|
485
|
+
unless res[:stderr].empty?
|
|
486
|
+
lines << "[stderr]"
|
|
487
|
+
lines << res[:stderr].chomp
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
lines << "" << "[Exit status: #{res[:exit_status]}]" unless res[:success]
|
|
491
|
+
|
|
492
|
+
lines.join("\n")
|
|
493
|
+
end
|
|
494
|
+
|
|
427
495
|
def create_buffer_from_path(path)
|
|
428
496
|
new_buffer = Buffer.new
|
|
429
497
|
new_buffer.load(path)
|
|
@@ -8,7 +8,8 @@ module Mui
|
|
|
8
8
|
super(mode_manager, buffer)
|
|
9
9
|
@undo_manager = undo_manager
|
|
10
10
|
# Start undo group unless already started (e.g., by change operator)
|
|
11
|
-
|
|
11
|
+
# Use dynamic undo_manager to support buffer changes via :e/:sp/:vs/:tabnew
|
|
12
|
+
self.undo_manager&.begin_group unless group_started
|
|
12
13
|
# Build word cache for fast completion (use active window's buffer)
|
|
13
14
|
@word_cache = BufferWordCache.new(self.buffer)
|
|
14
15
|
end
|
|
@@ -44,51 +45,13 @@ module Mui
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
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
48
|
private
|
|
86
49
|
|
|
87
50
|
def handle_escape
|
|
88
51
|
# Cancel completion if active
|
|
89
52
|
editor.insert_completion_state.reset if completion_active?
|
|
90
53
|
|
|
91
|
-
|
|
54
|
+
undo_manager&.end_group
|
|
92
55
|
# Remove trailing whitespace from current line if it's whitespace-only (Vim behavior)
|
|
93
56
|
stripped = strip_trailing_whitespace_if_empty_line
|
|
94
57
|
# Move cursor back one position unless we just stripped whitespace (cursor already at 0)
|
|
@@ -107,11 +70,13 @@ module Mui
|
|
|
107
70
|
end
|
|
108
71
|
|
|
109
72
|
def handle_move_left
|
|
73
|
+
reset_insert_completion_state
|
|
110
74
|
self.cursor_col = cursor_col - 1 if cursor_col.positive?
|
|
111
75
|
result
|
|
112
76
|
end
|
|
113
77
|
|
|
114
78
|
def handle_move_right
|
|
79
|
+
reset_insert_completion_state
|
|
115
80
|
self.cursor_col = cursor_col + 1 if cursor_col < current_line_length
|
|
116
81
|
result
|
|
117
82
|
end
|
|
@@ -134,7 +99,7 @@ module Mui
|
|
|
134
99
|
update_completion_list if completion_active?
|
|
135
100
|
elsif cursor_row.positive?
|
|
136
101
|
join_with_previous_line
|
|
137
|
-
|
|
102
|
+
reset_insert_completion_state
|
|
138
103
|
end
|
|
139
104
|
result
|
|
140
105
|
end
|
|
@@ -346,6 +311,10 @@ module Mui
|
|
|
346
311
|
end
|
|
347
312
|
end
|
|
348
313
|
|
|
314
|
+
def reset_insert_completion_state
|
|
315
|
+
editor.insert_completion_state.reset if completion_active?
|
|
316
|
+
end
|
|
317
|
+
|
|
349
318
|
def result(mode: nil, message: nil, quit: false)
|
|
350
319
|
HandlerResult::InsertModeResult.new(mode:, message:, quit:)
|
|
351
320
|
end
|
|
@@ -21,8 +21,9 @@ module Mui
|
|
|
21
21
|
# Sync operators with current buffer/window (may have changed via tab switch)
|
|
22
22
|
sync_operators
|
|
23
23
|
|
|
24
|
-
# Check plugin keymaps first
|
|
25
|
-
|
|
24
|
+
# Check plugin keymaps first
|
|
25
|
+
# Skip if: has pending motion OR (is builtin key AND no pending sequence)
|
|
26
|
+
unless @pending_motion || (builtin_operator_key?(key) && !pending_sequence?)
|
|
26
27
|
plugin_result = check_plugin_keymap(key, :normal)
|
|
27
28
|
return plugin_result if plugin_result
|
|
28
29
|
end
|
|
@@ -34,49 +35,22 @@ module Mui
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return nil unless key_str
|
|
42
|
-
|
|
43
|
-
plugin_handler = Mui.config.keymaps[mode_symbol]&.[](key_str)
|
|
44
|
-
return nil unless plugin_handler
|
|
45
|
-
|
|
46
|
-
context = CommandContext.new(
|
|
47
|
-
editor: @mode_manager.editor,
|
|
48
|
-
buffer:,
|
|
49
|
-
window:
|
|
50
|
-
)
|
|
51
|
-
handler_result = plugin_handler.call(context)
|
|
38
|
+
# Check if there's a pending multi-key sequence in progress
|
|
39
|
+
def pending_sequence?
|
|
40
|
+
key_sequence_handler&.pending?
|
|
41
|
+
end
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
# Check if key starts a built-in operator (takes priority over plugins)
|
|
44
|
+
def builtin_operator_key?(key)
|
|
45
|
+
char = key_to_char(key)
|
|
46
|
+
return false unless char
|
|
56
47
|
|
|
57
|
-
#
|
|
58
|
-
|
|
48
|
+
# Built-in operators and motions that set pending_motion
|
|
49
|
+
%w[d c y g f F t T].include?(char) || key == KeyCode::CTRL_W
|
|
59
50
|
end
|
|
60
51
|
|
|
61
52
|
private
|
|
62
53
|
|
|
63
|
-
# Convert key to string for keymap lookup
|
|
64
|
-
# Handles special keys like Enter that have Curses constants
|
|
65
|
-
def convert_key_to_string(key)
|
|
66
|
-
return key if key.is_a?(String)
|
|
67
|
-
|
|
68
|
-
# Handle special Curses keys
|
|
69
|
-
case key
|
|
70
|
-
when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
|
|
71
|
-
"\r"
|
|
72
|
-
else
|
|
73
|
-
key.chr
|
|
74
|
-
end
|
|
75
|
-
rescue RangeError
|
|
76
|
-
# Key code out of char range (e.g., special function keys)
|
|
77
|
-
nil
|
|
78
|
-
end
|
|
79
|
-
|
|
80
54
|
def initialize_operators
|
|
81
55
|
@operators = {
|
|
82
56
|
delete: Operators::DeleteOperator.new(
|
|
@@ -95,7 +69,7 @@ module Mui
|
|
|
95
69
|
end
|
|
96
70
|
|
|
97
71
|
def sync_operators
|
|
98
|
-
@operators.each_value { |op| op.update(buffer:, window:) }
|
|
72
|
+
@operators.each_value { |op| op.update(buffer:, window:, undo_manager:) }
|
|
99
73
|
end
|
|
100
74
|
|
|
101
75
|
def handle_normal_key(key)
|
|
@@ -298,7 +272,7 @@ module Mui
|
|
|
298
272
|
end
|
|
299
273
|
|
|
300
274
|
def handle_open_below
|
|
301
|
-
|
|
275
|
+
undo_manager&.begin_group
|
|
302
276
|
buffer.insert_line(cursor_row + 1)
|
|
303
277
|
self.cursor_row = cursor_row + 1
|
|
304
278
|
self.cursor_col = 0
|
|
@@ -306,7 +280,7 @@ module Mui
|
|
|
306
280
|
end
|
|
307
281
|
|
|
308
282
|
def handle_open_above
|
|
309
|
-
|
|
283
|
+
undo_manager&.begin_group
|
|
310
284
|
buffer.insert_line(cursor_row)
|
|
311
285
|
self.cursor_col = 0
|
|
312
286
|
result(mode: Mode::INSERT, group_started: true)
|
|
@@ -410,7 +384,7 @@ module Mui
|
|
|
410
384
|
|
|
411
385
|
# Undo/Redo handlers
|
|
412
386
|
def handle_undo
|
|
413
|
-
if
|
|
387
|
+
if undo_manager&.undo(buffer)
|
|
414
388
|
window.clamp_cursor_to_line(buffer)
|
|
415
389
|
result
|
|
416
390
|
else
|
|
@@ -419,7 +393,7 @@ module Mui
|
|
|
419
393
|
end
|
|
420
394
|
|
|
421
395
|
def handle_redo
|
|
422
|
-
if
|
|
396
|
+
if undo_manager&.redo(buffer)
|
|
423
397
|
window.clamp_cursor_to_line(buffer)
|
|
424
398
|
result
|
|
425
399
|
else
|
|
@@ -500,9 +474,9 @@ module Mui
|
|
|
500
474
|
return result(message: "No previous search pattern") unless @search_state&.has_pattern?
|
|
501
475
|
|
|
502
476
|
match = if @search_state.direction == :forward
|
|
503
|
-
@search_state.find_next(cursor_row, cursor_col)
|
|
477
|
+
@search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
504
478
|
else
|
|
505
|
-
@search_state.find_previous(cursor_row, cursor_col)
|
|
479
|
+
@search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
506
480
|
end
|
|
507
481
|
|
|
508
482
|
if match
|
|
@@ -517,9 +491,9 @@ module Mui
|
|
|
517
491
|
return result(message: "No previous search pattern") unless @search_state&.has_pattern?
|
|
518
492
|
|
|
519
493
|
match = if @search_state.direction == :forward
|
|
520
|
-
@search_state.find_previous(cursor_row, cursor_col)
|
|
494
|
+
@search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
521
495
|
else
|
|
522
|
-
@search_state.find_next(cursor_row, cursor_col)
|
|
496
|
+
@search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
523
497
|
end
|
|
524
498
|
|
|
525
499
|
if match
|
|
@@ -538,13 +512,12 @@ module Mui
|
|
|
538
512
|
escaped_pattern = "\\b#{Regexp.escape(word)}\\b"
|
|
539
513
|
|
|
540
514
|
@search_state.set_pattern(escaped_pattern, direction)
|
|
541
|
-
@search_state.find_all_matches(buffer)
|
|
542
515
|
|
|
543
516
|
# Find next/previous match from current position
|
|
544
517
|
match = if direction == :forward
|
|
545
|
-
@search_state.find_next(cursor_row, cursor_col)
|
|
518
|
+
@search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
546
519
|
else
|
|
547
|
-
@search_state.find_previous(cursor_row, cursor_col)
|
|
520
|
+
@search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
548
521
|
end
|
|
549
522
|
|
|
550
523
|
if match
|
|
@@ -37,6 +37,7 @@ module Mui
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
39
|
def paste_line_after(name: nil)
|
|
40
|
+
undo_manager&.begin_group
|
|
40
41
|
text = @register.get(name:)
|
|
41
42
|
lines = text.split("\n", -1)
|
|
42
43
|
lines.reverse_each do |line|
|
|
@@ -44,15 +45,18 @@ module Mui
|
|
|
44
45
|
end
|
|
45
46
|
self.cursor_row = cursor_row + 1
|
|
46
47
|
self.cursor_col = 0
|
|
48
|
+
undo_manager&.end_group
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def paste_line_before(name: nil)
|
|
52
|
+
undo_manager&.begin_group
|
|
50
53
|
text = @register.get(name:)
|
|
51
54
|
lines = text.split("\n", -1)
|
|
52
55
|
lines.reverse_each do |line|
|
|
53
56
|
@buffer.insert_line(cursor_row, line)
|
|
54
57
|
end
|
|
55
58
|
self.cursor_col = 0
|
|
59
|
+
undo_manager&.end_group
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
def paste_char_after(name: nil)
|
|
@@ -63,7 +67,7 @@ module Mui
|
|
|
63
67
|
if text.include?("\n")
|
|
64
68
|
paste_multiline_char(text, line, insert_col)
|
|
65
69
|
else
|
|
66
|
-
@buffer.
|
|
70
|
+
@buffer.replace_line(cursor_row, line[0...insert_col].to_s + text + line[insert_col..].to_s)
|
|
67
71
|
self.cursor_col = insert_col + text.length - 1
|
|
68
72
|
@window.clamp_cursor_to_line(@buffer)
|
|
69
73
|
end
|
|
@@ -76,19 +80,20 @@ module Mui
|
|
|
76
80
|
if text.include?("\n")
|
|
77
81
|
paste_multiline_char(text, line, cursor_col)
|
|
78
82
|
else
|
|
79
|
-
@buffer.
|
|
83
|
+
@buffer.replace_line(cursor_row, line[0...cursor_col].to_s + text + line[cursor_col..].to_s)
|
|
80
84
|
self.cursor_col = cursor_col + text.length - 1
|
|
81
85
|
@window.clamp_cursor_to_line(@buffer)
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
def paste_multiline_char(text, line, insert_col)
|
|
90
|
+
undo_manager&.begin_group
|
|
86
91
|
lines = text.split("\n", -1)
|
|
87
92
|
before = line[0...insert_col].to_s
|
|
88
93
|
after = line[insert_col..].to_s
|
|
89
94
|
|
|
90
95
|
# First line: before + first part of pasted text
|
|
91
|
-
@buffer.
|
|
96
|
+
@buffer.replace_line(cursor_row, before + lines.first)
|
|
92
97
|
|
|
93
98
|
# Middle lines: insert as new lines
|
|
94
99
|
lines[1...-1].each_with_index do |pasted_line, idx|
|
|
@@ -106,6 +111,7 @@ module Mui
|
|
|
106
111
|
self.cursor_col = lines.last.length - 1
|
|
107
112
|
self.cursor_col = 0 if cursor_col.negative?
|
|
108
113
|
@window.clamp_cursor_to_line(@buffer)
|
|
114
|
+
undo_manager&.end_group
|
|
109
115
|
end
|
|
110
116
|
end
|
|
111
117
|
end
|
|
@@ -23,6 +23,10 @@ module Mui
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def handle(key)
|
|
26
|
+
# Check plugin keymaps first
|
|
27
|
+
plugin_result = check_plugin_keymap(key, :search)
|
|
28
|
+
return plugin_result if plugin_result
|
|
29
|
+
|
|
26
30
|
case key
|
|
27
31
|
when KeyCode::ESCAPE
|
|
28
32
|
handle_escape
|
|
@@ -136,19 +140,19 @@ module Mui
|
|
|
136
140
|
|
|
137
141
|
direction = @search_input.prompt == "/" ? :forward : :backward
|
|
138
142
|
@search_state.set_pattern(pattern, direction)
|
|
139
|
-
@search_state.find_all_matches(buffer)
|
|
140
143
|
|
|
141
144
|
# Move cursor to first match from original position
|
|
142
|
-
|
|
145
|
+
matches = @search_state.find_all_matches(buffer)
|
|
146
|
+
return if matches.empty?
|
|
143
147
|
|
|
144
148
|
# Use original position if set, otherwise use current cursor position
|
|
145
149
|
search_row = @original_cursor_row || cursor_row
|
|
146
150
|
search_col = @original_cursor_col || cursor_col
|
|
147
151
|
|
|
148
152
|
match = if direction == :forward
|
|
149
|
-
@search_state.find_next(search_row, search_col)
|
|
153
|
+
@search_state.find_next(search_row, search_col, buffer:)
|
|
150
154
|
else
|
|
151
|
-
@search_state.find_previous(search_row, search_col)
|
|
155
|
+
@search_state.find_previous(search_row, search_col, buffer:)
|
|
152
156
|
end
|
|
153
157
|
|
|
154
158
|
return unless match
|
|
@@ -163,12 +167,11 @@ module Mui
|
|
|
163
167
|
|
|
164
168
|
direction = @search_input.prompt == "/" ? :forward : :backward
|
|
165
169
|
@search_state.set_pattern(pattern, direction)
|
|
166
|
-
@search_state.find_all_matches(@buffer)
|
|
167
170
|
|
|
168
171
|
match = if direction == :forward
|
|
169
|
-
@search_state.find_next(cursor_row, cursor_col)
|
|
172
|
+
@search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
170
173
|
else
|
|
171
|
-
@search_state.find_previous(cursor_row, cursor_col)
|
|
174
|
+
@search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
172
175
|
end
|
|
173
176
|
|
|
174
177
|
if match
|
|
@@ -18,6 +18,12 @@ module Mui
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def handle(key)
|
|
21
|
+
# Check plugin keymaps first (only when no pending motion)
|
|
22
|
+
unless @pending_motion
|
|
23
|
+
plugin_result = check_plugin_keymap(key, :visual)
|
|
24
|
+
return plugin_result if plugin_result
|
|
25
|
+
end
|
|
26
|
+
|
|
21
27
|
if @pending_motion
|
|
22
28
|
handle_pending_motion(key)
|
|
23
29
|
else
|
|
@@ -120,7 +126,7 @@ module Mui
|
|
|
120
126
|
if @selection.line_mode
|
|
121
127
|
change_lines(range)
|
|
122
128
|
else
|
|
123
|
-
|
|
129
|
+
undo_manager&.begin_group
|
|
124
130
|
change_range(range)
|
|
125
131
|
end
|
|
126
132
|
@pending_register = nil
|
|
@@ -154,16 +160,15 @@ module Mui
|
|
|
154
160
|
# Escape special regex characters for literal search
|
|
155
161
|
escaped_pattern = Regexp.escape(text)
|
|
156
162
|
|
|
157
|
-
# Set search state
|
|
163
|
+
# Set search state
|
|
158
164
|
search_state = @mode_manager.search_state
|
|
159
165
|
search_state.set_pattern(escaped_pattern, direction)
|
|
160
|
-
search_state.find_all_matches(buffer)
|
|
161
166
|
|
|
162
167
|
# Find next/previous match from current position
|
|
163
168
|
match = if direction == :forward
|
|
164
|
-
search_state.find_next(cursor_row, cursor_col)
|
|
169
|
+
search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
165
170
|
else
|
|
166
|
-
search_state.find_previous(cursor_row, cursor_col)
|
|
171
|
+
search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
167
172
|
end
|
|
168
173
|
|
|
169
174
|
if match
|
|
@@ -207,7 +212,7 @@ module Mui
|
|
|
207
212
|
def change_lines(range)
|
|
208
213
|
lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
|
|
209
214
|
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
210
|
-
|
|
215
|
+
undo_manager&.begin_group
|
|
211
216
|
(range[:end_row] - range[:start_row] + 1).times do
|
|
212
217
|
buffer.delete_line(range[:start_row])
|
|
213
218
|
end
|
|
@@ -229,11 +234,11 @@ module Mui
|
|
|
229
234
|
def delete_lines(range)
|
|
230
235
|
lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
|
|
231
236
|
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
232
|
-
|
|
237
|
+
undo_manager&.begin_group unless undo_manager&.in_group?
|
|
233
238
|
(range[:end_row] - range[:start_row] + 1).times do
|
|
234
239
|
buffer.delete_line(range[:start_row])
|
|
235
240
|
end
|
|
236
|
-
|
|
241
|
+
undo_manager&.end_group
|
|
237
242
|
self.cursor_row = [range[:start_row], buffer.line_count - 1].min
|
|
238
243
|
self.cursor_col = 0
|
|
239
244
|
window.clamp_cursor_to_line(buffer)
|
|
@@ -268,7 +273,7 @@ module Mui
|
|
|
268
273
|
def indent_lines(start_row, end_row, direction)
|
|
269
274
|
indent_string = build_indent_string
|
|
270
275
|
|
|
271
|
-
|
|
276
|
+
undo_manager&.begin_group unless undo_manager&.in_group?
|
|
272
277
|
|
|
273
278
|
(start_row..end_row).each do |row|
|
|
274
279
|
if direction == :right
|
|
@@ -278,7 +283,7 @@ module Mui
|
|
|
278
283
|
end
|
|
279
284
|
end
|
|
280
285
|
|
|
281
|
-
|
|
286
|
+
undo_manager&.end_group
|
|
282
287
|
end
|
|
283
288
|
|
|
284
289
|
def build_indent_string
|