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
@@ -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