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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +158 -0
  3. data/CHANGELOG.md +349 -0
  4. data/exe/mui +1 -2
  5. data/lib/mui/autocmd.rb +66 -0
  6. data/lib/mui/buffer.rb +275 -0
  7. data/lib/mui/buffer_word_cache.rb +131 -0
  8. data/lib/mui/buffer_word_completer.rb +77 -0
  9. data/lib/mui/color_manager.rb +136 -0
  10. data/lib/mui/color_scheme.rb +63 -0
  11. data/lib/mui/command_completer.rb +21 -0
  12. data/lib/mui/command_context.rb +90 -0
  13. data/lib/mui/command_line.rb +137 -0
  14. data/lib/mui/command_registry.rb +25 -0
  15. data/lib/mui/completion_renderer.rb +84 -0
  16. data/lib/mui/completion_state.rb +58 -0
  17. data/lib/mui/config.rb +56 -0
  18. data/lib/mui/editor.rb +319 -0
  19. data/lib/mui/error.rb +29 -0
  20. data/lib/mui/file_completer.rb +51 -0
  21. data/lib/mui/floating_window.rb +161 -0
  22. data/lib/mui/handler_result.rb +101 -0
  23. data/lib/mui/highlight.rb +22 -0
  24. data/lib/mui/highlighters/base.rb +23 -0
  25. data/lib/mui/highlighters/search_highlighter.rb +26 -0
  26. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  27. data/lib/mui/highlighters/syntax_highlighter.rb +105 -0
  28. data/lib/mui/input.rb +17 -0
  29. data/lib/mui/insert_completion_renderer.rb +92 -0
  30. data/lib/mui/insert_completion_state.rb +77 -0
  31. data/lib/mui/job.rb +81 -0
  32. data/lib/mui/job_manager.rb +113 -0
  33. data/lib/mui/key_code.rb +30 -0
  34. data/lib/mui/key_handler/base.rb +100 -0
  35. data/lib/mui/key_handler/command_mode.rb +443 -0
  36. data/lib/mui/key_handler/insert_mode.rb +354 -0
  37. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  38. data/lib/mui/key_handler/normal_mode.rb +579 -0
  39. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  40. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  41. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  42. data/lib/mui/key_handler/operators/paste_operator.rb +113 -0
  43. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  44. data/lib/mui/key_handler/search_mode.rb +188 -0
  45. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  46. data/lib/mui/key_handler/visual_mode.rb +397 -0
  47. data/lib/mui/key_handler/window_command.rb +112 -0
  48. data/lib/mui/key_handler.rb +16 -0
  49. data/lib/mui/layout/calculator.rb +15 -0
  50. data/lib/mui/layout/leaf_node.rb +33 -0
  51. data/lib/mui/layout/node.rb +29 -0
  52. data/lib/mui/layout/split_node.rb +132 -0
  53. data/lib/mui/line_renderer.rb +122 -0
  54. data/lib/mui/mode.rb +13 -0
  55. data/lib/mui/mode_manager.rb +185 -0
  56. data/lib/mui/motion.rb +139 -0
  57. data/lib/mui/plugin.rb +35 -0
  58. data/lib/mui/plugin_manager.rb +106 -0
  59. data/lib/mui/register.rb +110 -0
  60. data/lib/mui/screen.rb +85 -0
  61. data/lib/mui/search_completer.rb +50 -0
  62. data/lib/mui/search_input.rb +40 -0
  63. data/lib/mui/search_state.rb +88 -0
  64. data/lib/mui/selection.rb +55 -0
  65. data/lib/mui/status_line_renderer.rb +40 -0
  66. data/lib/mui/syntax/language_detector.rb +74 -0
  67. data/lib/mui/syntax/lexer_base.rb +106 -0
  68. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  69. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  70. data/lib/mui/syntax/token.rb +42 -0
  71. data/lib/mui/syntax/token_cache.rb +91 -0
  72. data/lib/mui/tab_bar_renderer.rb +87 -0
  73. data/lib/mui/tab_manager.rb +96 -0
  74. data/lib/mui/tab_page.rb +35 -0
  75. data/lib/mui/terminal_adapter/base.rb +92 -0
  76. data/lib/mui/terminal_adapter/curses.rb +162 -0
  77. data/lib/mui/terminal_adapter.rb +4 -0
  78. data/lib/mui/themes/default.rb +315 -0
  79. data/lib/mui/undo_manager.rb +83 -0
  80. data/lib/mui/undoable_action.rb +175 -0
  81. data/lib/mui/unicode_width.rb +100 -0
  82. data/lib/mui/version.rb +1 -1
  83. data/lib/mui/window.rb +158 -0
  84. data/lib/mui/window_manager.rb +249 -0
  85. data/lib/mui.rb +156 -2
  86. 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