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,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module KeyHandler
5
+ # Handles key inputs in Insert mode
6
+ class InsertMode < Base
7
+ def initialize(mode_manager, buffer, undo_manager: nil, group_started: false)
8
+ super(mode_manager, buffer)
9
+ @undo_manager = undo_manager
10
+ # Start undo group unless already started (e.g., by change operator)
11
+ # Use dynamic undo_manager to support buffer changes via :e/:sp/:vs/:tabnew
12
+ self.undo_manager&.begin_group unless group_started
13
+ # Build word cache for fast completion (use active window's buffer)
14
+ @word_cache = BufferWordCache.new(self.buffer)
15
+ end
16
+
17
+ def handle(key)
18
+ # Check plugin keymaps first
19
+ plugin_result = check_plugin_keymap(key, :insert)
20
+ return plugin_result if plugin_result
21
+
22
+ case key
23
+ when KeyCode::ESCAPE
24
+ handle_escape
25
+ when Curses::KEY_LEFT
26
+ handle_move_left
27
+ when Curses::KEY_RIGHT
28
+ handle_move_right
29
+ when Curses::KEY_UP
30
+ completion_active? ? handle_completion_previous : handle_move_up
31
+ when Curses::KEY_DOWN
32
+ completion_active? ? handle_completion_next : handle_move_down
33
+ when KeyCode::CTRL_P
34
+ completion_active? ? handle_completion_previous : handle_buffer_completion
35
+ when KeyCode::CTRL_N
36
+ completion_active? ? handle_completion_next : handle_buffer_completion
37
+ when KeyCode::TAB
38
+ completion_active? ? handle_completion_confirm : handle_tab
39
+ when KeyCode::BACKSPACE, Curses::KEY_BACKSPACE
40
+ handle_backspace
41
+ when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
42
+ handle_enter
43
+ else
44
+ handle_character_input(key)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def handle_escape
51
+ # Cancel completion if active
52
+ editor.insert_completion_state.reset if completion_active?
53
+
54
+ undo_manager&.end_group
55
+ # Remove trailing whitespace from current line if it's whitespace-only (Vim behavior)
56
+ stripped = strip_trailing_whitespace_if_empty_line
57
+ # Move cursor back one position unless we just stripped whitespace (cursor already at 0)
58
+ self.cursor_col = cursor_col - 1 if cursor_col.positive? && !stripped
59
+ result(mode: Mode::NORMAL)
60
+ end
61
+
62
+ def strip_trailing_whitespace_if_empty_line
63
+ line = buffer.line(cursor_row)
64
+ return false unless line.match?(/\A[ \t]+\z/)
65
+
66
+ # Line contains only whitespace, clear it
67
+ line.length.times { buffer.delete_char(cursor_row, 0) }
68
+ self.cursor_col = 0
69
+ true
70
+ end
71
+
72
+ def handle_move_left
73
+ reset_insert_completion_state
74
+ self.cursor_col = cursor_col - 1 if cursor_col.positive?
75
+ result
76
+ end
77
+
78
+ def handle_move_right
79
+ reset_insert_completion_state
80
+ self.cursor_col = cursor_col + 1 if cursor_col < current_line_length
81
+ result
82
+ end
83
+
84
+ def handle_move_up
85
+ window.move_up
86
+ result
87
+ end
88
+
89
+ def handle_move_down
90
+ window.move_down
91
+ result
92
+ end
93
+
94
+ def handle_backspace
95
+ if cursor_col.positive?
96
+ self.cursor_col = cursor_col - 1
97
+ buffer.delete_char(cursor_row, cursor_col)
98
+ # Update completion list after backspace
99
+ update_completion_list if completion_active?
100
+ elsif cursor_row.positive?
101
+ join_with_previous_line
102
+ reset_insert_completion_state
103
+ end
104
+ result
105
+ end
106
+
107
+ def join_with_previous_line
108
+ prev_line_len = buffer.line(cursor_row - 1).length
109
+ buffer.join_lines(cursor_row - 1)
110
+ self.cursor_row = cursor_row - 1
111
+ self.cursor_col = prev_line_len
112
+ end
113
+
114
+ def handle_enter
115
+ # Get indent from current line before splitting
116
+ current_line = buffer.line(cursor_row)
117
+ indent = extract_indent(current_line)
118
+
119
+ buffer.split_line(cursor_row, cursor_col)
120
+ self.cursor_row = cursor_row + 1
121
+
122
+ # Insert indent at the beginning of the new line
123
+ if indent && !indent.empty?
124
+ indent.each_char.with_index do |char, i|
125
+ buffer.insert_char(cursor_row, i, char)
126
+ end
127
+ self.cursor_col = indent.length
128
+ else
129
+ self.cursor_col = 0
130
+ end
131
+ result
132
+ end
133
+
134
+ def extract_indent(line)
135
+ match = line.match(/\A([ \t]*)/)
136
+ match ? match[1] : ""
137
+ end
138
+
139
+ def handle_character_input(key)
140
+ char = extract_printable_char(key)
141
+ if char
142
+ buffer.insert_char(cursor_row, cursor_col, char)
143
+ self.cursor_col = cursor_col + 1
144
+
145
+ # Update completion list if active
146
+ if completion_active? && word_char?(char)
147
+ update_completion_list
148
+ elsif completion_active?
149
+ # Non-word character typed, close completion and add completed word to cache
150
+ add_current_word_to_cache
151
+ editor.insert_completion_state.reset
152
+ trigger_completion_for(char)
153
+ else
154
+ # Non-word char means previous word is complete, add to cache
155
+ add_current_word_to_cache unless word_char?(char)
156
+ # Trigger completion for certain characters
157
+ trigger_completion_for(char)
158
+ end
159
+ end
160
+ result
161
+ end
162
+
163
+ def add_current_word_to_cache
164
+ # Get the word that just ended (before current cursor)
165
+ line = buffer.line(cursor_row)
166
+ return if cursor_col < 2
167
+
168
+ # Find the word that just ended
169
+ end_col = cursor_col - 1
170
+ start_col = end_col
171
+ start_col -= 1 while start_col.positive? && word_char?(line[start_col - 1])
172
+
173
+ word = line[start_col...end_col]
174
+ @word_cache.add_word(word) if word && word.length >= BufferWordCache::MIN_WORD_LENGTH
175
+ end
176
+
177
+ def update_completion_list
178
+ return unless editor
179
+
180
+ new_prefix = @word_cache.prefix_at(cursor_row, cursor_col)
181
+
182
+ if new_prefix.empty?
183
+ editor.insert_completion_state.reset
184
+ return
185
+ end
186
+
187
+ editor.insert_completion_state.update_prefix(new_prefix)
188
+
189
+ # Close completion if no matches remain
190
+ editor.insert_completion_state.reset unless editor.insert_completion_state.active?
191
+ end
192
+
193
+ def trigger_completion_for(char)
194
+ return unless editor
195
+
196
+ # LSP completion triggers
197
+ if %w[. @].include?(char) || (char == ":" && previous_char == ":")
198
+ editor.trigger_autocmd(:InsertCompletion)
199
+ return
200
+ end
201
+
202
+ # Buffer word completion - trigger after typing word characters
203
+ trigger_buffer_completion_if_needed(min_prefix: 1) if word_char?(char)
204
+ end
205
+
206
+ def trigger_buffer_completion_if_needed(min_prefix: 3)
207
+ prefix = @word_cache.prefix_at(cursor_row, cursor_col)
208
+
209
+ return if prefix.length < min_prefix
210
+
211
+ # Mark current row as dirty to exclude word at cursor
212
+ @word_cache.mark_dirty(cursor_row)
213
+ candidates = @word_cache.complete(prefix, cursor_row, cursor_col)
214
+ return if candidates.empty?
215
+
216
+ # Format candidates for InsertCompletionState
217
+ items = candidates.map do |word|
218
+ {
219
+ label: word,
220
+ insert_text: word
221
+ }
222
+ end
223
+
224
+ editor.start_insert_completion(items, prefix:)
225
+ end
226
+
227
+ def word_char?(char)
228
+ char&.match?(/\w/)
229
+ end
230
+
231
+ def previous_char
232
+ return nil if cursor_col < 2
233
+
234
+ buffer.line(cursor_row)[cursor_col - 2]
235
+ end
236
+
237
+ def handle_buffer_completion
238
+ return result unless editor
239
+
240
+ # For manual trigger (Ctrl+N/P), allow 1+ character prefix
241
+ trigger_buffer_completion_if_needed(min_prefix: 1)
242
+ result
243
+ end
244
+
245
+ def handle_tab
246
+ buffer.insert_char(cursor_row, cursor_col, "\t")
247
+ self.cursor_col = cursor_col + 1
248
+ result
249
+ end
250
+
251
+ def completion_active?
252
+ editor&.insert_completion_active?
253
+ end
254
+
255
+ def handle_completion_next
256
+ editor.insert_completion_state.select_next
257
+ result
258
+ end
259
+
260
+ def handle_completion_previous
261
+ editor.insert_completion_state.select_previous
262
+ result
263
+ end
264
+
265
+ def handle_completion_confirm
266
+ state = editor.insert_completion_state
267
+ return result unless state.current_item
268
+
269
+ insert_text = state.insert_text
270
+ text_edit_range = state.text_edit_range
271
+
272
+ if text_edit_range
273
+ # Use textEdit range for precise replacement
274
+ apply_text_edit(insert_text, text_edit_range)
275
+ else
276
+ # Fallback to prefix-based replacement
277
+ apply_prefix_replacement(insert_text, state.prefix)
278
+ end
279
+
280
+ state.reset
281
+ result
282
+ end
283
+
284
+ def apply_text_edit(insert_text, range)
285
+ start_char = range.dig(:start, :character) || range.dig("start", "character")
286
+ end_char = range.dig(:end, :character) || range.dig("end", "character")
287
+
288
+ # Delete from start to end
289
+ delete_count = end_char - start_char
290
+ self.cursor_col = start_char
291
+ delete_count.times { buffer.delete_char(cursor_row, cursor_col) }
292
+
293
+ # Insert new text
294
+ insert_text.each_char do |c|
295
+ buffer.insert_char(cursor_row, cursor_col, c)
296
+ self.cursor_col = cursor_col + 1
297
+ end
298
+ end
299
+
300
+ def apply_prefix_replacement(insert_text, prefix)
301
+ # Delete prefix
302
+ prefix.length.times do
303
+ self.cursor_col = cursor_col - 1
304
+ buffer.delete_char(cursor_row, cursor_col)
305
+ end
306
+
307
+ # Insert completion text
308
+ insert_text.each_char do |c|
309
+ buffer.insert_char(cursor_row, cursor_col, c)
310
+ self.cursor_col = cursor_col + 1
311
+ end
312
+ end
313
+
314
+ def reset_insert_completion_state
315
+ editor.insert_completion_state.reset if completion_active?
316
+ end
317
+
318
+ def result(mode: nil, message: nil, quit: false)
319
+ HandlerResult::InsertModeResult.new(mode:, message:, quit:)
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module KeyHandler
5
+ module Motions
6
+ # Shared motion handling for NormalMode and VisualMode
7
+ # Provides common handle_* methods that delegate to Motion module
8
+ module MotionHandler
9
+ def handle_word_forward
10
+ apply_motion(Motion.word_forward(buffer, cursor_row, cursor_col))
11
+ result
12
+ end
13
+
14
+ def handle_word_backward
15
+ apply_motion(Motion.word_backward(buffer, cursor_row, cursor_col))
16
+ result
17
+ end
18
+
19
+ def handle_word_end
20
+ apply_motion(Motion.word_end(buffer, cursor_row, cursor_col))
21
+ result
22
+ end
23
+
24
+ def handle_line_start
25
+ apply_motion(Motion.line_start(buffer, cursor_row, cursor_col))
26
+ result
27
+ end
28
+
29
+ def handle_first_non_blank
30
+ apply_motion(Motion.first_non_blank(buffer, cursor_row, cursor_col))
31
+ result
32
+ end
33
+
34
+ def handle_line_end
35
+ apply_motion(Motion.line_end(buffer, cursor_row, cursor_col))
36
+ result
37
+ end
38
+
39
+ def handle_file_end
40
+ apply_motion(Motion.file_end(buffer, cursor_row, cursor_col))
41
+ result
42
+ end
43
+
44
+ private
45
+
46
+ def apply_motion(motion_result)
47
+ return unless motion_result
48
+
49
+ self.cursor_row = motion_result[:row]
50
+ self.cursor_col = motion_result[:col]
51
+ window.clamp_cursor_to_line(buffer)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end