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
data/lib/mui/editor.rb ADDED
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Main editor class that coordinates all components
5
+ class Editor
6
+ attr_reader :tab_manager, :undo_manager, :autocmd, :command_registry, :job_manager, :color_scheme, :floating_window,
7
+ :insert_completion_state
8
+ attr_accessor :message, :running
9
+
10
+ def initialize(file_path = nil, adapter: TerminalAdapter::Curses.new, load_config: true)
11
+ Mui.load_config if load_config
12
+
13
+ @adapter = adapter
14
+ @color_manager = ColorManager.new
15
+ @adapter.color_resolver = @color_manager
16
+ @color_scheme = load_color_scheme
17
+ @screen = Screen.new(adapter: @adapter, color_manager: @color_manager)
18
+ @input = Input.new(adapter: @adapter)
19
+ @buffer = Buffer.new
20
+ @buffer.load(file_path) if file_path
21
+
22
+ @tab_manager = TabManager.new(@screen, color_scheme: @color_scheme)
23
+ initial_tab = @tab_manager.add
24
+ initial_tab.window_manager.add_window(@buffer)
25
+
26
+ @tab_bar_renderer = TabBarRenderer.new(@tab_manager, color_scheme: @color_scheme)
27
+
28
+ @command_line = CommandLine.new
29
+ @completion_renderer = CompletionRenderer.new(@screen, @color_scheme)
30
+ @insert_completion_renderer = InsertCompletionRenderer.new(@screen, @color_scheme)
31
+ @floating_window = FloatingWindow.new(@color_scheme)
32
+ @message = nil
33
+ @running = true
34
+
35
+ @undo_manager = UndoManager.new
36
+ @buffer.undo_manager = @undo_manager
37
+
38
+ @autocmd = Autocmd.new
39
+ @command_registry = CommandRegistry.new
40
+ @job_manager = JobManager.new(autocmd: @autocmd)
41
+ @insert_completion_state = InsertCompletionState.new
42
+
43
+ # Install and load plugins via bundler/inline
44
+ Mui.plugin_manager.install_and_load
45
+
46
+ # Load plugin autocmds
47
+ load_plugin_autocmds
48
+
49
+ @mode_manager = ModeManager.new(
50
+ window: @tab_manager,
51
+ buffer: @buffer,
52
+ command_line: @command_line,
53
+ undo_manager: @undo_manager,
54
+ editor: self
55
+ )
56
+
57
+ # Trigger BufEnter event
58
+ trigger_autocmd(:BufEnter)
59
+ end
60
+
61
+ def window_manager
62
+ @tab_manager.window_manager
63
+ end
64
+
65
+ def window
66
+ @tab_manager.active_window
67
+ end
68
+
69
+ def buffer
70
+ window.buffer
71
+ end
72
+
73
+ def mode
74
+ @mode_manager.mode
75
+ end
76
+
77
+ def selection
78
+ @mode_manager.selection
79
+ end
80
+
81
+ def register
82
+ @mode_manager.register
83
+ end
84
+
85
+ def run
86
+ while @running
87
+ process_job_results
88
+ update_window_size
89
+ render
90
+ key = @input.read_nonblock
91
+ if key
92
+ handle_key(key)
93
+ else
94
+ sleep 0.01 # CPU usage optimization
95
+ end
96
+ end
97
+ ensure
98
+ @adapter.close
99
+ end
100
+
101
+ # Open a scratch buffer (read-only) with the given content
102
+ def open_scratch_buffer(name, content)
103
+ scratch_buffer = Buffer.new(name)
104
+ scratch_buffer.content = content
105
+ scratch_buffer.readonly = true
106
+
107
+ # Split horizontally and show the scratch buffer
108
+ window_manager.split_horizontal(scratch_buffer)
109
+ end
110
+
111
+ # Suspend UI for running external interactive commands (e.g., fzf)
112
+ def suspend_ui
113
+ @adapter.suspend
114
+ yield
115
+ ensure
116
+ @adapter.resume
117
+ end
118
+
119
+ def handle_key(key)
120
+ @message = nil
121
+
122
+ # Close floating window on Escape or any key input (except scroll keys if we add them)
123
+ if @floating_window.visible
124
+ @floating_window.hide
125
+ return if key == KeyCode::ESCAPE
126
+ end
127
+
128
+ old_window = window
129
+ old_buffer = old_window&.buffer
130
+ old_modified = old_buffer&.modified
131
+ result = @mode_manager.current_handler.handle(key)
132
+ apply_result(result)
133
+
134
+ current_window = window
135
+ return unless current_window # Guard against nil window (e.g., after closing last tab)
136
+
137
+ current_buffer = current_window.buffer
138
+
139
+ # Trigger BufEnter if buffer changed (window focus change)
140
+ trigger_autocmd(:BufEnter) if current_buffer != old_buffer
141
+
142
+ # Trigger TextChanged if buffer was modified
143
+ trigger_autocmd(:TextChanged) if (current_buffer.modified && !old_modified) || buffer_content_changed?
144
+ end
145
+
146
+ # Trigger autocmd event with current context
147
+ def trigger_autocmd(event)
148
+ context = CommandContext.new(editor: self, buffer:, window:)
149
+ @autocmd.trigger(event, context)
150
+ end
151
+
152
+ # Show a floating window with content at the cursor position
153
+ def show_floating(content, max_width: nil, max_height: nil)
154
+ # Position below cursor
155
+ row = window.screen_cursor_y + 1
156
+ col = window.screen_cursor_x
157
+
158
+ @floating_window.show(
159
+ content,
160
+ row:,
161
+ col:,
162
+ max_width: max_width || (@screen.width / 2),
163
+ max_height: max_height || 10
164
+ )
165
+ end
166
+
167
+ # Hide the floating window
168
+ def hide_floating
169
+ @floating_window.hide
170
+ end
171
+
172
+ # Start insert mode completion with LSP items
173
+ def start_insert_completion(items, prefix: "")
174
+ @insert_completion_state.start(items, prefix:)
175
+ end
176
+
177
+ # Check if insert completion is active
178
+ def insert_completion_active?
179
+ @insert_completion_state.active?
180
+ end
181
+
182
+ private
183
+
184
+ def buffer_content_changed?
185
+ # Track if content actually changed (for TextChanged event)
186
+ # Use change_count instead of hash for O(1) performance
187
+ @last_change_count ||= buffer.change_count
188
+ current_count = buffer.change_count
189
+ changed = @last_change_count != current_count
190
+ @last_change_count = current_count
191
+ changed
192
+ end
193
+
194
+ def update_window_size
195
+ window_manager.update_layout(y_offset: tab_bar_height)
196
+ end
197
+
198
+ def render
199
+ @screen.clear
200
+
201
+ @tab_bar_renderer.render(@screen, 0)
202
+
203
+ window.ensure_cursor_visible
204
+ window_manager.render_all(
205
+ @screen,
206
+ selection: @mode_manager.selection,
207
+ search_state: @mode_manager.search_state
208
+ )
209
+
210
+ render_status_area
211
+
212
+ # Position cursor based on current mode
213
+ if @mode_manager.mode == Mode::COMMAND
214
+ # In command mode, cursor is on the command line (after ":" + buffer position)
215
+ @screen.move_cursor(@screen.height - 1, 1 + @command_line.cursor_pos)
216
+ elsif [Mode::SEARCH_FORWARD, Mode::SEARCH_BACKWARD].include?(@mode_manager.mode)
217
+ # In search mode, cursor is on the search input line (after "/" or "?" + pattern)
218
+ @screen.move_cursor(@screen.height - 1, 1 + @mode_manager.search_input.buffer.length)
219
+ else
220
+ # In other modes, cursor is in the editor window
221
+ @screen.move_cursor(window.screen_cursor_y, window.screen_cursor_x)
222
+ end
223
+ @screen.refresh
224
+ end
225
+
226
+ def tab_bar_height
227
+ @tab_bar_renderer.height
228
+ end
229
+
230
+ def render_status_area
231
+ status_text = case @mode_manager.mode
232
+ when Mode::COMMAND
233
+ @command_line.to_s
234
+ when Mode::INSERT
235
+ @message || "-- INSERT --"
236
+ when Mode::VISUAL
237
+ @message || "-- VISUAL --"
238
+ when Mode::VISUAL_LINE
239
+ @message || "-- VISUAL LINE --"
240
+ when Mode::SEARCH_FORWARD, Mode::SEARCH_BACKWARD
241
+ @mode_manager.search_input.to_s
242
+ else
243
+ @message || "-- NORMAL --"
244
+ end
245
+
246
+ status_line = status_text.ljust(@screen.width)
247
+ style = @color_scheme[:command_line]
248
+ @screen.put_with_style(@screen.height - 1, 0, status_line, style)
249
+
250
+ # Render completion popup based on mode
251
+ if @mode_manager.mode == Mode::COMMAND
252
+ render_completion_popup
253
+ elsif [Mode::SEARCH_FORWARD, Mode::SEARCH_BACKWARD].include?(@mode_manager.mode)
254
+ render_search_completion_popup
255
+ elsif @mode_manager.mode == Mode::INSERT
256
+ render_insert_completion_popup
257
+ end
258
+
259
+ # Render floating window if visible
260
+ @floating_window.render(@screen)
261
+ end
262
+
263
+ def render_search_completion_popup
264
+ completion_state = @mode_manager.current_handler.completion_state
265
+ return unless completion_state&.active?
266
+
267
+ # Popup appears above the search line, starting after "/" or "?"
268
+ base_row = @screen.height - 1
269
+ base_col = 1 # After the prompt
270
+ @completion_renderer.render(completion_state, base_row, base_col)
271
+ end
272
+
273
+ def render_completion_popup
274
+ completion_state = @mode_manager.current_handler.completion_state
275
+ return unless completion_state&.active?
276
+
277
+ # Popup appears above the command line, starting after the ":"
278
+ base_row = @screen.height - 1
279
+ base_col = 1 # After the ":"
280
+ @completion_renderer.render(completion_state, base_row, base_col)
281
+ end
282
+
283
+ def render_insert_completion_popup
284
+ return unless @insert_completion_state&.active?
285
+
286
+ # Popup appears below the cursor
287
+ @insert_completion_renderer.render(
288
+ @insert_completion_state,
289
+ window.screen_cursor_y,
290
+ window.screen_cursor_x
291
+ )
292
+ end
293
+
294
+ def apply_result(result)
295
+ @mode_manager.transition(result)
296
+ @message = result.message if result.message
297
+ @running = false if result.quit?
298
+ end
299
+
300
+ def load_color_scheme
301
+ scheme_name = Mui.config.get(:colorscheme)
302
+ Themes.send(scheme_name.to_sym)
303
+ rescue NoMethodError
304
+ Themes.mui
305
+ end
306
+
307
+ def load_plugin_autocmds
308
+ Mui.config.autocmds.each do |event, handlers|
309
+ handlers.each do |h|
310
+ @autocmd.register(event, pattern: h[:pattern], &h[:handler])
311
+ end
312
+ end
313
+ end
314
+
315
+ def process_job_results
316
+ @job_manager.poll
317
+ end
318
+ end
319
+ end
data/lib/mui/error.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class Error < StandardError; end
5
+
6
+ # Raised when a subclass does not override a required method
7
+ class MethodNotOverriddenError < Error
8
+ def initialize(method_name)
9
+ super("Subclass must implement ##{method_name}")
10
+ end
11
+ end
12
+
13
+ # Raised when a plugin operation fails
14
+ class PluginError < Error; end
15
+
16
+ # Raised when a plugin is not found
17
+ class PluginNotFoundError < PluginError
18
+ def initialize(plugin_name)
19
+ super("Plugin '#{plugin_name}' not found")
20
+ end
21
+ end
22
+
23
+ # Raised when an unknown command is executed
24
+ class UnknownCommandError < Error
25
+ def initialize(command_name)
26
+ super("Unknown command: #{command_name}")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Provides file path completion
5
+ class FileCompleter
6
+ def complete(partial_path)
7
+ return list_current_directory if partial_path.empty?
8
+
9
+ dir, prefix = split_path(partial_path)
10
+ entries = list_directory(dir)
11
+
12
+ entries.select { |entry| entry.start_with?(prefix) }
13
+ .map { |entry| join_path(dir, entry) }
14
+ .map { |path| format_path(path) }
15
+ end
16
+
17
+ private
18
+
19
+ def split_path(path)
20
+ if path.end_with?("/")
21
+ [path, ""]
22
+ else
23
+ dir = File.dirname(path)
24
+ dir = "" if dir == "."
25
+ [dir, File.basename(path)]
26
+ end
27
+ end
28
+
29
+ def list_directory(dir)
30
+ target = dir.empty? ? "." : dir
31
+ return [] unless Dir.exist?(target)
32
+
33
+ Dir.entries(target)
34
+ .reject { |e| e.start_with?(".") }
35
+ .sort
36
+ end
37
+
38
+ def list_current_directory
39
+ list_directory("").map { |entry| format_path(entry) }
40
+ end
41
+
42
+ def join_path(dir, entry)
43
+ dir.empty? ? entry : File.join(dir, entry)
44
+ end
45
+
46
+ def format_path(path)
47
+ full_path = path.start_with?("/") ? path : File.join(".", path)
48
+ File.directory?(full_path) ? "#{path}/" : path
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # A floating window (popup) for displaying temporary content like hover info
5
+ class FloatingWindow
6
+ attr_reader :content, :row, :col, :width, :height
7
+ attr_accessor :visible
8
+
9
+ def initialize(color_scheme)
10
+ @color_scheme = color_scheme
11
+ @content = []
12
+ @row = 0
13
+ @col = 0
14
+ @width = 0
15
+ @height = 0
16
+ @visible = false
17
+ @scroll_offset = 0
18
+ end
19
+
20
+ # Show the floating window with content at the specified position
21
+ def show(content, row:, col:, max_width: nil, max_height: nil)
22
+ @content = normalize_content(content)
23
+ @row = row
24
+ @col = col
25
+ @max_width = max_width
26
+ @max_height = max_height
27
+ @scroll_offset = 0
28
+ calculate_dimensions
29
+ @visible = true
30
+ end
31
+
32
+ # Hide the floating window
33
+ def hide
34
+ @visible = false
35
+ @content = []
36
+ end
37
+
38
+ # Scroll content up
39
+ def scroll_up
40
+ @scroll_offset = [@scroll_offset - 1, 0].max if @visible
41
+ end
42
+
43
+ # Scroll content down
44
+ def scroll_down
45
+ max_offset = [@content.length - @height, 0].max
46
+ @scroll_offset = [@scroll_offset + 1, max_offset].min if @visible
47
+ end
48
+
49
+ # Render the floating window to the screen
50
+ def render(screen)
51
+ return unless @visible
52
+ return if @content.empty?
53
+
54
+ # Adjust position to fit within screen bounds
55
+ adjusted_row, adjusted_col = adjust_position(screen)
56
+
57
+ # Draw border and content
58
+ draw_border(screen, adjusted_row, adjusted_col)
59
+ draw_content(screen, adjusted_row, adjusted_col)
60
+ end
61
+
62
+ private
63
+
64
+ def normalize_content(content)
65
+ case content
66
+ when String
67
+ content.split("\n")
68
+ when Array
69
+ content.flat_map { |line| line.to_s.split("\n") }
70
+ else
71
+ [content.to_s]
72
+ end
73
+ end
74
+
75
+ def calculate_dimensions
76
+ return if @content.empty?
77
+
78
+ # Calculate content dimensions
79
+ content_width = @content.map { |line| UnicodeWidth.string_width(line) }.max || 0
80
+ content_height = @content.length
81
+
82
+ # Apply max constraints (+2 for border)
83
+ @width = content_width + 2
84
+ @width = [@width, @max_width].min if @max_width
85
+
86
+ @height = content_height + 2
87
+ @height = [@height, @max_height].min if @max_height
88
+ end
89
+
90
+ def adjust_position(screen)
91
+ row = @row
92
+ col = @col
93
+
94
+ # Adjust horizontal position
95
+ col = screen.width - @width if col + @width > screen.width
96
+ col = [col, 0].max
97
+
98
+ # Adjust vertical position - prefer below cursor, but go above if not enough space
99
+ if row + @height > screen.height
100
+ # Try above the original position
101
+ row = @row - @height
102
+ end
103
+ row = [row, 0].max
104
+
105
+ [row, col]
106
+ end
107
+
108
+ def draw_border(screen, row, col)
109
+ style = @color_scheme[:floating_window] || @color_scheme[:completion_popup]
110
+
111
+ # Top border
112
+ top_border = "┌#{"─" * (@width - 2)}┐"
113
+ screen.put_with_style(row, col, top_border, style)
114
+
115
+ # Side borders
116
+ inner_height = @height - 2
117
+ inner_height.times do |i|
118
+ screen.put_with_style(row + 1 + i, col, "│", style)
119
+ screen.put_with_style(row + 1 + i, col + @width - 1, "│", style)
120
+ end
121
+
122
+ # Bottom border
123
+ bottom_border = "└#{"─" * (@width - 2)}┘"
124
+ screen.put_with_style(row + @height - 1, col, bottom_border, style)
125
+ end
126
+
127
+ def draw_content(screen, row, col)
128
+ style = @color_scheme[:floating_window] || @color_scheme[:completion_popup]
129
+ inner_width = @width - 2
130
+ inner_height = @height - 2
131
+
132
+ inner_height.times do |i|
133
+ line_index = @scroll_offset + i
134
+ line = @content[line_index] || ""
135
+
136
+ # Truncate line if needed
137
+ display_line = truncate_to_width(line, inner_width)
138
+ padded_line = display_line.ljust(inner_width)
139
+
140
+ screen.put_with_style(row + 1 + i, col + 1, padded_line, style)
141
+ end
142
+ end
143
+
144
+ def truncate_to_width(text, max_width)
145
+ return text if UnicodeWidth.string_width(text) <= max_width
146
+
147
+ result = ""
148
+ current_width = 0
149
+
150
+ text.each_char do |char|
151
+ char_width = UnicodeWidth.char_width(char)
152
+ break if current_width + char_width > max_width
153
+
154
+ result += char
155
+ current_width += char_width
156
+ end
157
+
158
+ result
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module HandlerResult
5
+ # Base class for handler results with common attributes and default implementations
6
+ class Base
7
+ attr_reader :mode, :message
8
+
9
+ def initialize(mode: nil, message: nil, quit: false)
10
+ @mode = mode
11
+ @message = message
12
+ @quit = quit
13
+ freeze
14
+ end
15
+
16
+ def quit?
17
+ @quit
18
+ end
19
+
20
+ def start_selection?
21
+ false
22
+ end
23
+
24
+ def line_mode?
25
+ false
26
+ end
27
+
28
+ def clear_selection?
29
+ false
30
+ end
31
+
32
+ def toggle_line_mode?
33
+ false
34
+ end
35
+ end
36
+
37
+ # Result for NormalMode - handles visual mode start
38
+ class NormalModeResult < Base
39
+ def initialize(mode: nil, message: nil, quit: false, start_selection: false, line_mode: false, group_started: false)
40
+ @start_selection = start_selection
41
+ @line_mode = line_mode
42
+ @group_started = group_started
43
+ super(mode:, message:, quit:)
44
+ end
45
+
46
+ def start_selection?
47
+ @start_selection
48
+ end
49
+
50
+ def line_mode?
51
+ @line_mode
52
+ end
53
+
54
+ def group_started?
55
+ @group_started
56
+ end
57
+ end
58
+
59
+ # Result for VisualMode - handles selection clear and line mode toggle
60
+ class VisualModeResult < Base
61
+ def initialize(mode: nil, message: nil, quit: false, clear_selection: false, toggle_line_mode: false, group_started: false)
62
+ @clear_selection = clear_selection
63
+ @toggle_line_mode = toggle_line_mode
64
+ @group_started = group_started
65
+ super(mode:, message:, quit:)
66
+ end
67
+
68
+ def clear_selection?
69
+ @clear_selection
70
+ end
71
+
72
+ def toggle_line_mode?
73
+ @toggle_line_mode
74
+ end
75
+
76
+ def group_started?
77
+ @group_started
78
+ end
79
+ end
80
+
81
+ # Result for InsertMode - uses base functionality only
82
+ class InsertModeResult < Base
83
+ end
84
+
85
+ # Result for CommandMode - uses base functionality only
86
+ class CommandModeResult < Base
87
+ end
88
+
89
+ # Result for SearchMode - handles search execution
90
+ class SearchModeResult < Base
91
+ def initialize(mode: nil, message: nil, quit: false, cancelled: false)
92
+ @cancelled = cancelled
93
+ super(mode:, message:, quit:)
94
+ end
95
+
96
+ def cancelled?
97
+ @cancelled
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class Highlight
5
+ attr_reader :start_col, :end_col, :style, :priority
6
+
7
+ def initialize(start_col:, end_col:, style:, priority:)
8
+ @start_col = start_col
9
+ @end_col = end_col
10
+ @style = style
11
+ @priority = priority
12
+ end
13
+
14
+ def overlaps?(other)
15
+ start_col <= other.end_col && end_col >= other.start_col
16
+ end
17
+
18
+ def <=>(other)
19
+ [start_col, -priority] <=> [other.start_col, -other.priority]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Highlighters
5
+ class Base
6
+ PRIORITY_SYNTAX = 100
7
+ PRIORITY_SELECTION = 200
8
+ PRIORITY_SEARCH = 300
9
+
10
+ def initialize(color_scheme)
11
+ @color_scheme = color_scheme
12
+ end
13
+
14
+ def highlights_for(_row, _line, _options = {})
15
+ raise Mui::MethodNotOverriddenError, "#{self.class}#highlights_for must be implemented"
16
+ end
17
+
18
+ def priority
19
+ 0
20
+ end
21
+ end
22
+ end
23
+ end