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