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,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
module Operators
|
|
6
|
+
# NOTE: ChangeOperator inherits from DeleteOperator.
|
|
7
|
+
# This aligns with Vim semantics (change = "delete then enter INSERT mode")
|
|
8
|
+
# and keeps the code simple.
|
|
9
|
+
# However, if changes to DeleteOperator cause bugs in ChangeOperator,
|
|
10
|
+
# consider switching to a delegation pattern
|
|
11
|
+
# (inherit from BaseOperator and delegate to a DeleteOperator instance).
|
|
12
|
+
|
|
13
|
+
# Handles change operator (c) in Normal mode
|
|
14
|
+
class ChangeOperator < DeleteOperator
|
|
15
|
+
# Handle pending change motion
|
|
16
|
+
def handle_pending(char, pending_register: nil)
|
|
17
|
+
@pending_register = pending_register
|
|
18
|
+
case char
|
|
19
|
+
when "c"
|
|
20
|
+
change_line
|
|
21
|
+
when "w"
|
|
22
|
+
change_motion(:word_forward)
|
|
23
|
+
when "e"
|
|
24
|
+
change_motion(:word_end)
|
|
25
|
+
when "b"
|
|
26
|
+
change_motion(:word_backward)
|
|
27
|
+
when "0"
|
|
28
|
+
change_to_line_start
|
|
29
|
+
when "$"
|
|
30
|
+
change_to_line_end
|
|
31
|
+
when "g"
|
|
32
|
+
:pending_cg
|
|
33
|
+
when "G"
|
|
34
|
+
change_to_file_end
|
|
35
|
+
when "f"
|
|
36
|
+
:pending_cf
|
|
37
|
+
when "F"
|
|
38
|
+
:pending_cF
|
|
39
|
+
when "t"
|
|
40
|
+
:pending_ct
|
|
41
|
+
when "T"
|
|
42
|
+
:pending_cT
|
|
43
|
+
else
|
|
44
|
+
:cancel
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Handle cg (change to file start with gg)
|
|
49
|
+
def handle_to_file_start(char)
|
|
50
|
+
return :cancel unless char == "g"
|
|
51
|
+
|
|
52
|
+
if cursor_row.zero?
|
|
53
|
+
change_to_line_start_internal
|
|
54
|
+
else
|
|
55
|
+
change_to_file_start_internal
|
|
56
|
+
end
|
|
57
|
+
:insert_mode
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Handle cf/cF/ct/cT (change with find char)
|
|
61
|
+
def handle_find_char(char, motion_type)
|
|
62
|
+
motion_result = case motion_type
|
|
63
|
+
when :cf
|
|
64
|
+
Motion.find_char_forward(@buffer, cursor_row, cursor_col, char)
|
|
65
|
+
when :cF
|
|
66
|
+
Motion.find_char_backward(@buffer, cursor_row, cursor_col, char)
|
|
67
|
+
when :ct
|
|
68
|
+
Motion.till_char_forward(@buffer, cursor_row, cursor_col, char)
|
|
69
|
+
when :cT
|
|
70
|
+
Motion.till_char_backward(@buffer, cursor_row, cursor_col, char)
|
|
71
|
+
end
|
|
72
|
+
return :cancel unless motion_result
|
|
73
|
+
|
|
74
|
+
execute_find_char_change(motion_result, motion_type)
|
|
75
|
+
:insert_mode
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def change_line
|
|
81
|
+
text = @buffer.line(cursor_row)
|
|
82
|
+
@register.delete(text, linewise: true, name: @pending_register)
|
|
83
|
+
@buffer.lines[cursor_row] = +""
|
|
84
|
+
self.cursor_col = 0
|
|
85
|
+
:insert_mode
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def change_motion(motion_type)
|
|
89
|
+
start_pos = { row: cursor_row, col: cursor_col }
|
|
90
|
+
# cw behaves like ce in Vim (changes to end of word, not to start of next word)
|
|
91
|
+
effective_motion = motion_type == :word_forward ? :word_end : motion_type
|
|
92
|
+
end_pos = calculate_motion_end(effective_motion)
|
|
93
|
+
return :cancel unless end_pos
|
|
94
|
+
|
|
95
|
+
inclusive = effective_motion == :word_end
|
|
96
|
+
text = extract_text(start_pos, end_pos, inclusive:)
|
|
97
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
98
|
+
execute_delete(start_pos, end_pos, inclusive:, clamp: false)
|
|
99
|
+
:insert_mode
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def change_to_line_start
|
|
103
|
+
return :insert_mode if cursor_col.zero?
|
|
104
|
+
|
|
105
|
+
text = @buffer.line(cursor_row)[0...cursor_col]
|
|
106
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
107
|
+
@buffer.delete_range(cursor_row, 0, cursor_row, cursor_col - 1)
|
|
108
|
+
self.cursor_col = 0
|
|
109
|
+
:insert_mode
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def change_to_line_end
|
|
113
|
+
line = @buffer.line(cursor_row)
|
|
114
|
+
return :insert_mode if line.empty?
|
|
115
|
+
|
|
116
|
+
text = line[cursor_col..]
|
|
117
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
118
|
+
end_col = line.length - 1
|
|
119
|
+
@buffer.delete_range(cursor_row, cursor_col, cursor_row, end_col)
|
|
120
|
+
# Don't clamp cursor - keep it at original position for insert mode
|
|
121
|
+
:insert_mode
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def change_to_file_end
|
|
125
|
+
last_row = @buffer.line_count - 1
|
|
126
|
+
if cursor_row == last_row
|
|
127
|
+
change_line
|
|
128
|
+
else
|
|
129
|
+
lines = (cursor_row..last_row).map { |r| @buffer.line(r) }
|
|
130
|
+
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
131
|
+
(last_row - cursor_row + 1).times { @buffer.delete_line(cursor_row) }
|
|
132
|
+
@buffer.insert_line(cursor_row) if @buffer.line_count == cursor_row
|
|
133
|
+
self.cursor_row = [cursor_row, @buffer.line_count - 1].min
|
|
134
|
+
self.cursor_col = 0
|
|
135
|
+
:insert_mode
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def change_to_line_start_internal
|
|
140
|
+
return if cursor_col.zero?
|
|
141
|
+
|
|
142
|
+
text = @buffer.line(cursor_row)[0...cursor_col]
|
|
143
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
144
|
+
@buffer.delete_range(cursor_row, 0, cursor_row, cursor_col - 1)
|
|
145
|
+
self.cursor_col = 0
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def change_to_file_start_internal
|
|
149
|
+
lines = (0..cursor_row).map { |r| @buffer.line(r) }
|
|
150
|
+
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
151
|
+
cursor_row.times { @buffer.delete_line(0) }
|
|
152
|
+
@buffer.delete_range(0, 0, 0, cursor_col - 1) if cursor_col.positive?
|
|
153
|
+
self.cursor_row = 0
|
|
154
|
+
self.cursor_col = 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def execute_find_char_change(motion_result, motion_type)
|
|
158
|
+
line = @buffer.line(cursor_row)
|
|
159
|
+
text = case motion_type
|
|
160
|
+
when :cf, :ct
|
|
161
|
+
line[cursor_col..motion_result[:col]]
|
|
162
|
+
when :cF, :cT
|
|
163
|
+
line[motion_result[:col]...cursor_col]
|
|
164
|
+
end
|
|
165
|
+
@register.delete(text, linewise: false, name: @pending_register) if text
|
|
166
|
+
|
|
167
|
+
case motion_type
|
|
168
|
+
when :cf, :ct
|
|
169
|
+
@buffer.delete_range(cursor_row, cursor_col, cursor_row, motion_result[:col])
|
|
170
|
+
when :cF, :cT
|
|
171
|
+
@buffer.delete_range(cursor_row, motion_result[:col], cursor_row, cursor_col - 1)
|
|
172
|
+
self.cursor_col = motion_result[:col]
|
|
173
|
+
end
|
|
174
|
+
# Don't clamp cursor - keep it at original position for insert mode
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles delete operator (d) in Normal mode
|
|
7
|
+
class DeleteOperator < BaseOperator
|
|
8
|
+
# Handle pending delete motion
|
|
9
|
+
def handle_pending(char, pending_register: nil)
|
|
10
|
+
@pending_register = pending_register
|
|
11
|
+
case char
|
|
12
|
+
when "d"
|
|
13
|
+
delete_line
|
|
14
|
+
when "w"
|
|
15
|
+
delete_motion(:word_forward)
|
|
16
|
+
when "e"
|
|
17
|
+
delete_motion(:word_end)
|
|
18
|
+
when "b"
|
|
19
|
+
delete_motion(:word_backward)
|
|
20
|
+
when "0"
|
|
21
|
+
delete_to_line_start
|
|
22
|
+
when "$"
|
|
23
|
+
delete_to_line_end
|
|
24
|
+
when "g"
|
|
25
|
+
:pending_dg
|
|
26
|
+
when "G"
|
|
27
|
+
delete_to_file_end
|
|
28
|
+
when "f"
|
|
29
|
+
:pending_df
|
|
30
|
+
when "F"
|
|
31
|
+
:pending_dF
|
|
32
|
+
when "t"
|
|
33
|
+
:pending_dt
|
|
34
|
+
when "T"
|
|
35
|
+
:pending_dT
|
|
36
|
+
else
|
|
37
|
+
:cancel
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Handle dg (delete to file start with gg)
|
|
42
|
+
def handle_to_file_start(char)
|
|
43
|
+
return :cancel unless char == "g"
|
|
44
|
+
|
|
45
|
+
if cursor_row.zero?
|
|
46
|
+
delete_to_line_start_internal
|
|
47
|
+
else
|
|
48
|
+
delete_to_file_start_internal
|
|
49
|
+
end
|
|
50
|
+
:done
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Handle df/dF/dt/dT (delete with find char)
|
|
54
|
+
def handle_find_char(char, motion_type)
|
|
55
|
+
motion_result = case motion_type
|
|
56
|
+
when :df
|
|
57
|
+
Motion.find_char_forward(@buffer, cursor_row, cursor_col, char)
|
|
58
|
+
when :dF
|
|
59
|
+
Motion.find_char_backward(@buffer, cursor_row, cursor_col, char)
|
|
60
|
+
when :dt
|
|
61
|
+
Motion.till_char_forward(@buffer, cursor_row, cursor_col, char)
|
|
62
|
+
when :dT
|
|
63
|
+
Motion.till_char_backward(@buffer, cursor_row, cursor_col, char)
|
|
64
|
+
end
|
|
65
|
+
return :cancel unless motion_result
|
|
66
|
+
|
|
67
|
+
execute_find_char_delete(motion_result, motion_type)
|
|
68
|
+
:done
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
protected
|
|
72
|
+
|
|
73
|
+
def delete_line
|
|
74
|
+
text = @buffer.line(cursor_row)
|
|
75
|
+
@register.delete(text, linewise: true, name: @pending_register)
|
|
76
|
+
@buffer.delete_line(cursor_row)
|
|
77
|
+
self.cursor_row = [cursor_row, @buffer.line_count - 1].min
|
|
78
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
79
|
+
:done
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def delete_motion(motion_type)
|
|
83
|
+
start_pos = { row: cursor_row, col: cursor_col }
|
|
84
|
+
end_pos = calculate_motion_end(motion_type)
|
|
85
|
+
return :cancel unless end_pos
|
|
86
|
+
|
|
87
|
+
inclusive = motion_type == :word_end
|
|
88
|
+
text = extract_text(start_pos, end_pos, inclusive:)
|
|
89
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
90
|
+
execute_delete(start_pos, end_pos, inclusive:)
|
|
91
|
+
:done
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete_to_line_start
|
|
95
|
+
return :done if cursor_col.zero?
|
|
96
|
+
|
|
97
|
+
text = @buffer.line(cursor_row)[0...cursor_col]
|
|
98
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
99
|
+
@buffer.delete_range(cursor_row, 0, cursor_row, cursor_col - 1)
|
|
100
|
+
self.cursor_col = 0
|
|
101
|
+
:done
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def delete_to_line_end
|
|
105
|
+
line = @buffer.line(cursor_row)
|
|
106
|
+
return :done if line.empty?
|
|
107
|
+
|
|
108
|
+
text = line[cursor_col..]
|
|
109
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
110
|
+
end_col = line.length - 1
|
|
111
|
+
@buffer.delete_range(cursor_row, cursor_col, cursor_row, end_col)
|
|
112
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
113
|
+
:done
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def delete_to_file_end
|
|
117
|
+
last_row = @buffer.line_count - 1
|
|
118
|
+
if cursor_row == last_row
|
|
119
|
+
delete_line
|
|
120
|
+
else
|
|
121
|
+
lines = (cursor_row..last_row).map { |r| @buffer.line(r) }
|
|
122
|
+
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
123
|
+
@undo_manager&.begin_group
|
|
124
|
+
(last_row - cursor_row + 1).times { @buffer.delete_line(cursor_row) }
|
|
125
|
+
@undo_manager&.end_group
|
|
126
|
+
self.cursor_row = [cursor_row, @buffer.line_count - 1].min
|
|
127
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
128
|
+
:done
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def delete_to_line_start_internal
|
|
135
|
+
return if cursor_col.zero?
|
|
136
|
+
|
|
137
|
+
text = @buffer.line(cursor_row)[0...cursor_col]
|
|
138
|
+
@register.delete(text, linewise: false, name: @pending_register)
|
|
139
|
+
@buffer.delete_range(cursor_row, 0, cursor_row, cursor_col - 1)
|
|
140
|
+
self.cursor_col = 0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def delete_to_file_start_internal
|
|
144
|
+
lines = (0..cursor_row).map { |r| @buffer.line(r) }
|
|
145
|
+
@register.delete(lines.join("\n"), linewise: true, name: @pending_register)
|
|
146
|
+
@undo_manager&.begin_group
|
|
147
|
+
cursor_row.times { @buffer.delete_line(0) }
|
|
148
|
+
@buffer.delete_range(0, 0, 0, cursor_col - 1) if cursor_col.positive?
|
|
149
|
+
@undo_manager&.end_group
|
|
150
|
+
self.cursor_row = 0
|
|
151
|
+
self.cursor_col = 0
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def execute_find_char_delete(motion_result, motion_type)
|
|
155
|
+
line = @buffer.line(cursor_row)
|
|
156
|
+
text = case motion_type
|
|
157
|
+
when :df, :dt
|
|
158
|
+
line[cursor_col..motion_result[:col]]
|
|
159
|
+
when :dF, :dT
|
|
160
|
+
line[motion_result[:col]...cursor_col]
|
|
161
|
+
end
|
|
162
|
+
@register.delete(text, linewise: false, name: @pending_register) if text
|
|
163
|
+
|
|
164
|
+
case motion_type
|
|
165
|
+
when :df, :dt
|
|
166
|
+
@buffer.delete_range(cursor_row, cursor_col, cursor_row, motion_result[:col])
|
|
167
|
+
when :dF, :dT
|
|
168
|
+
@buffer.delete_range(cursor_row, motion_result[:col], cursor_row, cursor_col - 1)
|
|
169
|
+
self.cursor_col = motion_result[:col]
|
|
170
|
+
end
|
|
171
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles paste operator (p/P) in Normal mode
|
|
7
|
+
class PasteOperator < BaseOperator
|
|
8
|
+
# Paste after cursor (p)
|
|
9
|
+
def paste_after(pending_register: nil)
|
|
10
|
+
return :done if @register.empty?(name: pending_register)
|
|
11
|
+
|
|
12
|
+
if @register.linewise?(name: pending_register)
|
|
13
|
+
paste_line_after(name: pending_register)
|
|
14
|
+
else
|
|
15
|
+
paste_char_after(name: pending_register)
|
|
16
|
+
end
|
|
17
|
+
:done
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Paste before cursor (P)
|
|
21
|
+
def paste_before(pending_register: nil)
|
|
22
|
+
return :done if @register.empty?(name: pending_register)
|
|
23
|
+
|
|
24
|
+
if @register.linewise?(name: pending_register)
|
|
25
|
+
paste_line_before(name: pending_register)
|
|
26
|
+
else
|
|
27
|
+
paste_char_before(name: pending_register)
|
|
28
|
+
end
|
|
29
|
+
:done
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Not used for paste, but required by base class interface
|
|
33
|
+
def handle_pending(_char, pending_register: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
34
|
+
:cancel
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def paste_line_after(name: nil)
|
|
40
|
+
text = @register.get(name:)
|
|
41
|
+
lines = text.split("\n", -1)
|
|
42
|
+
lines.reverse_each do |line|
|
|
43
|
+
@buffer.insert_line(cursor_row + 1, line)
|
|
44
|
+
end
|
|
45
|
+
self.cursor_row = cursor_row + 1
|
|
46
|
+
self.cursor_col = 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def paste_line_before(name: nil)
|
|
50
|
+
text = @register.get(name:)
|
|
51
|
+
lines = text.split("\n", -1)
|
|
52
|
+
lines.reverse_each do |line|
|
|
53
|
+
@buffer.insert_line(cursor_row, line)
|
|
54
|
+
end
|
|
55
|
+
self.cursor_col = 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def paste_char_after(name: nil)
|
|
59
|
+
text = @register.get(name:)
|
|
60
|
+
line = @buffer.line(cursor_row)
|
|
61
|
+
insert_col = line.empty? ? 0 : cursor_col + 1
|
|
62
|
+
|
|
63
|
+
if text.include?("\n")
|
|
64
|
+
paste_multiline_char(text, line, insert_col)
|
|
65
|
+
else
|
|
66
|
+
@buffer.lines[cursor_row] = line[0...insert_col].to_s + text + line[insert_col..].to_s
|
|
67
|
+
self.cursor_col = insert_col + text.length - 1
|
|
68
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def paste_char_before(name: nil)
|
|
73
|
+
text = @register.get(name:)
|
|
74
|
+
line = @buffer.line(cursor_row)
|
|
75
|
+
|
|
76
|
+
if text.include?("\n")
|
|
77
|
+
paste_multiline_char(text, line, cursor_col)
|
|
78
|
+
else
|
|
79
|
+
@buffer.lines[cursor_row] = line[0...cursor_col].to_s + text + line[cursor_col..].to_s
|
|
80
|
+
self.cursor_col = cursor_col + text.length - 1
|
|
81
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def paste_multiline_char(text, line, insert_col)
|
|
86
|
+
lines = text.split("\n", -1)
|
|
87
|
+
before = line[0...insert_col].to_s
|
|
88
|
+
after = line[insert_col..].to_s
|
|
89
|
+
|
|
90
|
+
# First line: before + first part of pasted text
|
|
91
|
+
@buffer.lines[cursor_row] = before + lines.first
|
|
92
|
+
|
|
93
|
+
# Middle lines: insert as new lines
|
|
94
|
+
lines[1...-1].each_with_index do |pasted_line, idx|
|
|
95
|
+
@buffer.insert_line(cursor_row + 1 + idx, pasted_line)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Last line: last part of pasted text + after
|
|
99
|
+
if lines.length > 1
|
|
100
|
+
last_line_row = cursor_row + lines.length - 1
|
|
101
|
+
@buffer.insert_line(last_line_row, lines.last + after)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Position cursor at the end of pasted text (before 'after' part)
|
|
105
|
+
self.cursor_row = cursor_row + lines.length - 1
|
|
106
|
+
self.cursor_col = lines.last.length - 1
|
|
107
|
+
self.cursor_col = 0 if cursor_col.negative?
|
|
108
|
+
@window.clamp_cursor_to_line(@buffer)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module KeyHandler
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles yank operator (y) in Normal mode
|
|
7
|
+
# Unlike delete/change, yank does not modify the buffer
|
|
8
|
+
class YankOperator < BaseOperator
|
|
9
|
+
# Handle pending yank motion
|
|
10
|
+
def handle_pending(char, pending_register: nil)
|
|
11
|
+
@pending_register = pending_register
|
|
12
|
+
case char
|
|
13
|
+
when "y"
|
|
14
|
+
yank_line
|
|
15
|
+
when "w"
|
|
16
|
+
yank_motion(:word_forward)
|
|
17
|
+
when "e"
|
|
18
|
+
yank_motion(:word_end)
|
|
19
|
+
when "b"
|
|
20
|
+
yank_motion(:word_backward)
|
|
21
|
+
when "0"
|
|
22
|
+
yank_to_line_start
|
|
23
|
+
when "$"
|
|
24
|
+
yank_to_line_end
|
|
25
|
+
when "g"
|
|
26
|
+
:pending_yg
|
|
27
|
+
when "G"
|
|
28
|
+
yank_to_file_end
|
|
29
|
+
when "f"
|
|
30
|
+
:pending_yf
|
|
31
|
+
when "F"
|
|
32
|
+
:pending_yF
|
|
33
|
+
when "t"
|
|
34
|
+
:pending_yt
|
|
35
|
+
when "T"
|
|
36
|
+
:pending_yT
|
|
37
|
+
else
|
|
38
|
+
:cancel
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Handle yg (yank to file start with gg)
|
|
43
|
+
def handle_to_file_start(char)
|
|
44
|
+
return :cancel unless char == "g"
|
|
45
|
+
|
|
46
|
+
lines = (0..cursor_row).map { |r| @buffer.line(r) }
|
|
47
|
+
text = lines.join("\n")
|
|
48
|
+
@register.yank(text, linewise: true, name: @pending_register)
|
|
49
|
+
:done
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handle yf/yF/yt/yT (yank with find char)
|
|
53
|
+
def handle_find_char(char, motion_type)
|
|
54
|
+
motion_result = case motion_type
|
|
55
|
+
when :yf
|
|
56
|
+
Motion.find_char_forward(@buffer, cursor_row, cursor_col, char)
|
|
57
|
+
when :yF
|
|
58
|
+
Motion.find_char_backward(@buffer, cursor_row, cursor_col, char)
|
|
59
|
+
when :yt
|
|
60
|
+
Motion.till_char_forward(@buffer, cursor_row, cursor_col, char)
|
|
61
|
+
when :yT
|
|
62
|
+
Motion.till_char_backward(@buffer, cursor_row, cursor_col, char)
|
|
63
|
+
end
|
|
64
|
+
return :cancel unless motion_result
|
|
65
|
+
|
|
66
|
+
execute_find_char_yank(motion_result, motion_type)
|
|
67
|
+
:done
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def yank_line
|
|
73
|
+
text = @buffer.line(cursor_row)
|
|
74
|
+
@register.yank(text, linewise: true, name: @pending_register)
|
|
75
|
+
:done
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def yank_motion(motion_type)
|
|
79
|
+
start_pos = { row: cursor_row, col: cursor_col }
|
|
80
|
+
effective_motion = motion_type == :word_forward ? :word_end : motion_type
|
|
81
|
+
end_pos = calculate_motion_end(effective_motion)
|
|
82
|
+
return :cancel unless end_pos
|
|
83
|
+
|
|
84
|
+
inclusive = effective_motion == :word_end
|
|
85
|
+
text = extract_text(start_pos, end_pos, inclusive:)
|
|
86
|
+
@register.yank(text, linewise: false, name: @pending_register)
|
|
87
|
+
:done
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def yank_to_line_start
|
|
91
|
+
return :done if cursor_col.zero?
|
|
92
|
+
|
|
93
|
+
text = @buffer.line(cursor_row)[0...cursor_col]
|
|
94
|
+
@register.yank(text, linewise: false, name: @pending_register)
|
|
95
|
+
:done
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def yank_to_line_end
|
|
99
|
+
line = @buffer.line(cursor_row)
|
|
100
|
+
return :done if line.empty?
|
|
101
|
+
|
|
102
|
+
text = line[cursor_col..]
|
|
103
|
+
@register.yank(text, linewise: false, name: @pending_register)
|
|
104
|
+
:done
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def yank_to_file_end
|
|
108
|
+
lines = (cursor_row...@buffer.line_count).map { |r| @buffer.line(r) }
|
|
109
|
+
text = lines.join("\n")
|
|
110
|
+
@register.yank(text, linewise: true, name: @pending_register)
|
|
111
|
+
:done
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def execute_find_char_yank(motion_result, motion_type)
|
|
115
|
+
line = @buffer.line(cursor_row)
|
|
116
|
+
text = case motion_type
|
|
117
|
+
when :yf, :yt
|
|
118
|
+
line[cursor_col..motion_result[:col]]
|
|
119
|
+
when :yF, :yT
|
|
120
|
+
line[motion_result[:col]...cursor_col]
|
|
121
|
+
end
|
|
122
|
+
@register.yank(text, linewise: false, name: @pending_register)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|