mui 0.2.0 → 0.3.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 +13 -8
- data/CHANGELOG.md +99 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +301 -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 +149 -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 +78 -2
- data/lib/mui/handler_result.rb +13 -7
- data/lib/mui/highlighters/search_highlighter.rb +2 -1
- data/lib/mui/highlighters/syntax_highlighter.rb +3 -1
- 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 +152 -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 +24 -6
- data/lib/mui/search_state.rb +61 -28
- data/lib/mui/syntax/language_detector.rb +33 -1
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
- data/lib/mui/terminal_adapter/curses.rb +13 -11
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +83 -40
- 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
data/lib/mui/editor.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Mui
|
|
|
4
4
|
# Main editor class that coordinates all components
|
|
5
5
|
class Editor
|
|
6
6
|
attr_reader :tab_manager, :undo_manager, :autocmd, :command_registry, :job_manager, :color_scheme, :floating_window,
|
|
7
|
-
:insert_completion_state
|
|
7
|
+
:insert_completion_state, :key_sequence_handler
|
|
8
8
|
attr_accessor :message, :running
|
|
9
9
|
|
|
10
10
|
def initialize(file_path = nil, adapter: TerminalAdapter::Curses.new, load_config: true)
|
|
@@ -46,12 +46,17 @@ module Mui
|
|
|
46
46
|
# Load plugin autocmds
|
|
47
47
|
load_plugin_autocmds
|
|
48
48
|
|
|
49
|
+
# Initialize key sequence handler for multi-key mappings
|
|
50
|
+
@key_sequence_handler = KeySequenceHandler.new(Mui.config)
|
|
51
|
+
@key_sequence_handler.rebuild_keymaps
|
|
52
|
+
|
|
49
53
|
@mode_manager = ModeManager.new(
|
|
50
54
|
window: @tab_manager,
|
|
51
55
|
buffer: @buffer,
|
|
52
56
|
command_line: @command_line,
|
|
53
57
|
undo_manager: @undo_manager,
|
|
54
|
-
editor: self
|
|
58
|
+
editor: self,
|
|
59
|
+
key_sequence_handler: @key_sequence_handler
|
|
55
60
|
)
|
|
56
61
|
|
|
57
62
|
# Trigger BufEnter event
|
|
@@ -85,6 +90,7 @@ module Mui
|
|
|
85
90
|
def run
|
|
86
91
|
while @running
|
|
87
92
|
process_job_results
|
|
93
|
+
check_key_sequence_timeout
|
|
88
94
|
update_window_size
|
|
89
95
|
render
|
|
90
96
|
key = @input.read_nonblock
|
|
@@ -108,6 +114,42 @@ module Mui
|
|
|
108
114
|
window_manager.split_horizontal(scratch_buffer)
|
|
109
115
|
end
|
|
110
116
|
|
|
117
|
+
# Update existing scratch buffer or create new one
|
|
118
|
+
def update_or_create_scratch_buffer(name, content)
|
|
119
|
+
existing = find_scratch_buffer(name)
|
|
120
|
+
|
|
121
|
+
if existing
|
|
122
|
+
existing.content = content
|
|
123
|
+
focus_buffer(existing)
|
|
124
|
+
else
|
|
125
|
+
open_scratch_buffer(name, content)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Find a scratch buffer by name across all tabs and windows
|
|
130
|
+
def find_scratch_buffer(name)
|
|
131
|
+
@tab_manager.tabs.each do |tab|
|
|
132
|
+
tab.window_manager.windows.each do |win|
|
|
133
|
+
return win.buffer if win.buffer.name == name && win.buffer.readonly?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Focus a specific buffer by switching to its window
|
|
140
|
+
def focus_buffer(target_buffer)
|
|
141
|
+
@tab_manager.tabs.each_with_index do |tab, tab_index|
|
|
142
|
+
tab.window_manager.windows.each do |win|
|
|
143
|
+
next unless win.buffer == target_buffer
|
|
144
|
+
|
|
145
|
+
@tab_manager.go_to(tab_index)
|
|
146
|
+
tab.window_manager.focus_window(win)
|
|
147
|
+
return true
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
111
153
|
# Suspend UI for running external interactive commands (e.g., fzf)
|
|
112
154
|
def suspend_ui
|
|
113
155
|
@adapter.suspend
|
|
@@ -315,5 +357,39 @@ module Mui
|
|
|
315
357
|
def process_job_results
|
|
316
358
|
@job_manager.poll
|
|
317
359
|
end
|
|
360
|
+
|
|
361
|
+
def check_key_sequence_timeout
|
|
362
|
+
return unless @key_sequence_handler.pending?
|
|
363
|
+
|
|
364
|
+
mode_symbol = mode_to_symbol(@mode_manager.mode)
|
|
365
|
+
result = @key_sequence_handler.check_timeout(mode_symbol)
|
|
366
|
+
|
|
367
|
+
return unless result
|
|
368
|
+
|
|
369
|
+
type, data = result
|
|
370
|
+
case type
|
|
371
|
+
when KeySequenceHandler::RESULT_HANDLED
|
|
372
|
+
execute_keymap_handler(data)
|
|
373
|
+
when KeySequenceHandler::RESULT_PASSTHROUGH
|
|
374
|
+
# Re-process the passthrough key
|
|
375
|
+
handle_key(data) if data
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def mode_to_symbol(mode)
|
|
380
|
+
case mode
|
|
381
|
+
when Mode::NORMAL then :normal
|
|
382
|
+
when Mode::INSERT then :insert
|
|
383
|
+
when Mode::VISUAL, Mode::VISUAL_LINE then :visual
|
|
384
|
+
when Mode::COMMAND then :command
|
|
385
|
+
when Mode::SEARCH_FORWARD, Mode::SEARCH_BACKWARD then :search
|
|
386
|
+
else :normal
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def execute_keymap_handler(handler)
|
|
391
|
+
context = CommandContext.new(editor: self, buffer:, window:)
|
|
392
|
+
handler.call(context)
|
|
393
|
+
end
|
|
318
394
|
end
|
|
319
395
|
end
|
data/lib/mui/handler_result.rb
CHANGED
|
@@ -6,10 +6,11 @@ module Mui
|
|
|
6
6
|
class Base
|
|
7
7
|
attr_reader :mode, :message
|
|
8
8
|
|
|
9
|
-
def initialize(mode: nil, message: nil, quit: false)
|
|
9
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false)
|
|
10
10
|
@mode = mode
|
|
11
11
|
@message = message
|
|
12
12
|
@quit = quit
|
|
13
|
+
@pending_sequence = pending_sequence
|
|
13
14
|
freeze
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -17,6 +18,11 @@ module Mui
|
|
|
17
18
|
@quit
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# True when waiting for more keys in a multi-key sequence
|
|
22
|
+
def pending_sequence?
|
|
23
|
+
@pending_sequence
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def start_selection?
|
|
21
27
|
false
|
|
22
28
|
end
|
|
@@ -36,11 +42,11 @@ module Mui
|
|
|
36
42
|
|
|
37
43
|
# Result for NormalMode - handles visual mode start
|
|
38
44
|
class NormalModeResult < Base
|
|
39
|
-
def initialize(mode: nil, message: nil, quit: false, start_selection: false, line_mode: false, group_started: false)
|
|
45
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, start_selection: false, line_mode: false, group_started: false)
|
|
40
46
|
@start_selection = start_selection
|
|
41
47
|
@line_mode = line_mode
|
|
42
48
|
@group_started = group_started
|
|
43
|
-
super(mode:, message:, quit:)
|
|
49
|
+
super(mode:, message:, quit:, pending_sequence:)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
def start_selection?
|
|
@@ -58,11 +64,11 @@ module Mui
|
|
|
58
64
|
|
|
59
65
|
# Result for VisualMode - handles selection clear and line mode toggle
|
|
60
66
|
class VisualModeResult < Base
|
|
61
|
-
def initialize(mode: nil, message: nil, quit: false, clear_selection: false, toggle_line_mode: false, group_started: false)
|
|
67
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, clear_selection: false, toggle_line_mode: false, group_started: false)
|
|
62
68
|
@clear_selection = clear_selection
|
|
63
69
|
@toggle_line_mode = toggle_line_mode
|
|
64
70
|
@group_started = group_started
|
|
65
|
-
super(mode:, message:, quit:)
|
|
71
|
+
super(mode:, message:, quit:, pending_sequence:)
|
|
66
72
|
end
|
|
67
73
|
|
|
68
74
|
def clear_selection?
|
|
@@ -88,9 +94,9 @@ module Mui
|
|
|
88
94
|
|
|
89
95
|
# Result for SearchMode - handles search execution
|
|
90
96
|
class SearchModeResult < Base
|
|
91
|
-
def initialize(mode: nil, message: nil, quit: false, cancelled: false)
|
|
97
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, cancelled: false)
|
|
92
98
|
@cancelled = cancelled
|
|
93
|
-
super(mode:, message:, quit:)
|
|
99
|
+
super(mode:, message:, quit:, pending_sequence:)
|
|
94
100
|
end
|
|
95
101
|
|
|
96
102
|
def cancelled?
|
|
@@ -5,9 +5,10 @@ module Mui
|
|
|
5
5
|
class SearchHighlighter < Base
|
|
6
6
|
def highlights_for(row, _line, options = {})
|
|
7
7
|
search_state = options[:search_state]
|
|
8
|
+
buffer = options[:buffer]
|
|
8
9
|
return [] unless search_state&.has_pattern?
|
|
9
10
|
|
|
10
|
-
matches = search_state.matches_for_row(row)
|
|
11
|
+
matches = search_state.matches_for_row(row, buffer:)
|
|
11
12
|
matches.map do |match|
|
|
12
13
|
Highlight.new(
|
|
13
14
|
start_col: match[:col],
|
|
@@ -19,7 +19,9 @@ module Mui
|
|
|
19
19
|
instance_variable: :syntax_instance_variable,
|
|
20
20
|
global_variable: :syntax_global_variable,
|
|
21
21
|
method_call: :syntax_method_call,
|
|
22
|
-
type: :syntax_type
|
|
22
|
+
type: :syntax_type,
|
|
23
|
+
macro: :syntax_keyword, # Rust macros (println!, vec!, etc.)
|
|
24
|
+
regex: :syntax_string # JavaScript/TypeScript regex literals
|
|
23
25
|
}.freeze
|
|
24
26
|
|
|
25
27
|
def initialize(color_scheme, buffer: nil)
|
data/lib/mui/key_handler/base.rb
CHANGED
|
@@ -27,13 +27,100 @@ module Mui
|
|
|
27
27
|
@mode_manager&.editor
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Returns the current buffer's undo_manager for dynamic access
|
|
31
|
+
# This ensures undo/redo works correctly when buffer changes (e.g., via :e)
|
|
32
|
+
def undo_manager
|
|
33
|
+
buffer&.undo_manager
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Access to key sequence handler for multi-key mappings
|
|
37
|
+
def key_sequence_handler
|
|
38
|
+
@mode_manager&.key_sequence_handler
|
|
39
|
+
end
|
|
40
|
+
|
|
30
41
|
# Handle a key input
|
|
31
42
|
def handle(_key)
|
|
32
43
|
raise MethodNotOverriddenError, "Subclasses must orverride #handle"
|
|
33
44
|
end
|
|
34
45
|
|
|
46
|
+
# Check plugin keymap with multi-key sequence support
|
|
47
|
+
# @param key [Integer, String] Raw key input
|
|
48
|
+
# @param mode_symbol [Symbol] Mode symbol (:normal, :insert, :visual, :command)
|
|
49
|
+
# @return [HandlerResult, nil] Result if handled or pending, nil to continue with built-in
|
|
50
|
+
def check_plugin_keymap(key, mode_symbol)
|
|
51
|
+
return nil unless key_sequence_handler
|
|
52
|
+
|
|
53
|
+
type, data = key_sequence_handler.process(key, mode_symbol)
|
|
54
|
+
|
|
55
|
+
case type
|
|
56
|
+
when KeySequenceHandler::RESULT_HANDLED
|
|
57
|
+
execute_plugin_handler(data, mode_symbol)
|
|
58
|
+
when KeySequenceHandler::RESULT_PENDING
|
|
59
|
+
# Return a result that tells the handler to wait
|
|
60
|
+
# Use base class directly since subclasses may not support pending_sequence
|
|
61
|
+
HandlerResult::Base.new(pending_sequence: true)
|
|
62
|
+
when KeySequenceHandler::RESULT_PASSTHROUGH
|
|
63
|
+
# Check for single-key keymap (backward compatibility)
|
|
64
|
+
check_single_key_keymap(data, mode_symbol)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
35
68
|
private
|
|
36
69
|
|
|
70
|
+
# Check single-key keymap for backward compatibility
|
|
71
|
+
def check_single_key_keymap(key, mode_symbol)
|
|
72
|
+
key_str = convert_key_to_string(key)
|
|
73
|
+
return nil unless key_str
|
|
74
|
+
|
|
75
|
+
plugin_handler = Mui.config.keymaps[mode_symbol]&.[](key_str)
|
|
76
|
+
return nil unless plugin_handler
|
|
77
|
+
|
|
78
|
+
execute_plugin_handler(plugin_handler, mode_symbol)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Execute a plugin handler and wrap result
|
|
82
|
+
def execute_plugin_handler(handler, mode_symbol)
|
|
83
|
+
return nil unless @mode_manager&.editor
|
|
84
|
+
|
|
85
|
+
context = CommandContext.new(
|
|
86
|
+
editor: @mode_manager.editor,
|
|
87
|
+
buffer:,
|
|
88
|
+
window:
|
|
89
|
+
)
|
|
90
|
+
handler_result = handler.call(context)
|
|
91
|
+
|
|
92
|
+
return nil unless handler_result
|
|
93
|
+
|
|
94
|
+
wrap_handler_result(handler_result, mode_symbol)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Wrap handler result in appropriate type
|
|
98
|
+
def wrap_handler_result(handler_result, _mode_symbol)
|
|
99
|
+
return handler_result if handler_result.is_a?(HandlerResult::Base)
|
|
100
|
+
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Convert key to string for keymap lookup
|
|
105
|
+
def convert_key_to_string(key)
|
|
106
|
+
return key if key.is_a?(String)
|
|
107
|
+
|
|
108
|
+
case key
|
|
109
|
+
when KeyCode::ENTER_CR, KeyCode::ENTER_LF
|
|
110
|
+
"\r"
|
|
111
|
+
when KeyCode::ESCAPE
|
|
112
|
+
"\e"
|
|
113
|
+
when KeyCode::TAB
|
|
114
|
+
"\t"
|
|
115
|
+
when KeyCode::BACKSPACE
|
|
116
|
+
"\x7f"
|
|
117
|
+
else
|
|
118
|
+
key.chr
|
|
119
|
+
end
|
|
120
|
+
rescue RangeError
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
37
124
|
def cursor_row
|
|
38
125
|
window.cursor_row
|
|
39
126
|
end
|
|
@@ -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
|