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