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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +163 -0
- data/CHANGELOG.md +448 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +301 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +149 -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 +30 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +167 -0
- data/lib/mui/command_registry.rb +44 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +58 -0
- data/lib/mui/editor.rb +395 -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 +107 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +27 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +107 -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 +187 -0
- data/lib/mui/key_handler/command_mode.rb +511 -0
- data/lib/mui/key_handler/insert_mode.rb +323 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +552 -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 +119 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +191 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +402 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -0
- data/lib/mui/key_notation_parser.rb +152 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -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 +173 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +186 -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 +103 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +121 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +106 -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/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -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 +164 -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 +201 -0
- data/lib/mui/window_manager.rb +256 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +171 -2
- metadata +123 -5
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
# Handles key inputs in Search mode (/ and ?)
|
|
6
|
+
class SearchMode < Base
|
|
7
|
+
attr_reader :completion_state
|
|
8
|
+
|
|
9
|
+
def initialize(mode_manager, buffer, search_input, search_state)
|
|
10
|
+
super(mode_manager, buffer)
|
|
11
|
+
@search_input = search_input
|
|
12
|
+
@search_state = search_state
|
|
13
|
+
@original_cursor_row = nil
|
|
14
|
+
@original_cursor_col = nil
|
|
15
|
+
@completion_state = CompletionState.new
|
|
16
|
+
@search_completer = SearchCompleter.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start_search
|
|
20
|
+
@original_cursor_row = cursor_row
|
|
21
|
+
@original_cursor_col = cursor_col
|
|
22
|
+
@completion_state.reset
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def handle(key)
|
|
26
|
+
# Check plugin keymaps first
|
|
27
|
+
plugin_result = check_plugin_keymap(key, :search)
|
|
28
|
+
return plugin_result if plugin_result
|
|
29
|
+
|
|
30
|
+
case key
|
|
31
|
+
when KeyCode::ESCAPE
|
|
32
|
+
handle_escape
|
|
33
|
+
when KeyCode::BACKSPACE, Curses::KEY_BACKSPACE
|
|
34
|
+
handle_backspace
|
|
35
|
+
when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
|
|
36
|
+
handle_enter
|
|
37
|
+
when KeyCode::TAB
|
|
38
|
+
handle_tab
|
|
39
|
+
when Curses::KEY_BTAB
|
|
40
|
+
handle_shift_tab
|
|
41
|
+
else
|
|
42
|
+
handle_character_input(key)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def handle_tab
|
|
49
|
+
return result unless @completion_state.active?
|
|
50
|
+
|
|
51
|
+
@completion_state.select_next
|
|
52
|
+
apply_current_completion
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_shift_tab
|
|
57
|
+
return result unless @completion_state.active?
|
|
58
|
+
|
|
59
|
+
@completion_state.select_previous
|
|
60
|
+
apply_current_completion
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def apply_current_completion
|
|
65
|
+
candidate = @completion_state.current_candidate
|
|
66
|
+
return unless candidate
|
|
67
|
+
|
|
68
|
+
@search_input.clear
|
|
69
|
+
candidate.each_char { |c| @search_input.input(c) }
|
|
70
|
+
update_incremental_search
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_escape
|
|
74
|
+
@search_input.clear
|
|
75
|
+
@search_state.clear
|
|
76
|
+
@completion_state.reset
|
|
77
|
+
# Restore original cursor position
|
|
78
|
+
restore_cursor_position
|
|
79
|
+
result(mode: Mode::NORMAL, cancelled: true)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def restore_cursor_position
|
|
83
|
+
return unless @original_cursor_row && @original_cursor_col
|
|
84
|
+
|
|
85
|
+
window.cursor_row = @original_cursor_row
|
|
86
|
+
window.cursor_col = @original_cursor_col
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle_backspace
|
|
90
|
+
if @search_input.empty?
|
|
91
|
+
@search_state.clear
|
|
92
|
+
@completion_state.reset
|
|
93
|
+
restore_cursor_position
|
|
94
|
+
result(mode: Mode::NORMAL, cancelled: true)
|
|
95
|
+
else
|
|
96
|
+
@search_input.backspace
|
|
97
|
+
update_incremental_search
|
|
98
|
+
update_completion
|
|
99
|
+
result
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_enter
|
|
104
|
+
@completion_state.reset
|
|
105
|
+
execute_search
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_character_input(key)
|
|
109
|
+
char = extract_printable_char(key)
|
|
110
|
+
if char
|
|
111
|
+
@search_input.input(char)
|
|
112
|
+
update_incremental_search
|
|
113
|
+
update_completion
|
|
114
|
+
end
|
|
115
|
+
result
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def update_completion
|
|
119
|
+
prefix = @search_input.pattern
|
|
120
|
+
if prefix.empty?
|
|
121
|
+
@completion_state.reset
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
candidates = @search_completer.complete(buffer, prefix)
|
|
126
|
+
if candidates.empty?
|
|
127
|
+
@completion_state.reset
|
|
128
|
+
else
|
|
129
|
+
@completion_state.start(candidates, prefix, :search)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def update_incremental_search
|
|
134
|
+
pattern = @search_input.pattern
|
|
135
|
+
if pattern.empty?
|
|
136
|
+
@search_state.clear
|
|
137
|
+
restore_cursor_position
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
direction = @search_input.prompt == "/" ? :forward : :backward
|
|
142
|
+
@search_state.set_pattern(pattern, direction)
|
|
143
|
+
|
|
144
|
+
# Move cursor to first match from original position
|
|
145
|
+
matches = @search_state.find_all_matches(buffer)
|
|
146
|
+
return if matches.empty?
|
|
147
|
+
|
|
148
|
+
# Use original position if set, otherwise use current cursor position
|
|
149
|
+
search_row = @original_cursor_row || cursor_row
|
|
150
|
+
search_col = @original_cursor_col || cursor_col
|
|
151
|
+
|
|
152
|
+
match = if direction == :forward
|
|
153
|
+
@search_state.find_next(search_row, search_col, buffer:)
|
|
154
|
+
else
|
|
155
|
+
@search_state.find_previous(search_row, search_col, buffer:)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
return unless match
|
|
159
|
+
|
|
160
|
+
window.cursor_row = match[:row]
|
|
161
|
+
window.cursor_col = match[:col]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def execute_search
|
|
165
|
+
pattern = @search_input.pattern
|
|
166
|
+
return result(mode: Mode::NORMAL, cancelled: true) if pattern.empty?
|
|
167
|
+
|
|
168
|
+
direction = @search_input.prompt == "/" ? :forward : :backward
|
|
169
|
+
@search_state.set_pattern(pattern, direction)
|
|
170
|
+
|
|
171
|
+
match = if direction == :forward
|
|
172
|
+
@search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
173
|
+
else
|
|
174
|
+
@search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if match
|
|
178
|
+
window.cursor_row = match[:row]
|
|
179
|
+
window.cursor_col = match[:col]
|
|
180
|
+
result(mode: Mode::NORMAL)
|
|
181
|
+
else
|
|
182
|
+
result(mode: Mode::NORMAL, message: "Pattern not found: #{pattern}")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def result(mode: nil, message: nil, quit: false, cancelled: false)
|
|
187
|
+
HandlerResult::SearchModeResult.new(mode:, message:, quit:, cancelled:)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
# Handler for line-wise visual mode (V)
|
|
6
|
+
class VisualLineMode < VisualMode
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def handle_v_key
|
|
10
|
+
# v in visual line mode switches to visual mode
|
|
11
|
+
result(mode: Mode::VISUAL, toggle_line_mode: true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def handle_upper_v_key
|
|
15
|
+
# V in visual line mode exits to normal mode
|
|
16
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
# Handler for character-wise visual mode (v)
|
|
6
|
+
class VisualMode < Base
|
|
7
|
+
include Motions::MotionHandler
|
|
8
|
+
|
|
9
|
+
attr_reader :selection
|
|
10
|
+
|
|
11
|
+
def initialize(mode_manager, buffer, selection, register = nil, undo_manager: nil)
|
|
12
|
+
super(mode_manager, buffer)
|
|
13
|
+
@selection = selection
|
|
14
|
+
@register = register || Register.new
|
|
15
|
+
@undo_manager = undo_manager
|
|
16
|
+
@pending_motion = nil
|
|
17
|
+
@pending_register = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handle(key)
|
|
21
|
+
# Check plugin keymaps first (only when no pending motion)
|
|
22
|
+
unless @pending_motion
|
|
23
|
+
plugin_result = check_plugin_keymap(key, :visual)
|
|
24
|
+
return plugin_result if plugin_result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if @pending_motion
|
|
28
|
+
handle_pending_motion(key)
|
|
29
|
+
else
|
|
30
|
+
handle_visual_key(key)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def handle_visual_key(key)
|
|
37
|
+
case key
|
|
38
|
+
when KeyCode::ESCAPE
|
|
39
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
40
|
+
when "v"
|
|
41
|
+
handle_v_key
|
|
42
|
+
when "V"
|
|
43
|
+
handle_upper_v_key
|
|
44
|
+
when "h", Curses::KEY_LEFT
|
|
45
|
+
handle_move_left
|
|
46
|
+
when "j", Curses::KEY_DOWN
|
|
47
|
+
handle_move_down
|
|
48
|
+
when "k", Curses::KEY_UP
|
|
49
|
+
handle_move_up
|
|
50
|
+
when "l", Curses::KEY_RIGHT
|
|
51
|
+
handle_move_right
|
|
52
|
+
when "w"
|
|
53
|
+
handle_word_forward
|
|
54
|
+
when "b"
|
|
55
|
+
handle_word_backward
|
|
56
|
+
when "e"
|
|
57
|
+
handle_word_end
|
|
58
|
+
when "0"
|
|
59
|
+
handle_line_start
|
|
60
|
+
when "^"
|
|
61
|
+
handle_first_non_blank
|
|
62
|
+
when "$"
|
|
63
|
+
handle_line_end
|
|
64
|
+
when "g"
|
|
65
|
+
@pending_motion = :g
|
|
66
|
+
result
|
|
67
|
+
when "G"
|
|
68
|
+
handle_file_end
|
|
69
|
+
when "f"
|
|
70
|
+
@pending_motion = :f
|
|
71
|
+
result
|
|
72
|
+
when "F"
|
|
73
|
+
@pending_motion = :F
|
|
74
|
+
result
|
|
75
|
+
when "t"
|
|
76
|
+
@pending_motion = :t
|
|
77
|
+
result
|
|
78
|
+
when "T"
|
|
79
|
+
@pending_motion = :T
|
|
80
|
+
result
|
|
81
|
+
when "d"
|
|
82
|
+
handle_delete
|
|
83
|
+
when "c"
|
|
84
|
+
handle_change
|
|
85
|
+
when "y"
|
|
86
|
+
handle_yank
|
|
87
|
+
when ">"
|
|
88
|
+
handle_indent(:right)
|
|
89
|
+
when "<"
|
|
90
|
+
handle_indent(:left)
|
|
91
|
+
when '"'
|
|
92
|
+
@pending_motion = :register_select
|
|
93
|
+
result
|
|
94
|
+
when "*"
|
|
95
|
+
handle_search_selection(:forward)
|
|
96
|
+
when "#"
|
|
97
|
+
handle_search_selection(:backward)
|
|
98
|
+
else
|
|
99
|
+
result
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_v_key
|
|
104
|
+
# v in visual mode exits to normal mode
|
|
105
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_upper_v_key
|
|
109
|
+
# V in visual mode switches to visual line mode
|
|
110
|
+
result(mode: Mode::VISUAL_LINE, toggle_line_mode: true)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle_delete
|
|
114
|
+
range = @selection.normalized_range
|
|
115
|
+
if @selection.line_mode
|
|
116
|
+
delete_lines(range)
|
|
117
|
+
else
|
|
118
|
+
delete_range(range)
|
|
119
|
+
end
|
|
120
|
+
@pending_register = nil
|
|
121
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle_change
|
|
125
|
+
range = @selection.normalized_range
|
|
126
|
+
if @selection.line_mode
|
|
127
|
+
change_lines(range)
|
|
128
|
+
else
|
|
129
|
+
undo_manager&.begin_group
|
|
130
|
+
change_range(range)
|
|
131
|
+
end
|
|
132
|
+
@pending_register = nil
|
|
133
|
+
result(mode: Mode::INSERT, clear_selection: true, group_started: true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def handle_yank
|
|
137
|
+
range = @selection.normalized_range
|
|
138
|
+
if @selection.line_mode
|
|
139
|
+
yank_lines(range)
|
|
140
|
+
else
|
|
141
|
+
yank_range(range)
|
|
142
|
+
end
|
|
143
|
+
@pending_register = nil
|
|
144
|
+
self.cursor_row = range[:start_row]
|
|
145
|
+
self.cursor_col = range[:start_col]
|
|
146
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def handle_search_selection(direction)
|
|
150
|
+
range = @selection.normalized_range
|
|
151
|
+
text = if @selection.line_mode
|
|
152
|
+
# For line mode, use the full line content (trimmed)
|
|
153
|
+
buffer.line(range[:start_row]).strip
|
|
154
|
+
else
|
|
155
|
+
extract_selection_text(range)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
return result(mode: Mode::NORMAL, clear_selection: true) if text.empty?
|
|
159
|
+
|
|
160
|
+
# Escape special regex characters for literal search
|
|
161
|
+
escaped_pattern = Regexp.escape(text)
|
|
162
|
+
|
|
163
|
+
# Set search state
|
|
164
|
+
search_state = @mode_manager.search_state
|
|
165
|
+
search_state.set_pattern(escaped_pattern, direction)
|
|
166
|
+
|
|
167
|
+
# Find next/previous match from current position
|
|
168
|
+
match = if direction == :forward
|
|
169
|
+
search_state.find_next(cursor_row, cursor_col, buffer:)
|
|
170
|
+
else
|
|
171
|
+
search_state.find_previous(cursor_row, cursor_col, buffer:)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if match
|
|
175
|
+
window.cursor_row = match[:row]
|
|
176
|
+
window.cursor_col = match[:col]
|
|
177
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
178
|
+
else
|
|
179
|
+
result(mode: Mode::NORMAL, clear_selection: true, message: "Pattern not found: #{text}")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def yank_lines(range)
|
|
184
|
+
lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
|
|
185
|
+
@register.yank(lines.join("\n"), linewise: true, name: @pending_register)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def yank_range(range)
|
|
189
|
+
text = extract_selection_text(range)
|
|
190
|
+
@register.yank(text, linewise: false, name: @pending_register)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def extract_selection_text(range)
|
|
194
|
+
if range[:start_row] == range[:end_row]
|
|
195
|
+
buffer.line(range[:start_row])[range[:start_col]..range[:end_col]] || ""
|
|
196
|
+
else
|
|
197
|
+
lines = []
|
|
198
|
+
(range[:start_row]..range[:end_row]).each do |row|
|
|
199
|
+
line = buffer.line(row)
|
|
200
|
+
lines << if row == range[:start_row]
|
|
201
|
+
line[range[:start_col]..]
|
|
202
|
+
elsif row == range[:end_row]
|
|
203
|
+
line[0..range[:end_col]]
|
|
204
|
+
else
|
|
205
|
+
line
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
lines.join("\n")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def change_lines(range)
|
|
213
|
+
lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
|
|
214
|
+
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
215
|
+
undo_manager&.begin_group
|
|
216
|
+
(range[:end_row] - range[:start_row] + 1).times do
|
|
217
|
+
buffer.delete_line(range[:start_row])
|
|
218
|
+
end
|
|
219
|
+
buffer.insert_line(range[:start_row])
|
|
220
|
+
# NOTE: group will be closed when leaving Insert mode
|
|
221
|
+
self.cursor_row = range[:start_row]
|
|
222
|
+
self.cursor_col = 0
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def change_range(range)
|
|
226
|
+
text = extract_selection_text(range)
|
|
227
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
228
|
+
buffer.delete_range(range[:start_row], range[:start_col], range[:end_row], range[:end_col])
|
|
229
|
+
self.cursor_row = range[:start_row]
|
|
230
|
+
self.cursor_col = range[:start_col]
|
|
231
|
+
window.clamp_cursor_to_line(buffer)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def delete_lines(range)
|
|
235
|
+
lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
|
|
236
|
+
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
237
|
+
undo_manager&.begin_group unless undo_manager&.in_group?
|
|
238
|
+
(range[:end_row] - range[:start_row] + 1).times do
|
|
239
|
+
buffer.delete_line(range[:start_row])
|
|
240
|
+
end
|
|
241
|
+
undo_manager&.end_group
|
|
242
|
+
self.cursor_row = [range[:start_row], buffer.line_count - 1].min
|
|
243
|
+
self.cursor_col = 0
|
|
244
|
+
window.clamp_cursor_to_line(buffer)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def delete_range(range)
|
|
248
|
+
text = extract_selection_text(range)
|
|
249
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
250
|
+
buffer.delete_range(range[:start_row], range[:start_col], range[:end_row], range[:end_col])
|
|
251
|
+
self.cursor_row = range[:start_row]
|
|
252
|
+
self.cursor_col = range[:start_col]
|
|
253
|
+
window.clamp_cursor_to_line(buffer)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def handle_indent(direction)
|
|
257
|
+
range = @selection.normalized_range
|
|
258
|
+
indent_lines(range[:start_row], range[:end_row], direction)
|
|
259
|
+
|
|
260
|
+
# Move cursor to the beginning of the first selected line (Vim behavior)
|
|
261
|
+
self.cursor_row = range[:start_row]
|
|
262
|
+
self.cursor_col = 0
|
|
263
|
+
|
|
264
|
+
if Mui.config.get(:reselect_after_indent)
|
|
265
|
+
# Keep selection for continuous indent adjustment
|
|
266
|
+
@selection.update_end(range[:end_row], buffer.line(range[:end_row]).length)
|
|
267
|
+
result
|
|
268
|
+
else
|
|
269
|
+
result(mode: Mode::NORMAL, clear_selection: true)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def indent_lines(start_row, end_row, direction)
|
|
274
|
+
indent_string = build_indent_string
|
|
275
|
+
|
|
276
|
+
undo_manager&.begin_group unless undo_manager&.in_group?
|
|
277
|
+
|
|
278
|
+
(start_row..end_row).each do |row|
|
|
279
|
+
if direction == :right
|
|
280
|
+
add_indent(row, indent_string)
|
|
281
|
+
else
|
|
282
|
+
remove_indent(row, Mui.config.get(:shiftwidth))
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
undo_manager&.end_group
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def build_indent_string
|
|
290
|
+
if Mui.config.get(:expandtab)
|
|
291
|
+
" " * Mui.config.get(:shiftwidth)
|
|
292
|
+
else
|
|
293
|
+
"\t"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def add_indent(row, indent_string)
|
|
298
|
+
return if buffer.line(row).empty? # Skip empty lines
|
|
299
|
+
|
|
300
|
+
indent_string.reverse.each_char do |char|
|
|
301
|
+
buffer.insert_char(row, 0, char)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def remove_indent(row, width)
|
|
306
|
+
line = buffer.line(row)
|
|
307
|
+
return if line.empty? # Skip empty lines
|
|
308
|
+
|
|
309
|
+
removed = 0
|
|
310
|
+
|
|
311
|
+
while removed < width && !line.empty?
|
|
312
|
+
char = line[0]
|
|
313
|
+
break unless [" ", "\t"].include?(char)
|
|
314
|
+
|
|
315
|
+
char_width = char == "\t" ? Mui.config.get(:tabstop) : 1
|
|
316
|
+
break if removed + char_width > width && char == "\t"
|
|
317
|
+
|
|
318
|
+
buffer.delete_char(row, 0)
|
|
319
|
+
removed += char_width
|
|
320
|
+
line = buffer.line(row)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def handle_pending_motion(key)
|
|
325
|
+
char = key_to_char(key)
|
|
326
|
+
return clear_pending unless char
|
|
327
|
+
|
|
328
|
+
return handle_register_select(char) if @pending_motion == :register_select
|
|
329
|
+
|
|
330
|
+
motion_result = execute_pending_motion(char)
|
|
331
|
+
apply_motion(motion_result) if motion_result
|
|
332
|
+
clear_pending
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def handle_register_select(char)
|
|
336
|
+
if valid_register_name?(char)
|
|
337
|
+
@pending_register = char
|
|
338
|
+
@pending_motion = nil
|
|
339
|
+
result
|
|
340
|
+
else
|
|
341
|
+
clear_pending
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def valid_register_name?(char)
|
|
346
|
+
Register::NAMED_REGISTERS.include?(char) ||
|
|
347
|
+
Register::DELETE_HISTORY_REGISTERS.include?(char) ||
|
|
348
|
+
[Register::YANK_REGISTER, Register::BLACK_HOLE_REGISTER, Register::UNNAMED_REGISTER].include?(char)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def clear_pending
|
|
352
|
+
@pending_motion = nil
|
|
353
|
+
@pending_register = nil
|
|
354
|
+
result
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def handle_move_left
|
|
358
|
+
window.move_left
|
|
359
|
+
update_selection
|
|
360
|
+
result
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def handle_move_down
|
|
364
|
+
window.move_down
|
|
365
|
+
update_selection
|
|
366
|
+
result
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def handle_move_up
|
|
370
|
+
window.move_up
|
|
371
|
+
update_selection
|
|
372
|
+
result
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def handle_move_right
|
|
376
|
+
window.move_right
|
|
377
|
+
update_selection
|
|
378
|
+
result
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def apply_motion(motion_result)
|
|
382
|
+
super
|
|
383
|
+
update_selection if motion_result
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def update_selection
|
|
387
|
+
@selection.update_end(cursor_row, cursor_col)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def result(mode: nil, message: nil, quit: false, clear_selection: false, toggle_line_mode: false, group_started: false)
|
|
391
|
+
HandlerResult::VisualModeResult.new(
|
|
392
|
+
mode:,
|
|
393
|
+
message:,
|
|
394
|
+
quit:,
|
|
395
|
+
clear_selection:,
|
|
396
|
+
toggle_line_mode:,
|
|
397
|
+
group_started:
|
|
398
|
+
)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|