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,443 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
# Handles key inputs in Command mode
|
|
6
|
+
class CommandMode < Base
|
|
7
|
+
attr_reader :completion_state
|
|
8
|
+
|
|
9
|
+
def initialize(mode_manager, buffer, command_line)
|
|
10
|
+
super(mode_manager, buffer)
|
|
11
|
+
@command_line = command_line
|
|
12
|
+
@completion_state = CompletionState.new
|
|
13
|
+
@command_completer = CommandCompleter.new
|
|
14
|
+
@file_completer = FileCompleter.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handle(key)
|
|
18
|
+
case key
|
|
19
|
+
when KeyCode::ESCAPE
|
|
20
|
+
handle_escape
|
|
21
|
+
when KeyCode::BACKSPACE, Curses::KEY_BACKSPACE
|
|
22
|
+
handle_backspace
|
|
23
|
+
when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
|
|
24
|
+
handle_enter
|
|
25
|
+
when KeyCode::TAB
|
|
26
|
+
handle_tab
|
|
27
|
+
when Curses::KEY_BTAB
|
|
28
|
+
handle_shift_tab
|
|
29
|
+
when Curses::KEY_LEFT
|
|
30
|
+
handle_cursor_left
|
|
31
|
+
when Curses::KEY_RIGHT
|
|
32
|
+
handle_cursor_right
|
|
33
|
+
else
|
|
34
|
+
handle_character_input(key)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def handle_escape
|
|
41
|
+
@completion_state.reset
|
|
42
|
+
@command_line.clear
|
|
43
|
+
result(mode: Mode::NORMAL)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_backspace
|
|
47
|
+
if @command_line.buffer.empty?
|
|
48
|
+
@completion_state.reset
|
|
49
|
+
result(mode: Mode::NORMAL)
|
|
50
|
+
else
|
|
51
|
+
@command_line.backspace
|
|
52
|
+
update_completion
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_tab
|
|
58
|
+
return result unless @completion_state.active?
|
|
59
|
+
|
|
60
|
+
@completion_state.select_next if @completion_state.confirmed?
|
|
61
|
+
apply_current_completion
|
|
62
|
+
@completion_state.confirm
|
|
63
|
+
result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_shift_tab
|
|
67
|
+
return result unless @completion_state.active?
|
|
68
|
+
|
|
69
|
+
@completion_state.select_previous if @completion_state.confirmed?
|
|
70
|
+
apply_current_completion
|
|
71
|
+
@completion_state.confirm
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_cursor_left
|
|
76
|
+
@command_line.move_cursor_left
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_cursor_right
|
|
81
|
+
@command_line.move_cursor_right
|
|
82
|
+
result
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update_completion
|
|
86
|
+
context = @command_line.completion_context
|
|
87
|
+
unless context
|
|
88
|
+
@completion_state.reset
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
candidates = case context[:type]
|
|
93
|
+
when :command
|
|
94
|
+
@command_completer.complete(context[:prefix])
|
|
95
|
+
when :file
|
|
96
|
+
@file_completer.complete(context[:prefix])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if candidates.nil? || candidates.empty?
|
|
100
|
+
@completion_state.reset
|
|
101
|
+
else
|
|
102
|
+
@completion_state.start(candidates, @command_line.buffer, context[:type])
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def apply_current_completion
|
|
107
|
+
candidate = @completion_state.current_candidate
|
|
108
|
+
return unless candidate
|
|
109
|
+
|
|
110
|
+
context = @command_line.completion_context
|
|
111
|
+
return unless context
|
|
112
|
+
|
|
113
|
+
@command_line.apply_completion(candidate, context)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def handle_enter
|
|
117
|
+
command_result = @command_line.execute
|
|
118
|
+
action_result = execute_action(command_result)
|
|
119
|
+
HandlerResult::CommandModeResult.new(
|
|
120
|
+
mode: Mode::NORMAL,
|
|
121
|
+
message: action_result.message,
|
|
122
|
+
quit: action_result.quit?
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_character_input(key)
|
|
127
|
+
char = extract_printable_char(key)
|
|
128
|
+
if char
|
|
129
|
+
@command_line.input(char)
|
|
130
|
+
update_completion
|
|
131
|
+
end
|
|
132
|
+
result
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def execute_action(command_result)
|
|
136
|
+
case command_result[:action]
|
|
137
|
+
when :no_op
|
|
138
|
+
result
|
|
139
|
+
when :open
|
|
140
|
+
handle_open
|
|
141
|
+
when :open_as
|
|
142
|
+
open_buffer(command_result[:path])
|
|
143
|
+
when :write
|
|
144
|
+
handle_write
|
|
145
|
+
when :quit
|
|
146
|
+
handle_quit
|
|
147
|
+
when :write_quit
|
|
148
|
+
handle_write_quit
|
|
149
|
+
when :force_quit
|
|
150
|
+
handle_force_quit
|
|
151
|
+
when :write_as
|
|
152
|
+
save_buffer(command_result[:path])
|
|
153
|
+
when :split_horizontal
|
|
154
|
+
handle_split_horizontal(command_result[:path])
|
|
155
|
+
when :split_vertical
|
|
156
|
+
handle_split_vertical(command_result[:path])
|
|
157
|
+
when :close_window
|
|
158
|
+
handle_close_window
|
|
159
|
+
when :only_window
|
|
160
|
+
handle_only_window
|
|
161
|
+
when :tab_new
|
|
162
|
+
handle_tab_new(command_result[:path])
|
|
163
|
+
when :tab_close
|
|
164
|
+
handle_tab_close
|
|
165
|
+
when :tab_next
|
|
166
|
+
handle_tab_next
|
|
167
|
+
when :tab_prev
|
|
168
|
+
handle_tab_prev
|
|
169
|
+
when :tab_first
|
|
170
|
+
handle_tab_first
|
|
171
|
+
when :tab_last
|
|
172
|
+
handle_tab_last
|
|
173
|
+
when :tab_go
|
|
174
|
+
handle_tab_go(command_result[:index])
|
|
175
|
+
when :tab_move
|
|
176
|
+
handle_tab_move(command_result[:position])
|
|
177
|
+
when :goto_line
|
|
178
|
+
handle_goto_line(command_result[:line_number])
|
|
179
|
+
when :unknown
|
|
180
|
+
# Check plugin commands before reporting unknown
|
|
181
|
+
plugin_result = try_plugin_command(command_result[:command])
|
|
182
|
+
plugin_result || result(message: "Unknown command: #{command_result[:command]}")
|
|
183
|
+
else
|
|
184
|
+
result
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def try_plugin_command(command_str)
|
|
189
|
+
return nil unless @mode_manager&.editor
|
|
190
|
+
|
|
191
|
+
parts = command_str.to_s.split(/\s+/, 2)
|
|
192
|
+
cmd_name = parts[0]
|
|
193
|
+
args = parts[1]
|
|
194
|
+
|
|
195
|
+
plugin_command = Mui.config.commands[cmd_name.to_sym]
|
|
196
|
+
return nil unless plugin_command
|
|
197
|
+
|
|
198
|
+
context = CommandContext.new(
|
|
199
|
+
editor: @mode_manager.editor,
|
|
200
|
+
buffer:,
|
|
201
|
+
window:
|
|
202
|
+
)
|
|
203
|
+
plugin_command.call(context, args)
|
|
204
|
+
result
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def handle_open
|
|
208
|
+
open_buffer
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def handle_write
|
|
212
|
+
return result(mode: Mode::NORMAL, message: "E21: Cannot write readonly buffer") if buffer.readonly?
|
|
213
|
+
|
|
214
|
+
save_buffer
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def handle_quit
|
|
218
|
+
return result(message: "No write since last change (add ! to override)") if buffer.modified
|
|
219
|
+
|
|
220
|
+
close_or_quit
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def handle_write_quit
|
|
224
|
+
return result(mode: Mode::NORMAL, message: "E21: Cannot write readonly buffer") if buffer.readonly?
|
|
225
|
+
|
|
226
|
+
save_result = save_buffer
|
|
227
|
+
return save_result if save_result.message && !save_result.message.include?("written")
|
|
228
|
+
|
|
229
|
+
close_or_quit(message: save_result.message)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def handle_force_quit
|
|
233
|
+
close_or_quit
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def close_or_quit(message: nil)
|
|
237
|
+
with_window_manager do |wm|
|
|
238
|
+
# If multiple windows in current tab, close window
|
|
239
|
+
unless wm.single_window?
|
|
240
|
+
wm.close_current_window
|
|
241
|
+
return result(message:)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Single window in current tab - check if we have multiple tabs
|
|
245
|
+
tab_manager = @mode_manager&.editor&.tab_manager
|
|
246
|
+
if tab_manager && !tab_manager.single_tab?
|
|
247
|
+
tab_manager.close_current
|
|
248
|
+
return result(message:)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Single window, single tab - quit editor
|
|
252
|
+
return result(message:, quit: true)
|
|
253
|
+
end
|
|
254
|
+
result(message:, quit: true)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def open_buffer(path = nil)
|
|
258
|
+
if path
|
|
259
|
+
open_new_buffer(path)
|
|
260
|
+
else
|
|
261
|
+
reload_current_buffer
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def open_new_buffer(path)
|
|
266
|
+
new_buffer = create_buffer_from_path(path)
|
|
267
|
+
window.buffer = new_buffer
|
|
268
|
+
result(message: "\"#{path}\" opened")
|
|
269
|
+
rescue SystemCallError => e
|
|
270
|
+
result(message: "Error: #{e.message}")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def reload_current_buffer
|
|
274
|
+
target_path = buffer.name
|
|
275
|
+
return result(message: "No file name") if target_path.nil? || target_path == "[No Name]"
|
|
276
|
+
|
|
277
|
+
buffer.load(target_path)
|
|
278
|
+
result(message: "File reopened")
|
|
279
|
+
rescue SystemCallError => e
|
|
280
|
+
result(message: "Error: #{e.message}")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def save_buffer(path = nil)
|
|
284
|
+
# Trigger BufWritePre before saving
|
|
285
|
+
@mode_manager.editor&.trigger_autocmd(:BufWritePre)
|
|
286
|
+
|
|
287
|
+
if path
|
|
288
|
+
buffer.save(path)
|
|
289
|
+
elsif buffer.name == "[No Name]"
|
|
290
|
+
return result(message: "No file name")
|
|
291
|
+
else
|
|
292
|
+
buffer.save
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Trigger BufWritePost after saving
|
|
296
|
+
@mode_manager.editor&.trigger_autocmd(:BufWritePost)
|
|
297
|
+
|
|
298
|
+
result(message: "\"#{buffer.name}\" written")
|
|
299
|
+
rescue SystemCallError => e
|
|
300
|
+
result(message: "Error: #{e.message}")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def handle_split_horizontal(path = nil)
|
|
304
|
+
with_window_manager do |wm|
|
|
305
|
+
buffer = path ? create_buffer_from_path(path) : nil
|
|
306
|
+
wm.split_horizontal(buffer)
|
|
307
|
+
result
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def handle_split_vertical(path = nil)
|
|
312
|
+
with_window_manager do |wm|
|
|
313
|
+
buffer = path ? create_buffer_from_path(path) : nil
|
|
314
|
+
wm.split_vertical(buffer)
|
|
315
|
+
result
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def handle_close_window
|
|
320
|
+
with_window_manager do |wm|
|
|
321
|
+
if wm.single_window?
|
|
322
|
+
result(message: "Cannot close last window")
|
|
323
|
+
else
|
|
324
|
+
wm.close_current_window
|
|
325
|
+
result
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def handle_only_window
|
|
331
|
+
with_window_manager do |wm|
|
|
332
|
+
wm.close_all_except_current
|
|
333
|
+
result
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def with_window_manager
|
|
338
|
+
wm = @mode_manager&.window_manager
|
|
339
|
+
return result(message: "Window commands not available") unless wm
|
|
340
|
+
|
|
341
|
+
yield wm
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def with_tab_manager
|
|
345
|
+
tm = @mode_manager&.editor&.tab_manager
|
|
346
|
+
return result(message: "Tab commands not available") unless tm
|
|
347
|
+
|
|
348
|
+
yield tm
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def handle_tab_new(path = nil)
|
|
352
|
+
with_tab_manager do |tm|
|
|
353
|
+
new_tab = tm.add
|
|
354
|
+
buffer = path ? create_buffer_from_path(path) : Buffer.new
|
|
355
|
+
new_tab.window_manager.add_window(buffer)
|
|
356
|
+
result
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def handle_tab_close
|
|
361
|
+
with_tab_manager do |tm|
|
|
362
|
+
if tm.single_tab?
|
|
363
|
+
result(message: "Cannot close last tab")
|
|
364
|
+
else
|
|
365
|
+
tm.close_current
|
|
366
|
+
result
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def handle_tab_next
|
|
372
|
+
with_tab_manager do |tm|
|
|
373
|
+
tm.next_tab
|
|
374
|
+
result
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def handle_tab_prev
|
|
379
|
+
with_tab_manager do |tm|
|
|
380
|
+
tm.prev_tab
|
|
381
|
+
result
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def handle_tab_first
|
|
386
|
+
with_tab_manager do |tm|
|
|
387
|
+
tm.first_tab
|
|
388
|
+
result
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def handle_tab_last
|
|
393
|
+
with_tab_manager do |tm|
|
|
394
|
+
tm.last_tab
|
|
395
|
+
result
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def handle_tab_go(index)
|
|
400
|
+
with_tab_manager do |tm|
|
|
401
|
+
if tm.go_to(index)
|
|
402
|
+
result
|
|
403
|
+
else
|
|
404
|
+
result(message: "Invalid tab index")
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def handle_tab_move(position)
|
|
410
|
+
with_tab_manager do |tm|
|
|
411
|
+
tm.move_tab(position)
|
|
412
|
+
result
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def handle_goto_line(line_number)
|
|
417
|
+
# Convert 1-indexed to 0-indexed, clamp to valid range
|
|
418
|
+
target_row = (line_number - 1).clamp(0, buffer.line_count - 1)
|
|
419
|
+
|
|
420
|
+
window.cursor_row = target_row
|
|
421
|
+
window.cursor_col = 0
|
|
422
|
+
window.ensure_cursor_visible
|
|
423
|
+
|
|
424
|
+
result
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def create_buffer_from_path(path)
|
|
428
|
+
new_buffer = Buffer.new
|
|
429
|
+
new_buffer.load(path)
|
|
430
|
+
new_buffer
|
|
431
|
+
rescue SystemCallError
|
|
432
|
+
# File doesn't exist yet, create new buffer with the name
|
|
433
|
+
new_buffer = Buffer.new
|
|
434
|
+
new_buffer.name = path
|
|
435
|
+
new_buffer
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def result(mode: nil, message: nil, quit: false)
|
|
439
|
+
HandlerResult::CommandModeResult.new(mode:, message:, quit:)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|