kward 0.71.0 → 0.72.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/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +387 -35
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -0,0 +1,1962 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Vibe-style keymap for the built-in composer file editor.
|
|
6
|
+
module VibeEditorMode
|
|
7
|
+
VIBE_SIMPLE_MOTION_KEYS = [
|
|
8
|
+
"w", "e", "b", "$", "0", "^", "+", "\n", "\r", "-", "_",
|
|
9
|
+
"h", "\b", "\x7F", "j", "k", "l", " ", "{", "}"
|
|
10
|
+
].freeze
|
|
11
|
+
VIBE_PAIR_TEXT_OBJECTS = {
|
|
12
|
+
"(" => ["(", ")"], ")" => ["(", ")"], "b" => ["(", ")"],
|
|
13
|
+
"[" => ["[", "]"], "]" => ["[", "]"],
|
|
14
|
+
"{" => ["{", "}"], "}" => ["{", "}"], "B" => ["{", "}"],
|
|
15
|
+
"\"" => ["\"", "\""], "'" => ["'", "'"]
|
|
16
|
+
}.freeze
|
|
17
|
+
VIBE_RUBY_BLOCK_OPENERS = %w[if unless case while until for def module class do begin].freeze
|
|
18
|
+
VIBE_RUBY_PATHS = %w[Gemfile Rakefile Guardfile Capfile Vagrantfile].freeze
|
|
19
|
+
VIBE_RUBY_EXTENSIONS = %w[.rb .rake .gemspec].freeze
|
|
20
|
+
|
|
21
|
+
VibeOperatorTarget = Struct.new(:type, :start_index, :end_index, :replacement_text, :replacement_cursor_offset, keyword_init: true) do
|
|
22
|
+
def characterwise?
|
|
23
|
+
type == :characterwise
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def change_replacement_text
|
|
27
|
+
replacement_text.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def change_cursor_index
|
|
31
|
+
start_index + replacement_cursor_offset.to_i
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def handle_vibe_key(key)
|
|
38
|
+
csi_result = handle_vibe_csi_u_key(key)
|
|
39
|
+
return csi_result unless csi_result == false
|
|
40
|
+
|
|
41
|
+
tab_result = handle_tab_key_binding(key)
|
|
42
|
+
return tab_result unless tab_result == false
|
|
43
|
+
|
|
44
|
+
return vibe_stop_macro_recording if key == "q" && @editor_state.vibe_recording_macro && !%w[insert replace command].include?(@editor_state.vibe_mode)
|
|
45
|
+
vibe_record_macro_key(key)
|
|
46
|
+
return vibe_begin_visual_mode("visual_block") if key == "\x16" && @editor_state.vibe_mode == "normal"
|
|
47
|
+
return handle_vibe_repeat_change if key == "." && @editor_state.vibe_mode == "normal"
|
|
48
|
+
return handle_vibe_search_key(key) if editor_search_active?
|
|
49
|
+
return handle_vibe_command_key(key) if @editor_state.vibe_mode == "command"
|
|
50
|
+
return handle_vibe_insert_key(key) if @editor_state.vibe_mode == "insert"
|
|
51
|
+
return handle_vibe_replace_key(key) if @editor_state.vibe_mode == "replace"
|
|
52
|
+
return handle_vibe_visual_key(key) if vibe_visual_mode?
|
|
53
|
+
|
|
54
|
+
handle_vibe_normal_key(key)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_vibe_csi_u_key(key)
|
|
58
|
+
sequence = parse_csi_u_key(key)
|
|
59
|
+
return false unless sequence
|
|
60
|
+
|
|
61
|
+
code = sequence[:code]
|
|
62
|
+
modifier = sequence[:modifier]
|
|
63
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
64
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
65
|
+
if ctrl_modifier?(modifier) && code == 13 && %w[insert replace].include?(@editor_state.vibe_mode)
|
|
66
|
+
return vibe_record_undo { editor_insert_endwise_modifier_newline }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if @editor_state.vibe_mode == "normal" && ctrl_modifier?(modifier)
|
|
70
|
+
ctrl_result = handle_vibe_normal_ctrl_key(normalized_code)
|
|
71
|
+
return ctrl_result unless ctrl_result == false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
logical_key = vibe_csi_u_logical_key(sequence)
|
|
75
|
+
return handle_vibe_key(logical_key) if logical_key
|
|
76
|
+
return false unless code == 27 || (ctrl_modifier?(modifier) && normalized_code == 99)
|
|
77
|
+
|
|
78
|
+
return editor_search_cancel if editor_search_active?
|
|
79
|
+
|
|
80
|
+
@editor_state.vibe_command = ""
|
|
81
|
+
@editor_state.vibe_pending = ""
|
|
82
|
+
@editor_state.clear_selection
|
|
83
|
+
vibe_return_to_normal
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_vibe_normal_ctrl_key(normalized_code)
|
|
87
|
+
case normalized_code
|
|
88
|
+
when 104
|
|
89
|
+
@editor_state.move_line_first_non_blank
|
|
90
|
+
when 106
|
|
91
|
+
@editor_state.move_indentation_down
|
|
92
|
+
when 107
|
|
93
|
+
@editor_state.move_indentation_up
|
|
94
|
+
when 108
|
|
95
|
+
@editor_state.move_line_end
|
|
96
|
+
when 118
|
|
97
|
+
vibe_begin_visual_mode("visual_block")
|
|
98
|
+
else
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def vibe_csi_u_logical_key(sequence)
|
|
104
|
+
code = sequence[:code]
|
|
105
|
+
text = csi_u_text(sequence)
|
|
106
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
107
|
+
return "\n" if code == 13
|
|
108
|
+
return "\x7F" if [8, 127].include?(code)
|
|
109
|
+
return (normalized_code - 96).chr if ctrl_modifier?(sequence[:modifier]) && normalized_code.between?(97, 122)
|
|
110
|
+
return text if text.length == 1 && printable_key?(text)
|
|
111
|
+
return code.chr(Encoding::UTF_8) if sequence[:modifier] == 1 && code.between?(32, 126)
|
|
112
|
+
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def handle_vibe_search_key(key)
|
|
117
|
+
case key
|
|
118
|
+
when "\n", "\r"
|
|
119
|
+
editor_search_confirm
|
|
120
|
+
when "\b", "\x7F"
|
|
121
|
+
editor_search_delete_character
|
|
122
|
+
when "\e", "\x03"
|
|
123
|
+
editor_search_cancel
|
|
124
|
+
else
|
|
125
|
+
editor_search_append(key) if printable_key?(key)
|
|
126
|
+
end
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def handle_vibe_insert_key(key)
|
|
131
|
+
return if handle_editor_bracketed_paste_key(key)
|
|
132
|
+
|
|
133
|
+
vibe_record_insert_change_key(key)
|
|
134
|
+
tab_result = handle_editor_tab_key(key) { |direction| vibe_record_undo { direction == :forward ? editor_insert_tab : editor_outdent_tab } }
|
|
135
|
+
return tab_result unless tab_result == false
|
|
136
|
+
|
|
137
|
+
case key
|
|
138
|
+
when "\e", "\x03", :escape
|
|
139
|
+
vibe_return_to_normal
|
|
140
|
+
when "\b", "\x7F"
|
|
141
|
+
vibe_record_undo { editor_delete_before_cursor }
|
|
142
|
+
when "\n", "\r"
|
|
143
|
+
vibe_record_undo { editor_insert_newline }
|
|
144
|
+
else
|
|
145
|
+
readline_result = handle_vibe_insert_readline_key(key)
|
|
146
|
+
return readline_result unless readline_result == false || readline_result.nil?
|
|
147
|
+
|
|
148
|
+
key_name = key_name_for(key)
|
|
149
|
+
named_result = handle_vibe_insert_named_key(key_name) if key_name
|
|
150
|
+
return named_result unless named_result == false || named_result.nil?
|
|
151
|
+
|
|
152
|
+
vibe_record_undo { editor_insert_printable(key) } if printable_key?(key)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_vibe_insert_readline_key(key)
|
|
157
|
+
csi_result = handle_vibe_insert_readline_csi_u_key(key)
|
|
158
|
+
return csi_result unless csi_result == false
|
|
159
|
+
|
|
160
|
+
handle_vibe_insert_readline_ansi_key(key)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def handle_vibe_insert_readline_csi_u_key(key)
|
|
164
|
+
sequence = parse_csi_u_key(key)
|
|
165
|
+
return false unless sequence
|
|
166
|
+
|
|
167
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
168
|
+
modifier = sequence[:modifier]
|
|
169
|
+
normalized_code = sequence[:code].to_i.chr.downcase.ord rescue sequence[:code]
|
|
170
|
+
if ctrl_modifier?(modifier)
|
|
171
|
+
return handle_vibe_insert_readline_ctrl_key(normalized_code)
|
|
172
|
+
elsif alt_modifier?(modifier)
|
|
173
|
+
return handle_vibe_insert_readline_alt_key(normalized_code)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def handle_vibe_insert_readline_ansi_key(key)
|
|
180
|
+
case key
|
|
181
|
+
when "\x01"
|
|
182
|
+
@editor_state.move_line_start
|
|
183
|
+
when "\x02"
|
|
184
|
+
@editor_state.move_left
|
|
185
|
+
when "\x04"
|
|
186
|
+
vibe_record_undo { @editor_state.delete_at_cursor }
|
|
187
|
+
when "\x05"
|
|
188
|
+
@editor_state.move_line_end
|
|
189
|
+
when "\x06"
|
|
190
|
+
@editor_state.move_right
|
|
191
|
+
when "\x0B"
|
|
192
|
+
vibe_record_undo { @editor_state.kill_line_after_cursor }
|
|
193
|
+
when "\x15"
|
|
194
|
+
vibe_record_undo { @editor_state.kill_line_before_cursor }
|
|
195
|
+
when "\x17"
|
|
196
|
+
vibe_record_undo { @editor_state.delete_word_before_cursor }
|
|
197
|
+
when "\x19"
|
|
198
|
+
vibe_record_undo { @editor_state.yank_kill_buffer }
|
|
199
|
+
when "\e[D", "\eOD"
|
|
200
|
+
@editor_state.move_left
|
|
201
|
+
when "\e[C", "\eOC"
|
|
202
|
+
@editor_state.move_right
|
|
203
|
+
when "\e[H", "\eOH", "\e[1~", "\e[7~"
|
|
204
|
+
@editor_state.move_line_start
|
|
205
|
+
when "\e[F", "\eOF", "\e[4~", "\e[8~"
|
|
206
|
+
@editor_state.move_line_end
|
|
207
|
+
when "\e[3~"
|
|
208
|
+
vibe_record_undo { @editor_state.delete_at_cursor }
|
|
209
|
+
when "\eb", "\eB"
|
|
210
|
+
@editor_state.move_to_previous_word
|
|
211
|
+
when "\ef", "\eF"
|
|
212
|
+
@editor_state.move_to_next_word
|
|
213
|
+
when "\ed", "\eD"
|
|
214
|
+
vibe_record_undo { @editor_state.delete_word_after_cursor }
|
|
215
|
+
when "\e\b", "\e\x7F"
|
|
216
|
+
vibe_record_undo { @editor_state.delete_word_before_cursor }
|
|
217
|
+
else
|
|
218
|
+
handle_vibe_insert_modified_ansi_key(key)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def handle_vibe_insert_readline_ctrl_key(normalized_code)
|
|
223
|
+
case normalized_code
|
|
224
|
+
when 97
|
|
225
|
+
@editor_state.move_line_start
|
|
226
|
+
when 98
|
|
227
|
+
@editor_state.move_left
|
|
228
|
+
when 100
|
|
229
|
+
vibe_record_undo { @editor_state.delete_at_cursor }
|
|
230
|
+
when 101
|
|
231
|
+
@editor_state.move_line_end
|
|
232
|
+
when 102
|
|
233
|
+
@editor_state.move_right
|
|
234
|
+
when 107
|
|
235
|
+
vibe_record_undo { @editor_state.kill_line_after_cursor }
|
|
236
|
+
when 117
|
|
237
|
+
vibe_record_undo { @editor_state.kill_line_before_cursor }
|
|
238
|
+
when 119
|
|
239
|
+
vibe_record_undo { @editor_state.delete_word_before_cursor }
|
|
240
|
+
when 121
|
|
241
|
+
vibe_record_undo { @editor_state.yank_kill_buffer }
|
|
242
|
+
else
|
|
243
|
+
false
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def handle_vibe_insert_readline_alt_key(normalized_code)
|
|
248
|
+
case normalized_code
|
|
249
|
+
when 98
|
|
250
|
+
@editor_state.move_to_previous_word
|
|
251
|
+
when 100
|
|
252
|
+
vibe_record_undo { @editor_state.delete_word_after_cursor }
|
|
253
|
+
when 102
|
|
254
|
+
@editor_state.move_to_next_word
|
|
255
|
+
else
|
|
256
|
+
false
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def handle_vibe_insert_modified_ansi_key(key)
|
|
261
|
+
sequence = parse_modified_ansi_key(key)
|
|
262
|
+
return false unless sequence
|
|
263
|
+
|
|
264
|
+
case sequence[:type]
|
|
265
|
+
when :cursor
|
|
266
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
267
|
+
|
|
268
|
+
case sequence[:final]
|
|
269
|
+
when "C"
|
|
270
|
+
@editor_state.move_to_next_word
|
|
271
|
+
when "D"
|
|
272
|
+
@editor_state.move_to_previous_word
|
|
273
|
+
when "F"
|
|
274
|
+
@editor_state.move_line_end
|
|
275
|
+
when "H"
|
|
276
|
+
@editor_state.move_line_start
|
|
277
|
+
else
|
|
278
|
+
false
|
|
279
|
+
end
|
|
280
|
+
when :delete
|
|
281
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
282
|
+
|
|
283
|
+
vibe_record_undo { @editor_state.delete_word_after_cursor }
|
|
284
|
+
else
|
|
285
|
+
false
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def handle_vibe_insert_named_key(key_name)
|
|
290
|
+
case key_name
|
|
291
|
+
when :escape
|
|
292
|
+
vibe_return_to_normal
|
|
293
|
+
when :return, :enter
|
|
294
|
+
vibe_record_undo { editor_insert_newline }
|
|
295
|
+
when :backspace
|
|
296
|
+
vibe_record_undo { editor_delete_before_cursor }
|
|
297
|
+
when :delete
|
|
298
|
+
vibe_record_undo { @editor_state.delete_at_cursor }
|
|
299
|
+
when :left
|
|
300
|
+
@editor_state.move_left
|
|
301
|
+
when :right
|
|
302
|
+
@editor_state.move_right
|
|
303
|
+
when :up
|
|
304
|
+
editor_move_up
|
|
305
|
+
when :down
|
|
306
|
+
editor_move_down
|
|
307
|
+
else
|
|
308
|
+
false
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def handle_vibe_replace_key(key)
|
|
313
|
+
return if handle_editor_bracketed_paste_key(key)
|
|
314
|
+
|
|
315
|
+
vibe_record_insert_change_key(key)
|
|
316
|
+
tab_result = handle_editor_tab_key(key) { |direction| vibe_record_undo { direction == :forward ? editor_insert_tab : editor_outdent_tab } }
|
|
317
|
+
return tab_result unless tab_result == false
|
|
318
|
+
|
|
319
|
+
case key
|
|
320
|
+
when "\e", "\x03", :escape
|
|
321
|
+
vibe_return_to_normal
|
|
322
|
+
when "\b", "\x7F"
|
|
323
|
+
vibe_record_undo { editor_delete_before_cursor }
|
|
324
|
+
when "\n", "\r"
|
|
325
|
+
vibe_record_undo { editor_insert_newline }
|
|
326
|
+
else
|
|
327
|
+
key_name = key_name_for(key)
|
|
328
|
+
named_result = handle_vibe_insert_named_key(key_name) if key_name
|
|
329
|
+
return named_result unless named_result == false || named_result.nil?
|
|
330
|
+
|
|
331
|
+
vibe_record_undo { vibe_replace_character(key) } if printable_key?(key)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def vibe_replace_character(key)
|
|
336
|
+
@editor_state.delete_at_cursor
|
|
337
|
+
editor_insert_printable(key)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def handle_vibe_command_key(key)
|
|
341
|
+
case key
|
|
342
|
+
when "\e", "\x03", :escape
|
|
343
|
+
@editor_state.vibe_command = ""
|
|
344
|
+
vibe_return_to_normal
|
|
345
|
+
when "\b", "\x7F"
|
|
346
|
+
@editor_state.vibe_command = @editor_state.vibe_command[0...-1].to_s
|
|
347
|
+
@editor_state.status = ":#{@editor_state.vibe_command}"
|
|
348
|
+
when "\n", "\r"
|
|
349
|
+
execute_vibe_command(@editor_state.vibe_command)
|
|
350
|
+
else
|
|
351
|
+
if printable_key?(key)
|
|
352
|
+
@editor_state.vibe_command = @editor_state.vibe_command.to_s + key
|
|
353
|
+
@editor_state.status = ":#{@editor_state.vibe_command}"
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
true
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def execute_vibe_command(command)
|
|
360
|
+
command = command.to_s.strip
|
|
361
|
+
@editor_state.vibe_mode = "normal"
|
|
362
|
+
@editor_state.vibe_command = ""
|
|
363
|
+
case command
|
|
364
|
+
when "w"
|
|
365
|
+
save_editor
|
|
366
|
+
when "q"
|
|
367
|
+
vibe_quit_editor
|
|
368
|
+
when "q!"
|
|
369
|
+
close_editor
|
|
370
|
+
when "wq"
|
|
371
|
+
save_editor && close_editor
|
|
372
|
+
when "x"
|
|
373
|
+
save_editor if @editor_state&.dirty?
|
|
374
|
+
close_editor if @editor_state
|
|
375
|
+
when /\A(?:(%|\d+,\d+))?s\/([^\/]*)\/([^\/]*)\/(g?)\z/
|
|
376
|
+
vibe_substitute_command(Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3), global: Regexp.last_match(4) == "g")
|
|
377
|
+
when /\A\d+\z/
|
|
378
|
+
@editor_state.set_cursor_line_and_column(command.to_i - 1, 0)
|
|
379
|
+
@editor_state.status = "Line #{command}"
|
|
380
|
+
else
|
|
381
|
+
@editor_state.status = "Unknown command: #{command}"
|
|
382
|
+
end
|
|
383
|
+
true
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def vibe_substitute_command(range, pattern, replacement, global: false)
|
|
387
|
+
if pattern.empty?
|
|
388
|
+
@editor_state.status = "Substitute pattern required"
|
|
389
|
+
return false
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
start_line = 0
|
|
393
|
+
end_line = @editor_state.lines.length - 1
|
|
394
|
+
if range&.include?(",")
|
|
395
|
+
start_line, end_line = range.split(",", 2).map { |value| value.to_i - 1 }
|
|
396
|
+
end
|
|
397
|
+
start_line = [[start_line, 0].max, @editor_state.lines.length - 1].min
|
|
398
|
+
end_line = [[end_line, 0].max, @editor_state.lines.length - 1].min
|
|
399
|
+
start_line, end_line = [start_line, end_line].minmax
|
|
400
|
+
start_index = @editor_state.line_range(start_line)[0]
|
|
401
|
+
end_index = @editor_state.line_range(end_line)[1]
|
|
402
|
+
text = @editor_state.buffer[start_index...end_index].to_s
|
|
403
|
+
changed = global ? text.gsub(pattern, replacement) : text.lines.map { |line| line.sub(pattern, replacement) }.join
|
|
404
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, changed) }
|
|
405
|
+
@editor_state.status = "Substituted"
|
|
406
|
+
true
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def vibe_quit_editor
|
|
410
|
+
return close_editor unless @editor_state.dirty?
|
|
411
|
+
|
|
412
|
+
@editor_state.status = "No write since last change (:q! overrides)"
|
|
413
|
+
true
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def handle_vibe_normal_key(key)
|
|
417
|
+
if key == "\e" || key == "\x03"
|
|
418
|
+
@editor_state.vibe_pending = ""
|
|
419
|
+
vibe_return_to_normal
|
|
420
|
+
return true
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
key_name = key_name_for(key)
|
|
424
|
+
named_result = handle_vibe_named_key(key_name) if key_name
|
|
425
|
+
return named_result unless named_result == false || named_result.nil?
|
|
426
|
+
return false unless key.is_a?(String)
|
|
427
|
+
return true unless printable_key?(key) || vibe_normal_control_key?(key)
|
|
428
|
+
|
|
429
|
+
pending = @editor_state.vibe_pending.to_s + key
|
|
430
|
+
return vibe_store_pending_command(pending) if vibe_waiting_for_more?(pending)
|
|
431
|
+
|
|
432
|
+
@editor_state.vibe_pending = ""
|
|
433
|
+
execute_vibe_normal_command(pending)
|
|
434
|
+
true
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def vibe_store_pending_command(command)
|
|
438
|
+
@editor_state.vibe_pending = command
|
|
439
|
+
@editor_state.status = "#{@editor_state.vibe_mode.upcase.tr("_", " ")} #{command}"
|
|
440
|
+
true
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def handle_vibe_named_key(key_name)
|
|
444
|
+
case key_name
|
|
445
|
+
when :escape
|
|
446
|
+
@editor_state.vibe_pending = ""
|
|
447
|
+
vibe_return_to_normal
|
|
448
|
+
when :left
|
|
449
|
+
@editor_state.move_left
|
|
450
|
+
when :right
|
|
451
|
+
@editor_state.move_right
|
|
452
|
+
when :up
|
|
453
|
+
editor_move_up
|
|
454
|
+
when :down
|
|
455
|
+
editor_move_down
|
|
456
|
+
when :backspace
|
|
457
|
+
@editor_state.move_left
|
|
458
|
+
when :return, :enter
|
|
459
|
+
vibe_move_to_relative_line_first_non_blank(1)
|
|
460
|
+
when :ctrl_b
|
|
461
|
+
@editor_state.page_up(editor_page_rows)
|
|
462
|
+
when :ctrl_f
|
|
463
|
+
@editor_state.page_down(editor_page_rows)
|
|
464
|
+
when :ctrl_d
|
|
465
|
+
@editor_state.page_down(vibe_half_page_rows)
|
|
466
|
+
when :ctrl_u
|
|
467
|
+
@editor_state.page_up(vibe_half_page_rows)
|
|
468
|
+
when :ctrl_e
|
|
469
|
+
vibe_scroll_down
|
|
470
|
+
when :ctrl_y
|
|
471
|
+
vibe_scroll_up
|
|
472
|
+
when :ctrl_r
|
|
473
|
+
@editor_state.redo
|
|
474
|
+
else
|
|
475
|
+
false
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def vibe_normal_control_key?(key)
|
|
480
|
+
["\n", "\r", "\b", "\x7F", "\x02", "\x04", "\x05", "\x06", "\x12", "\x15", "\x19"].include?(key)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def vibe_visual_mode?
|
|
484
|
+
%w[visual visual_line visual_block].include?(@editor_state.vibe_mode)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def vibe_return_to_normal
|
|
488
|
+
vibe_apply_visual_block_insert if @editor_state.vibe_visual_block_insert
|
|
489
|
+
@editor_state.vibe_mode = "normal"
|
|
490
|
+
@editor_state.status = "NORMAL · i insert · :w save · :q quit"
|
|
491
|
+
true
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def vibe_cancel_visual_mode
|
|
495
|
+
@editor_state.vibe_pending = ""
|
|
496
|
+
vibe_remember_visual_selection
|
|
497
|
+
@editor_state.clear_selection
|
|
498
|
+
vibe_return_to_normal
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def vibe_remember_visual_selection
|
|
502
|
+
return unless @editor_state.selection_active?
|
|
503
|
+
|
|
504
|
+
@editor_state.vibe_last_visual_selection = {
|
|
505
|
+
mode: @editor_state.vibe_mode,
|
|
506
|
+
anchor: @editor_state.selection_anchor,
|
|
507
|
+
cursor: @editor_state.cursor
|
|
508
|
+
}
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def vibe_waiting_for_more?(command)
|
|
512
|
+
return true if command.match?(/\A\d+\z/) && command != "0"
|
|
513
|
+
return true if command.match?(/\A\d*g\z/)
|
|
514
|
+
return true if command.match?(/\A\d*z\z/)
|
|
515
|
+
return true if command.match?(/\A\d*[cdy]\d*\z/)
|
|
516
|
+
return true if command.match?(/\A\d*[cdy]\d*[ai]\z/)
|
|
517
|
+
return true if command.match?(/\A\d*[cdy]\d*[fFtT]\z/)
|
|
518
|
+
return true if command.match?(/\A\d*[fFtT]\z/)
|
|
519
|
+
return true if command.match?(/\A\d*r\z/)
|
|
520
|
+
return true if command.match?(/\Am\z/)
|
|
521
|
+
return true if command.match?(/\A"[a-z]?\z/)
|
|
522
|
+
return true if command.match?(/\A"[a-z][cdy]\z/)
|
|
523
|
+
return true if command.match?(/\A"[a-z][cdy][ai]\z/)
|
|
524
|
+
return true if command.match?(/\Aq\z/)
|
|
525
|
+
return true if command.match?(/\A@\z/)
|
|
526
|
+
return true if command.match?(/\A[\[\]]\z/)
|
|
527
|
+
return true if command.match?(/\A['`]\z/)
|
|
528
|
+
|
|
529
|
+
false
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def execute_vibe_normal_command(command)
|
|
533
|
+
original_command = command
|
|
534
|
+
register = nil
|
|
535
|
+
if (register_match = command.match(/\A"([a-z])(.*)\z/))
|
|
536
|
+
register = register_match[1]
|
|
537
|
+
command = register_match[2]
|
|
538
|
+
end
|
|
539
|
+
@vibe_active_register = register
|
|
540
|
+
count, body = vibe_count_and_body(command)
|
|
541
|
+
count = 1 if count.zero?
|
|
542
|
+
case body
|
|
543
|
+
when *VIBE_SIMPLE_MOTION_KEYS
|
|
544
|
+
vibe_apply_cursor_motion(body, count)
|
|
545
|
+
when "gg"
|
|
546
|
+
@editor_state.move_file_start
|
|
547
|
+
when "gv"
|
|
548
|
+
vibe_restore_visual_selection
|
|
549
|
+
when "]m"
|
|
550
|
+
vibe_jump_ruby_method(:forward)
|
|
551
|
+
when "[m"
|
|
552
|
+
vibe_jump_ruby_method(:backward)
|
|
553
|
+
when /\Aq(.+)\z/
|
|
554
|
+
vibe_start_macro_recording(Regexp.last_match(1))
|
|
555
|
+
when "@@"
|
|
556
|
+
vibe_play_macro(@editor_state.vibe_last_macro)
|
|
557
|
+
when /\A@(.+)\z/
|
|
558
|
+
vibe_play_macro(Regexp.last_match(1))
|
|
559
|
+
when /\Am(.+)\z/
|
|
560
|
+
vibe_set_mark(Regexp.last_match(1))
|
|
561
|
+
when /\A'(.+)\z/
|
|
562
|
+
vibe_jump_to_mark(Regexp.last_match(1), linewise: true)
|
|
563
|
+
when /\A`(.+)\z/
|
|
564
|
+
vibe_jump_to_mark(Regexp.last_match(1), linewise: false)
|
|
565
|
+
when "G"
|
|
566
|
+
line = command.match?(/\A\d+G\z/) ? count - 1 : @editor_state.lines.length - 1
|
|
567
|
+
@editor_state.set_cursor_line_and_column(line, 0)
|
|
568
|
+
when "zz"
|
|
569
|
+
vibe_position_cursor_line(:center)
|
|
570
|
+
when "zt"
|
|
571
|
+
vibe_position_cursor_line(:top)
|
|
572
|
+
when "zb"
|
|
573
|
+
vibe_position_cursor_line(:bottom)
|
|
574
|
+
when "H"
|
|
575
|
+
vibe_move_to_screen_line(count - 1)
|
|
576
|
+
when "M"
|
|
577
|
+
vibe_move_to_screen_line(editor_page_rows / 2)
|
|
578
|
+
when "L"
|
|
579
|
+
vibe_move_to_screen_line(editor_page_rows - count)
|
|
580
|
+
when "\x06"
|
|
581
|
+
@editor_state.page_down(editor_page_rows)
|
|
582
|
+
when "\x02"
|
|
583
|
+
@editor_state.page_up(editor_page_rows)
|
|
584
|
+
when "\x04"
|
|
585
|
+
@editor_state.page_down(vibe_half_page_rows)
|
|
586
|
+
when "\x15"
|
|
587
|
+
@editor_state.page_up(vibe_half_page_rows)
|
|
588
|
+
when "\x05"
|
|
589
|
+
vibe_scroll_down
|
|
590
|
+
when "\x19"
|
|
591
|
+
vibe_scroll_up
|
|
592
|
+
when "\x12"
|
|
593
|
+
@editor_state.redo
|
|
594
|
+
when "i"
|
|
595
|
+
vibe_enter_insert_mode(command)
|
|
596
|
+
when "I"
|
|
597
|
+
@editor_state.move_line_first_non_blank
|
|
598
|
+
vibe_enter_insert_mode(command)
|
|
599
|
+
when "a"
|
|
600
|
+
@editor_state.move_right
|
|
601
|
+
vibe_enter_insert_mode(command)
|
|
602
|
+
when "A"
|
|
603
|
+
@editor_state.move_line_end
|
|
604
|
+
vibe_enter_insert_mode(command)
|
|
605
|
+
when "C"
|
|
606
|
+
vibe_change_to_line_end(command)
|
|
607
|
+
when "D"
|
|
608
|
+
vibe_delete_to_line_end(command)
|
|
609
|
+
when "R"
|
|
610
|
+
@editor_state.vibe_mode = "replace"
|
|
611
|
+
@editor_state.status = "REPLACE · Esc normal"
|
|
612
|
+
vibe_begin_change_recording(command)
|
|
613
|
+
when "s"
|
|
614
|
+
vibe_substitute_characters(count, command)
|
|
615
|
+
when "S"
|
|
616
|
+
vibe_change_lines(count, command)
|
|
617
|
+
when "J"
|
|
618
|
+
vibe_join_lines(count, command)
|
|
619
|
+
when "n"
|
|
620
|
+
editor_search_repeat
|
|
621
|
+
when "N"
|
|
622
|
+
editor_search_repeat(vibe_opposite_search_direction)
|
|
623
|
+
when "*"
|
|
624
|
+
editor_search_word_under_cursor(:forward)
|
|
625
|
+
when "#"
|
|
626
|
+
editor_search_word_under_cursor(:backward)
|
|
627
|
+
when "U"
|
|
628
|
+
vibe_restore_current_line
|
|
629
|
+
when "%"
|
|
630
|
+
vibe_jump_to_matching_pair
|
|
631
|
+
when /^([fFtT])(.?)$/
|
|
632
|
+
vibe_find_character(Regexp.last_match(1), Regexp.last_match(2), count)
|
|
633
|
+
when ";"
|
|
634
|
+
vibe_repeat_find_character
|
|
635
|
+
when ","
|
|
636
|
+
vibe_repeat_find_character(reverse: true)
|
|
637
|
+
when /^r(.?)$/
|
|
638
|
+
vibe_replace_single_character(Regexp.last_match(1), count, command)
|
|
639
|
+
when "v"
|
|
640
|
+
vibe_begin_visual_mode("visual")
|
|
641
|
+
when "V"
|
|
642
|
+
vibe_begin_visual_mode("visual_line")
|
|
643
|
+
when "o"
|
|
644
|
+
vibe_open_line_below
|
|
645
|
+
when "O"
|
|
646
|
+
vibe_open_line_above
|
|
647
|
+
when "x"
|
|
648
|
+
vibe_record_undo { count.times { @editor_state.delete_at_cursor } }
|
|
649
|
+
vibe_remember_change(command)
|
|
650
|
+
when "X"
|
|
651
|
+
vibe_record_undo { count.times { @editor_state.delete_before_cursor } }
|
|
652
|
+
vibe_remember_change(command)
|
|
653
|
+
when "dd"
|
|
654
|
+
vibe_delete_lines(count)
|
|
655
|
+
vibe_store_active_register
|
|
656
|
+
vibe_remember_change(command)
|
|
657
|
+
when "cc"
|
|
658
|
+
vibe_change_lines(count, command)
|
|
659
|
+
vibe_store_active_register
|
|
660
|
+
when "yy"
|
|
661
|
+
vibe_yank_lines(count)
|
|
662
|
+
vibe_store_active_register
|
|
663
|
+
when "p"
|
|
664
|
+
vibe_record_undo { @editor_state.insert(vibe_active_register_text) }
|
|
665
|
+
vibe_remember_change(original_command)
|
|
666
|
+
when "P"
|
|
667
|
+
vibe_paste_before(original_command)
|
|
668
|
+
when "u"
|
|
669
|
+
@editor_state.undo
|
|
670
|
+
when ":"
|
|
671
|
+
@editor_state.vibe_mode = "command"
|
|
672
|
+
@editor_state.vibe_command = ""
|
|
673
|
+
@editor_state.status = ":"
|
|
674
|
+
when "/"
|
|
675
|
+
editor_search_begin
|
|
676
|
+
when "?"
|
|
677
|
+
editor_search_begin(:backward)
|
|
678
|
+
else
|
|
679
|
+
if body.start_with?("d") || body.start_with?("y") || body.start_with?("c")
|
|
680
|
+
vibe_operator_motion(body[0], body[1..], count, command)
|
|
681
|
+
elsif body.start_with?("z") && body.length > 1
|
|
682
|
+
execute_vibe_normal_command(body[1..])
|
|
683
|
+
else
|
|
684
|
+
@editor_state.status = "Unknown command: #{command}"
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
ensure
|
|
688
|
+
@vibe_active_register = nil
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def handle_vibe_visual_key(key)
|
|
692
|
+
key_name = key_name_for(key)
|
|
693
|
+
return handle_vibe_visual_named_key(key_name) if key_name
|
|
694
|
+
if key == "\e" || key == "\x03"
|
|
695
|
+
@editor_state.vibe_pending = ""
|
|
696
|
+
vibe_cancel_visual_mode
|
|
697
|
+
return true
|
|
698
|
+
end
|
|
699
|
+
return true unless printable_key?(key)
|
|
700
|
+
|
|
701
|
+
command = @editor_state.vibe_pending.to_s + key
|
|
702
|
+
return vibe_store_pending_command(command) if vibe_visual_waiting_for_more?(command)
|
|
703
|
+
|
|
704
|
+
@editor_state.vibe_pending = ""
|
|
705
|
+
execute_vibe_visual_command(command)
|
|
706
|
+
true
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def execute_vibe_visual_command(command)
|
|
710
|
+
count, body = vibe_count_and_body(command)
|
|
711
|
+
count = 1 if count.zero?
|
|
712
|
+
|
|
713
|
+
case body
|
|
714
|
+
when *EditorAutoClosePairs::AUTO_CLOSE_OPENERS
|
|
715
|
+
vibe_record_undo { editor_insert_printable(body) }
|
|
716
|
+
vibe_return_to_normal
|
|
717
|
+
when "y"
|
|
718
|
+
vibe_yank_visual_selection
|
|
719
|
+
when "d", "x"
|
|
720
|
+
vibe_delete_visual_selection
|
|
721
|
+
when "c"
|
|
722
|
+
vibe_change_visual_selection
|
|
723
|
+
when "p"
|
|
724
|
+
vibe_paste_visual_selection
|
|
725
|
+
when "I"
|
|
726
|
+
vibe_begin_visual_block_insert(:before)
|
|
727
|
+
when "A"
|
|
728
|
+
vibe_begin_visual_block_insert(:after)
|
|
729
|
+
when ">"
|
|
730
|
+
vibe_indent_visual_selection(:right)
|
|
731
|
+
when "<"
|
|
732
|
+
vibe_indent_visual_selection(:left)
|
|
733
|
+
when "J"
|
|
734
|
+
vibe_join_visual_selection
|
|
735
|
+
when "~"
|
|
736
|
+
vibe_transform_visual_selection(:swapcase)
|
|
737
|
+
when "u"
|
|
738
|
+
vibe_transform_visual_selection(:downcase)
|
|
739
|
+
when "U"
|
|
740
|
+
vibe_transform_visual_selection(:upcase)
|
|
741
|
+
when "/"
|
|
742
|
+
editor_search_begin
|
|
743
|
+
when "?"
|
|
744
|
+
editor_search_begin(:backward)
|
|
745
|
+
when "n"
|
|
746
|
+
editor_search_repeat
|
|
747
|
+
when "N"
|
|
748
|
+
editor_search_repeat(vibe_opposite_search_direction)
|
|
749
|
+
when "o"
|
|
750
|
+
vibe_switch_visual_selection_end
|
|
751
|
+
when "G"
|
|
752
|
+
vibe_visual_goto_line(command.match?(/\A\d+G\z/) ? count : nil)
|
|
753
|
+
when "gg"
|
|
754
|
+
vibe_visual_goto_line(command.match?(/\A\d+gg\z/) ? count : 1)
|
|
755
|
+
when "%"
|
|
756
|
+
vibe_jump_to_matching_pair
|
|
757
|
+
when /^([fFtT])(.?)$/
|
|
758
|
+
vibe_find_character(Regexp.last_match(1), Regexp.last_match(2), count)
|
|
759
|
+
when /\A[ai].\z/
|
|
760
|
+
vibe_select_text_object(body)
|
|
761
|
+
when ";"
|
|
762
|
+
vibe_repeat_find_character
|
|
763
|
+
when ","
|
|
764
|
+
vibe_repeat_find_character(reverse: true)
|
|
765
|
+
else
|
|
766
|
+
vibe_move_visual_selection(body, count)
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def vibe_visual_waiting_for_more?(command)
|
|
771
|
+
return true if command.match?(/\A[1-9]\d*\z/)
|
|
772
|
+
return true if command.match?(/\A\d*g\z/)
|
|
773
|
+
return true if command.match?(/\A\d*[fFtT]\z/)
|
|
774
|
+
return true if command.match?(/\A\d*[ai]\z/)
|
|
775
|
+
|
|
776
|
+
false
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def handle_vibe_visual_named_key(key_name)
|
|
780
|
+
case key_name
|
|
781
|
+
when :escape
|
|
782
|
+
vibe_cancel_visual_mode
|
|
783
|
+
when :left
|
|
784
|
+
@editor_state.move_left
|
|
785
|
+
when :right
|
|
786
|
+
@editor_state.move_right
|
|
787
|
+
when :up
|
|
788
|
+
editor_move_up
|
|
789
|
+
when :down
|
|
790
|
+
editor_move_down
|
|
791
|
+
else
|
|
792
|
+
false
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def vibe_switch_visual_selection_end
|
|
797
|
+
@editor_state.selection_anchor, @editor_state.cursor = @editor_state.cursor, @editor_state.selection_anchor
|
|
798
|
+
true
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def vibe_jump_ruby_method(direction)
|
|
802
|
+
unless vibe_ruby_file?
|
|
803
|
+
@editor_state.status = "Ruby navigation requires Ruby file"
|
|
804
|
+
return false
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
line, = @editor_state.cursor_line_and_column
|
|
808
|
+
candidates = @editor_state.lines.each_with_index.select { |source, _index| source.match?(/\A\s*def\b/) }.map(&:last)
|
|
809
|
+
target = if direction == :forward
|
|
810
|
+
candidates.find { |index| index > line }
|
|
811
|
+
else
|
|
812
|
+
candidates.reverse.find { |index| index < line }
|
|
813
|
+
end
|
|
814
|
+
unless target
|
|
815
|
+
@editor_state.status = "Ruby method not found"
|
|
816
|
+
return false
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
@editor_state.move_to_line_first_non_blank(target)
|
|
820
|
+
true
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def vibe_start_macro_recording(name)
|
|
824
|
+
@editor_state.vibe_recording_macro = name
|
|
825
|
+
@editor_state.vibe_macros[name] = []
|
|
826
|
+
@editor_state.status = "Recording macro #{name}"
|
|
827
|
+
true
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def vibe_stop_macro_recording
|
|
831
|
+
name = @editor_state.vibe_recording_macro
|
|
832
|
+
@editor_state.vibe_pending = ""
|
|
833
|
+
@editor_state.vibe_recording_macro = nil
|
|
834
|
+
@editor_state.vibe_last_macro = name
|
|
835
|
+
@editor_state.status = "Recorded macro #{name}"
|
|
836
|
+
true
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def vibe_record_macro_key(key)
|
|
840
|
+
name = @editor_state.vibe_recording_macro
|
|
841
|
+
return if !name || @vibe_replaying_macro
|
|
842
|
+
|
|
843
|
+
@editor_state.vibe_macros[name] << key
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def vibe_play_macro(name)
|
|
847
|
+
macro = @editor_state.vibe_macros[name]
|
|
848
|
+
unless macro
|
|
849
|
+
@editor_state.status = "Macro not set: #{name}"
|
|
850
|
+
return false
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
@editor_state.vibe_last_macro = name
|
|
854
|
+
@vibe_replaying_macro = true
|
|
855
|
+
macro.each { |key| handle_vibe_key(key) }
|
|
856
|
+
@editor_state.status = "Played macro #{name}"
|
|
857
|
+
true
|
|
858
|
+
ensure
|
|
859
|
+
@vibe_replaying_macro = false
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def vibe_set_mark(name)
|
|
863
|
+
@editor_state.vibe_marks[name] = { cursor: @editor_state.cursor }
|
|
864
|
+
@editor_state.status = "Set mark #{name}"
|
|
865
|
+
true
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def vibe_jump_to_mark(name, linewise:)
|
|
869
|
+
mark = @editor_state.vibe_marks[name]
|
|
870
|
+
unless mark
|
|
871
|
+
@editor_state.status = "Mark not set: #{name}"
|
|
872
|
+
return false
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
@editor_state.cursor = [[mark[:cursor], 0].max, @editor_state.buffer.length].min
|
|
876
|
+
@editor_state.move_line_first_non_blank if linewise
|
|
877
|
+
true
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def vibe_restore_visual_selection
|
|
881
|
+
selection = @editor_state.vibe_last_visual_selection
|
|
882
|
+
unless selection
|
|
883
|
+
@editor_state.status = "No visual selection to restore"
|
|
884
|
+
return false
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
@editor_state.vibe_mode = selection[:mode]
|
|
888
|
+
@editor_state.selection_anchor = [[selection[:anchor], 0].max, @editor_state.buffer.length].min
|
|
889
|
+
@editor_state.cursor = [[selection[:cursor], 0].max, @editor_state.buffer.length].min
|
|
890
|
+
@editor_state.status = case @editor_state.vibe_mode
|
|
891
|
+
when "visual_line" then "VISUAL LINE"
|
|
892
|
+
when "visual_block" then "VISUAL BLOCK"
|
|
893
|
+
else "VISUAL"
|
|
894
|
+
end
|
|
895
|
+
true
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def vibe_begin_visual_mode(mode)
|
|
899
|
+
@editor_state.clear_selection
|
|
900
|
+
@editor_state.selection_anchor = @editor_state.cursor
|
|
901
|
+
@editor_state.vibe_mode = mode
|
|
902
|
+
@editor_state.status = case mode
|
|
903
|
+
when "visual_line" then "VISUAL LINE"
|
|
904
|
+
when "visual_block" then "VISUAL BLOCK"
|
|
905
|
+
else "VISUAL"
|
|
906
|
+
end
|
|
907
|
+
true
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def vibe_select_text_object(text_object)
|
|
911
|
+
target = vibe_text_object_target(text_object)
|
|
912
|
+
return false unless target
|
|
913
|
+
|
|
914
|
+
@editor_state.selection_anchor = target.start_index
|
|
915
|
+
@editor_state.cursor = [target.end_index - 1, target.start_index].max
|
|
916
|
+
true
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def vibe_visual_goto_line(line_number = nil)
|
|
920
|
+
line = line_number ? line_number - 1 : @editor_state.lines.length - 1
|
|
921
|
+
@editor_state.set_cursor_line_and_column(line, 0)
|
|
922
|
+
true
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def vibe_move_visual_selection(motion, count = 1)
|
|
926
|
+
vibe_apply_cursor_motion(motion, count)
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def vibe_visual_range
|
|
930
|
+
@editor_state.selection_range
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
def vibe_yank_visual_selection
|
|
934
|
+
if @editor_state.vibe_mode == "visual_block"
|
|
935
|
+
@editor_state.kill_buffer = @editor_state.selected_text
|
|
936
|
+
@editor_state.status = "Yanked selection"
|
|
937
|
+
vibe_cancel_visual_mode
|
|
938
|
+
return true
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
range = vibe_visual_range
|
|
942
|
+
return false unless range
|
|
943
|
+
|
|
944
|
+
vibe_copy_range(range[0], range[1], "Yanked selection")
|
|
945
|
+
vibe_cancel_visual_mode
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def vibe_delete_visual_selection
|
|
949
|
+
if @editor_state.vibe_mode == "visual_block"
|
|
950
|
+
@editor_state.kill_buffer = @editor_state.selected_text
|
|
951
|
+
vibe_record_undo { @editor_state.selection_ranges.reverse_each { |range| @editor_state.replace_range(range[0], range[1], "") } }
|
|
952
|
+
vibe_cancel_visual_mode
|
|
953
|
+
return true
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
range = vibe_visual_range
|
|
957
|
+
return false unless range
|
|
958
|
+
|
|
959
|
+
@editor_state.copy_range(range[0], range[1])
|
|
960
|
+
vibe_record_undo { @editor_state.replace_range(range[0], range[1], "") }
|
|
961
|
+
vibe_cancel_visual_mode
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def vibe_change_visual_selection
|
|
965
|
+
range = vibe_visual_range
|
|
966
|
+
return false unless range
|
|
967
|
+
|
|
968
|
+
@editor_state.copy_range(range[0], range[1])
|
|
969
|
+
vibe_record_undo { @editor_state.replace_range(range[0], range[1], "") }
|
|
970
|
+
@editor_state.clear_selection
|
|
971
|
+
@editor_state.vibe_mode = "insert"
|
|
972
|
+
@editor_state.status = "INSERT · Esc normal"
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def vibe_paste_visual_selection
|
|
976
|
+
range = vibe_visual_range
|
|
977
|
+
return false unless range
|
|
978
|
+
|
|
979
|
+
text = @editor_state.kill_buffer.to_s
|
|
980
|
+
vibe_record_undo { @editor_state.replace_range(range[0], range[1], text) }
|
|
981
|
+
vibe_cancel_visual_mode
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def vibe_begin_visual_block_insert(position)
|
|
985
|
+
return vibe_move_visual_selection(position == :before ? "I" : "A") unless @editor_state.vibe_mode == "visual_block"
|
|
986
|
+
|
|
987
|
+
anchor_line, anchor_column = @editor_state.cursor_line_and_column_for(@editor_state.selection_anchor)
|
|
988
|
+
cursor_line, cursor_column = @editor_state.cursor_line_and_column
|
|
989
|
+
start_line, end_line = [anchor_line, cursor_line].minmax
|
|
990
|
+
start_column, end_column = [anchor_column, cursor_column].minmax
|
|
991
|
+
column = position == :before ? start_column : end_column + 1
|
|
992
|
+
@editor_state.vibe_visual_block_insert = { start_line: start_line, end_line: end_line, column: column }
|
|
993
|
+
@editor_state.clear_selection
|
|
994
|
+
@editor_state.set_cursor_line_and_column(start_line, column)
|
|
995
|
+
@editor_state.vibe_visual_block_insert[:start_index] = @editor_state.cursor
|
|
996
|
+
@editor_state.vibe_mode = "insert"
|
|
997
|
+
@editor_state.status = "INSERT · Esc normal"
|
|
998
|
+
true
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def vibe_apply_visual_block_insert
|
|
1002
|
+
block = @editor_state.vibe_visual_block_insert
|
|
1003
|
+
@editor_state.vibe_visual_block_insert = nil
|
|
1004
|
+
return unless block
|
|
1005
|
+
|
|
1006
|
+
inserted_text = @editor_state.buffer[block[:start_index]...@editor_state.cursor].to_s
|
|
1007
|
+
return if inserted_text.empty?
|
|
1008
|
+
|
|
1009
|
+
block[:end_line].downto(block[:start_line] + 1) do |line_index|
|
|
1010
|
+
line_start = @editor_state.line_start_offset(line_index)
|
|
1011
|
+
line_length = @editor_state.lines[line_index].to_s.length
|
|
1012
|
+
@editor_state.cursor = line_start + [block[:column], line_length].min
|
|
1013
|
+
@editor_state.insert(inserted_text)
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def vibe_transform_visual_selection(transform)
|
|
1018
|
+
range = vibe_visual_range
|
|
1019
|
+
return false unless range
|
|
1020
|
+
|
|
1021
|
+
text = @editor_state.buffer[range[0]...range[1]].to_s
|
|
1022
|
+
replacement = case transform
|
|
1023
|
+
when :swapcase then text.swapcase
|
|
1024
|
+
when :downcase then text.downcase
|
|
1025
|
+
else text.upcase
|
|
1026
|
+
end
|
|
1027
|
+
vibe_record_undo { @editor_state.replace_range(range[0], range[1], replacement) }
|
|
1028
|
+
vibe_cancel_visual_mode
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
def vibe_join_visual_selection
|
|
1032
|
+
range = vibe_visual_range
|
|
1033
|
+
return false unless range
|
|
1034
|
+
|
|
1035
|
+
start_line, = @editor_state.cursor_line_and_column_for(range[0])
|
|
1036
|
+
end_line, = @editor_state.cursor_line_and_column_for([range[1] - 1, range[0]].max)
|
|
1037
|
+
@editor_state.set_cursor_line_and_column(start_line, 0)
|
|
1038
|
+
vibe_join_lines(end_line - start_line + 1)
|
|
1039
|
+
vibe_cancel_visual_mode
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
def vibe_indent_visual_selection(direction)
|
|
1043
|
+
range = vibe_visual_range
|
|
1044
|
+
return false unless range
|
|
1045
|
+
|
|
1046
|
+
start_line, = @editor_state.cursor_line_and_column_for(range[0])
|
|
1047
|
+
end_line, = @editor_state.cursor_line_and_column_for([range[1] - 1, range[0]].max)
|
|
1048
|
+
start_index = @editor_state.line_range(start_line)[0]
|
|
1049
|
+
end_index = @editor_state.line_range(end_line)[1]
|
|
1050
|
+
original_text = @editor_state.buffer[start_index...end_index].to_s
|
|
1051
|
+
lines = @editor_state.lines[start_line..end_line].map do |line|
|
|
1052
|
+
direction == :right ? " #{line}" : line.sub(/\A(?: |\t| )/, "")
|
|
1053
|
+
end
|
|
1054
|
+
replacement = lines.join("\n")
|
|
1055
|
+
replacement += "\n" if original_text.end_with?("\n")
|
|
1056
|
+
|
|
1057
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, replacement) }
|
|
1058
|
+
vibe_cancel_visual_mode
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def vibe_count_and_body(command)
|
|
1062
|
+
return [0, "0"] if command == "0"
|
|
1063
|
+
|
|
1064
|
+
match = command.match(/\A(\d*)(.*)\z/)
|
|
1065
|
+
[match[1].to_i, match[2]]
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
def vibe_move_to_relative_line_first_non_blank(offset)
|
|
1069
|
+
line, = @editor_state.cursor_line_and_column
|
|
1070
|
+
@editor_state.move_to_line_first_non_blank(line + offset)
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def vibe_move_to_screen_line(offset)
|
|
1074
|
+
target_row = @editor_state.viewport_row + offset
|
|
1075
|
+
if current_editor_soft_wrap?
|
|
1076
|
+
visual_rows = editor_visual_rows(current_editor_text_width)
|
|
1077
|
+
line_index = visual_rows[target_row]&.fetch(:line_index) || @editor_state.lines.length - 1
|
|
1078
|
+
@editor_state.move_to_line_first_non_blank(line_index)
|
|
1079
|
+
else
|
|
1080
|
+
@editor_state.move_to_line_first_non_blank(target_row)
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
def vibe_position_cursor_line(position)
|
|
1085
|
+
row = if current_editor_soft_wrap?
|
|
1086
|
+
editor_visual_row_for(*@editor_state.cursor_line_and_column, current_editor_text_width)
|
|
1087
|
+
else
|
|
1088
|
+
@editor_state.cursor_line_and_column.first
|
|
1089
|
+
end
|
|
1090
|
+
offset = case position
|
|
1091
|
+
when :top then 0
|
|
1092
|
+
when :bottom then editor_page_rows - 1
|
|
1093
|
+
else editor_page_rows / 2
|
|
1094
|
+
end
|
|
1095
|
+
@editor_state.viewport_row = [[row - offset, 0].max, vibe_last_viewport_row].min
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def vibe_last_viewport_row
|
|
1099
|
+
visible_count = editor_page_rows
|
|
1100
|
+
if current_editor_soft_wrap?
|
|
1101
|
+
[editor_visual_rows(current_editor_text_width).length - visible_count, 0].max
|
|
1102
|
+
else
|
|
1103
|
+
[@editor_state.lines.length - visible_count, 0].max
|
|
1104
|
+
end
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
def vibe_half_page_rows
|
|
1108
|
+
[editor_page_rows / 2, 1].max
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
def vibe_scroll_down
|
|
1112
|
+
@editor_state.viewport_row = [@editor_state.viewport_row + 1, @editor_state.lines.length - 1].min
|
|
1113
|
+
line, column = @editor_state.cursor_line_and_column
|
|
1114
|
+
@editor_state.set_cursor_line_and_column(@editor_state.viewport_row, column) if line < @editor_state.viewport_row
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def vibe_scroll_up
|
|
1118
|
+
@editor_state.viewport_row = [@editor_state.viewport_row - 1, 0].max
|
|
1119
|
+
bottom_line = @editor_state.viewport_row + editor_page_rows - 1
|
|
1120
|
+
line, column = @editor_state.cursor_line_and_column
|
|
1121
|
+
@editor_state.set_cursor_line_and_column(bottom_line, column) if line > bottom_line
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
def vibe_open_line_below
|
|
1125
|
+
line, = @editor_state.cursor_line_and_column
|
|
1126
|
+
indentation = @editor_state.lines[line].to_s[/\A\s*/].to_s
|
|
1127
|
+
line_end = @editor_state.line_start_offset(line) + @editor_state.lines[line].to_s.length
|
|
1128
|
+
vibe_record_undo do
|
|
1129
|
+
@editor_state.cursor = line_end
|
|
1130
|
+
@editor_state.insert("\n#{indentation}")
|
|
1131
|
+
end
|
|
1132
|
+
@editor_state.vibe_mode = "insert"
|
|
1133
|
+
@editor_state.status = "INSERT · Esc normal"
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def vibe_open_line_above
|
|
1137
|
+
line, = @editor_state.cursor_line_and_column
|
|
1138
|
+
indentation = @editor_state.lines[line].to_s[/\A\s*/].to_s
|
|
1139
|
+
start_index = @editor_state.line_start_offset(line)
|
|
1140
|
+
vibe_record_undo do
|
|
1141
|
+
@editor_state.cursor = start_index
|
|
1142
|
+
@editor_state.insert("#{indentation}\n")
|
|
1143
|
+
@editor_state.cursor = start_index + indentation.length
|
|
1144
|
+
end
|
|
1145
|
+
@editor_state.vibe_mode = "insert"
|
|
1146
|
+
@editor_state.status = "INSERT · Esc normal"
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
def vibe_paste_before(command = nil)
|
|
1150
|
+
text = vibe_active_register_text
|
|
1151
|
+
return false if text.empty?
|
|
1152
|
+
|
|
1153
|
+
vibe_record_undo do
|
|
1154
|
+
@editor_state.cursor = @editor_state.current_line_range.first if text.end_with?("\n")
|
|
1155
|
+
@editor_state.insert(text)
|
|
1156
|
+
end
|
|
1157
|
+
vibe_remember_change(command)
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
def vibe_delete_lines(count)
|
|
1161
|
+
start_index, end_index = vibe_linewise_delete_range(count)
|
|
1162
|
+
@editor_state.copy_range(start_index, end_index)
|
|
1163
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, "") }
|
|
1164
|
+
@editor_state.status = "Deleted #{count} line#{count == 1 ? "" : "s"}"
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def vibe_linewise_delete_range(count)
|
|
1168
|
+
line, = @editor_state.cursor_line_and_column
|
|
1169
|
+
start_index, = @editor_state.line_range(line)
|
|
1170
|
+
end_line = [line + count - 1, @editor_state.lines.length - 1].min
|
|
1171
|
+
_, end_index = @editor_state.line_range(end_line)
|
|
1172
|
+
if end_index == @editor_state.buffer.length && start_index.positive?
|
|
1173
|
+
start_index -= 1
|
|
1174
|
+
end
|
|
1175
|
+
[start_index, end_index]
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def vibe_yank_lines(count)
|
|
1179
|
+
line, = @editor_state.cursor_line_and_column
|
|
1180
|
+
start_index, = @editor_state.line_range(line)
|
|
1181
|
+
end_line = [line + count - 1, @editor_state.lines.length - 1].min
|
|
1182
|
+
_, end_index = @editor_state.line_range(end_line)
|
|
1183
|
+
vibe_copy_range(start_index, end_index, "Yanked #{count} line#{count == 1 ? "" : "s"}")
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def vibe_change_lines(count, command = nil)
|
|
1187
|
+
start_index, end_index = vibe_linewise_change_range(count)
|
|
1188
|
+
@editor_state.copy_range(start_index, end_index)
|
|
1189
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, "") }
|
|
1190
|
+
@editor_state.cursor = start_index
|
|
1191
|
+
vibe_enter_insert_mode(command)
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1194
|
+
def vibe_linewise_change_range(count)
|
|
1195
|
+
line, = @editor_state.cursor_line_and_column
|
|
1196
|
+
start_index = @editor_state.line_start_offset(line)
|
|
1197
|
+
end_line = [line + count - 1, @editor_state.lines.length - 1].min
|
|
1198
|
+
end_index = @editor_state.line_start_offset(end_line) + @editor_state.lines[end_line].to_s.length
|
|
1199
|
+
[start_index, end_index]
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def vibe_change_to_line_end(command = nil)
|
|
1203
|
+
start_index = @editor_state.cursor
|
|
1204
|
+
line, = @editor_state.cursor_line_and_column
|
|
1205
|
+
end_index = @editor_state.line_start_offset(line) + @editor_state.lines[line].to_s.length
|
|
1206
|
+
return vibe_enter_insert_mode(command) if start_index == end_index
|
|
1207
|
+
|
|
1208
|
+
@editor_state.copy_range(start_index, end_index)
|
|
1209
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, "") }
|
|
1210
|
+
vibe_enter_insert_mode(command)
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def vibe_delete_to_line_end(command = nil)
|
|
1214
|
+
start_index = @editor_state.cursor
|
|
1215
|
+
line, = @editor_state.cursor_line_and_column
|
|
1216
|
+
end_index = @editor_state.line_start_offset(line) + @editor_state.lines[line].to_s.length
|
|
1217
|
+
return @editor_state.status = "Empty range" if start_index == end_index
|
|
1218
|
+
|
|
1219
|
+
@editor_state.copy_range(start_index, end_index)
|
|
1220
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, "") }
|
|
1221
|
+
@editor_state.status = "Deleted"
|
|
1222
|
+
vibe_remember_change(command)
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def vibe_substitute_characters(count, command = nil)
|
|
1226
|
+
start_index = @editor_state.cursor
|
|
1227
|
+
end_index = [start_index + count, @editor_state.buffer.length].min
|
|
1228
|
+
@editor_state.copy_range(start_index, end_index)
|
|
1229
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, "") }
|
|
1230
|
+
vibe_enter_insert_mode(command)
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
def vibe_replace_single_character(character, count, command = nil)
|
|
1234
|
+
return @editor_state.status = "Replacement character required" if character.to_s.empty?
|
|
1235
|
+
|
|
1236
|
+
vibe_record_undo do
|
|
1237
|
+
count.times do
|
|
1238
|
+
@editor_state.delete_at_cursor
|
|
1239
|
+
@editor_state.insert(character)
|
|
1240
|
+
end
|
|
1241
|
+
end
|
|
1242
|
+
@editor_state.move_left
|
|
1243
|
+
vibe_remember_change(command)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
def vibe_join_lines(count, command = nil)
|
|
1247
|
+
line, = @editor_state.cursor_line_and_column
|
|
1248
|
+
join_count = [count, 2].max
|
|
1249
|
+
end_line = [line + join_count - 1, @editor_state.lines.length - 1].min
|
|
1250
|
+
return @editor_state.status = "Already at last line" if end_line == line
|
|
1251
|
+
|
|
1252
|
+
vibe_record_undo do
|
|
1253
|
+
(end_line - line).times do
|
|
1254
|
+
line_end = @editor_state.line_start_offset(line) + @editor_state.lines[line].to_s.length
|
|
1255
|
+
next_line_start = line_end + 1
|
|
1256
|
+
next_line_end = next_line_start + @editor_state.lines[line + 1].to_s.length
|
|
1257
|
+
next_line = @editor_state.buffer[next_line_start...next_line_end].to_s.sub(/\A\s+/, "")
|
|
1258
|
+
separator = next_line.empty? ? "" : " "
|
|
1259
|
+
@editor_state.replace_range(line_end, next_line_end, separator + next_line)
|
|
1260
|
+
@editor_state.cursor = line_end
|
|
1261
|
+
end
|
|
1262
|
+
end
|
|
1263
|
+
vibe_remember_change(command)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
def vibe_enter_insert_mode(command = nil)
|
|
1267
|
+
@editor_state.vibe_mode = "insert"
|
|
1268
|
+
@editor_state.status = "INSERT · Esc normal"
|
|
1269
|
+
vibe_begin_change_recording(command) if command
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
def vibe_operator_motion(operator, motion, count, command = nil)
|
|
1273
|
+
motion_count, motion = vibe_count_and_body(motion)
|
|
1274
|
+
count *= motion_count if motion_count.positive?
|
|
1275
|
+
return vibe_operator_linewise(operator, count, command) if motion == operator
|
|
1276
|
+
|
|
1277
|
+
target = vibe_operator_target(motion, count)
|
|
1278
|
+
return false unless target
|
|
1279
|
+
return @editor_state.status = "Empty range" if target.start_index == target.end_index
|
|
1280
|
+
|
|
1281
|
+
vibe_apply_operator_to_target(operator, target, command, motion, count, motion_count)
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
def vibe_active_register_text
|
|
1285
|
+
return @editor_state.vibe_registers[@vibe_active_register].to_s if @vibe_active_register
|
|
1286
|
+
|
|
1287
|
+
@editor_state.kill_buffer.to_s
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
def vibe_store_active_register
|
|
1291
|
+
return unless @vibe_active_register
|
|
1292
|
+
|
|
1293
|
+
@editor_state.vibe_registers[@vibe_active_register] = @editor_state.kill_buffer.to_s
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
def vibe_apply_operator_to_target(operator, target, command, motion, count, motion_count)
|
|
1297
|
+
case operator
|
|
1298
|
+
when "d"
|
|
1299
|
+
@editor_state.copy_range(target.start_index, target.end_index)
|
|
1300
|
+
vibe_record_undo { @editor_state.replace_range(target.start_index, target.end_index, "") }
|
|
1301
|
+
@editor_state.status = "Deleted"
|
|
1302
|
+
vibe_store_active_register
|
|
1303
|
+
vibe_remember_change(command)
|
|
1304
|
+
when "c"
|
|
1305
|
+
@editor_state.copy_range(target.start_index, target.end_index)
|
|
1306
|
+
vibe_record_undo do
|
|
1307
|
+
@editor_state.replace_range(target.start_index, target.end_index, target.change_replacement_text)
|
|
1308
|
+
@editor_state.cursor = target.change_cursor_index
|
|
1309
|
+
end
|
|
1310
|
+
vibe_store_active_register
|
|
1311
|
+
vibe_enter_insert_mode(vibe_build_change_command(operator, motion, count, motion_count))
|
|
1312
|
+
else
|
|
1313
|
+
vibe_copy_range(target.start_index, target.end_index, "Yanked")
|
|
1314
|
+
vibe_store_active_register
|
|
1315
|
+
@editor_state.cursor = target.start_index
|
|
1316
|
+
end
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
def vibe_operator_target(motion, count)
|
|
1320
|
+
return vibe_text_object_target(motion) if motion.match?(/\A[ai].\z/)
|
|
1321
|
+
return vibe_word_motion_target(motion, count) if %w[w e b].include?(motion)
|
|
1322
|
+
return vibe_find_motion_target(motion, count) if motion.match?(/\A[fFtT].\z/)
|
|
1323
|
+
return vibe_percent_motion_target if motion == "%"
|
|
1324
|
+
|
|
1325
|
+
start_index = @editor_state.cursor
|
|
1326
|
+
return false unless vibe_apply_motion(motion, count)
|
|
1327
|
+
|
|
1328
|
+
end_index = @editor_state.cursor
|
|
1329
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: start_index, end_index: end_index)
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
def vibe_find_motion_target(motion, count)
|
|
1333
|
+
start_index = @editor_state.cursor
|
|
1334
|
+
command = motion[0]
|
|
1335
|
+
char = motion[1]
|
|
1336
|
+
reverse = %w[F T].include?(command)
|
|
1337
|
+
before = %w[t T].include?(command)
|
|
1338
|
+
end_index = vibe_find_character_index(char, count, reverse: reverse)
|
|
1339
|
+
unless end_index
|
|
1340
|
+
@editor_state.status = "Character not found: #{char}"
|
|
1341
|
+
return false
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
motion_index = end_index
|
|
1345
|
+
motion_index += reverse ? 1 : -1 if before
|
|
1346
|
+
@editor_state.cursor = motion_index
|
|
1347
|
+
target_end_index = if reverse
|
|
1348
|
+
before ? end_index + 1 : end_index
|
|
1349
|
+
else
|
|
1350
|
+
before ? end_index : end_index + 1
|
|
1351
|
+
end
|
|
1352
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: start_index, end_index: target_end_index)
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
def vibe_percent_motion_target
|
|
1356
|
+
start_index = @editor_state.cursor
|
|
1357
|
+
end_index = vibe_matching_pair_index(start_index)
|
|
1358
|
+
unless end_index
|
|
1359
|
+
@editor_state.status = "No matching pair under cursor"
|
|
1360
|
+
return false
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
if end_index > start_index
|
|
1364
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: start_index, end_index: end_index + 1)
|
|
1365
|
+
else
|
|
1366
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: end_index, end_index: start_index + 1)
|
|
1367
|
+
end
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
def vibe_word_motion_target(motion, count)
|
|
1371
|
+
start_index = @editor_state.cursor
|
|
1372
|
+
end_index = start_index
|
|
1373
|
+
if motion == "w"
|
|
1374
|
+
end_index = vibe_word_operator_forward_index(end_index, count)
|
|
1375
|
+
else
|
|
1376
|
+
count.times { end_index = vibe_word_motion_index(motion, end_index) }
|
|
1377
|
+
end_index = [end_index + 1, @editor_state.buffer.length].min if motion == "e"
|
|
1378
|
+
end
|
|
1379
|
+
@editor_state.cursor = end_index
|
|
1380
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: start_index, end_index: end_index)
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
def vibe_word_operator_forward_index(index, count)
|
|
1384
|
+
cursor = index
|
|
1385
|
+
buffer = @editor_state.buffer
|
|
1386
|
+
count.times do |step|
|
|
1387
|
+
current_kind = vibe_word_kind(buffer[cursor])
|
|
1388
|
+
cursor += 1 while cursor < buffer.length && vibe_word_kind(buffer[cursor]) == current_kind
|
|
1389
|
+
if step < count - 1
|
|
1390
|
+
cursor += 1 while cursor < buffer.length && vibe_word_kind(buffer[cursor]) == :space
|
|
1391
|
+
end
|
|
1392
|
+
end
|
|
1393
|
+
cursor
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
def vibe_word_motion_index(motion, index)
|
|
1397
|
+
original_cursor = @editor_state.cursor
|
|
1398
|
+
@editor_state.cursor = index
|
|
1399
|
+
case motion
|
|
1400
|
+
when "w"
|
|
1401
|
+
vibe_move_to_next_word_start
|
|
1402
|
+
when "e"
|
|
1403
|
+
vibe_move_to_word_end
|
|
1404
|
+
else
|
|
1405
|
+
vibe_move_to_previous_word_start
|
|
1406
|
+
end
|
|
1407
|
+
@editor_state.cursor
|
|
1408
|
+
ensure
|
|
1409
|
+
@editor_state.cursor = original_cursor
|
|
1410
|
+
end
|
|
1411
|
+
|
|
1412
|
+
def vibe_text_object_target(text_object)
|
|
1413
|
+
case text_object
|
|
1414
|
+
when "iw"
|
|
1415
|
+
vibe_inner_word_target
|
|
1416
|
+
when "aw"
|
|
1417
|
+
vibe_a_word_target
|
|
1418
|
+
when "ir", "ar"
|
|
1419
|
+
vibe_ruby_block_target(text_object)
|
|
1420
|
+
when "ip", "ap"
|
|
1421
|
+
vibe_paragraph_target(text_object)
|
|
1422
|
+
else
|
|
1423
|
+
return vibe_pair_text_object_target(text_object) if VIBE_PAIR_TEXT_OBJECTS.key?(text_object[1])
|
|
1424
|
+
|
|
1425
|
+
@editor_state.status = "Unsupported text object: #{text_object}"
|
|
1426
|
+
false
|
|
1427
|
+
end
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
def vibe_paragraph_target(text_object)
|
|
1431
|
+
line, = @editor_state.cursor_line_and_column
|
|
1432
|
+
lines = @editor_state.lines
|
|
1433
|
+
if lines[line].to_s.strip.empty?
|
|
1434
|
+
@editor_state.status = "Paragraph not found"
|
|
1435
|
+
return false
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
start_line = line
|
|
1439
|
+
start_line -= 1 while start_line.positive? && !lines[start_line - 1].to_s.strip.empty?
|
|
1440
|
+
end_line = line
|
|
1441
|
+
end_line += 1 while end_line < lines.length - 1 && !lines[end_line + 1].to_s.strip.empty?
|
|
1442
|
+
|
|
1443
|
+
if text_object == "ap"
|
|
1444
|
+
if end_line < lines.length - 1 && lines[end_line + 1].to_s.strip.empty?
|
|
1445
|
+
end_line += 1 while end_line < lines.length - 1 && lines[end_line + 1].to_s.strip.empty?
|
|
1446
|
+
else
|
|
1447
|
+
start_line -= 1 while start_line.positive? && lines[start_line - 1].to_s.strip.empty?
|
|
1448
|
+
end
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
VibeOperatorTarget.new(
|
|
1452
|
+
type: :characterwise,
|
|
1453
|
+
start_index: @editor_state.line_range(start_line)[0],
|
|
1454
|
+
end_index: @editor_state.line_range(end_line)[1]
|
|
1455
|
+
)
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
def vibe_ruby_block_target(text_object)
|
|
1459
|
+
unless vibe_ruby_file?
|
|
1460
|
+
@editor_state.status = "Ruby text object requires Ruby file"
|
|
1461
|
+
return false
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
block = vibe_enclosing_ruby_block
|
|
1465
|
+
unless block
|
|
1466
|
+
@editor_state.status = "Ruby block not found"
|
|
1467
|
+
return false
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
start_line = block[:start_line]
|
|
1471
|
+
end_line = block[:end_line]
|
|
1472
|
+
if text_object.start_with?("i")
|
|
1473
|
+
start_line += 1
|
|
1474
|
+
end_line -= 1
|
|
1475
|
+
end
|
|
1476
|
+
if start_line > end_line
|
|
1477
|
+
@editor_state.status = "Empty Ruby block"
|
|
1478
|
+
return false
|
|
1479
|
+
end
|
|
1480
|
+
|
|
1481
|
+
replacement_text = nil
|
|
1482
|
+
replacement_cursor_offset = nil
|
|
1483
|
+
if text_object == "ir"
|
|
1484
|
+
indentation = @editor_state.lines[start_line].to_s[/\A\s*/].to_s
|
|
1485
|
+
replacement_text = "#{indentation}\n"
|
|
1486
|
+
replacement_cursor_offset = indentation.length
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
VibeOperatorTarget.new(
|
|
1490
|
+
type: :characterwise,
|
|
1491
|
+
start_index: @editor_state.line_range(start_line)[0],
|
|
1492
|
+
end_index: @editor_state.line_range(end_line)[1],
|
|
1493
|
+
replacement_text: replacement_text,
|
|
1494
|
+
replacement_cursor_offset: replacement_cursor_offset
|
|
1495
|
+
)
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
def vibe_ruby_file?
|
|
1499
|
+
path = File.basename(@editor_state.path.to_s)
|
|
1500
|
+
VIBE_RUBY_PATHS.include?(path) || VIBE_RUBY_EXTENSIONS.include?(File.extname(path))
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
def vibe_enclosing_ruby_block
|
|
1504
|
+
blocks = []
|
|
1505
|
+
stack = []
|
|
1506
|
+
@editor_state.lines.each_with_index do |line, line_index|
|
|
1507
|
+
tokens = vibe_ruby_block_tokens(line)
|
|
1508
|
+
tokens.each do |token|
|
|
1509
|
+
if token == "end"
|
|
1510
|
+
opener = stack.pop
|
|
1511
|
+
blocks << opener.merge(end_line: line_index) if opener
|
|
1512
|
+
elsif VIBE_RUBY_BLOCK_OPENERS.include?(token)
|
|
1513
|
+
stack << { opener: token, start_line: line_index }
|
|
1514
|
+
end
|
|
1515
|
+
end
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
cursor_line, = @editor_state.cursor_line_and_column
|
|
1519
|
+
blocks.select { |block| block[:start_line] <= cursor_line && cursor_line <= block[:end_line] }
|
|
1520
|
+
.max_by { |block| block[:start_line] }
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
def vibe_ruby_block_tokens(line)
|
|
1524
|
+
code = vibe_ruby_code_for_block_scan(line)
|
|
1525
|
+
tokens = []
|
|
1526
|
+
stripped = code.strip
|
|
1527
|
+
opener = stripped.match(/\A(if|unless|case|while|until|for|def|module|class|begin)\b/)
|
|
1528
|
+
tokens << opener[1] if opener
|
|
1529
|
+
tokens << "do" if stripped.match?(/\bdo\b/)
|
|
1530
|
+
tokens << "end" if stripped.match?(/\Aend\b/)
|
|
1531
|
+
tokens
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
def vibe_ruby_code_for_block_scan(line)
|
|
1535
|
+
code = +""
|
|
1536
|
+
quote = nil
|
|
1537
|
+
escaped = false
|
|
1538
|
+
line.each_char do |char|
|
|
1539
|
+
if quote
|
|
1540
|
+
escaped = !escaped && char == "\\"
|
|
1541
|
+
quote = nil if char == quote && !escaped
|
|
1542
|
+
next
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
break if char == "#"
|
|
1546
|
+
if ["'", '"'].include?(char)
|
|
1547
|
+
quote = char
|
|
1548
|
+
escaped = false
|
|
1549
|
+
next
|
|
1550
|
+
end
|
|
1551
|
+
code << char
|
|
1552
|
+
end
|
|
1553
|
+
code
|
|
1554
|
+
end
|
|
1555
|
+
|
|
1556
|
+
def vibe_pair_text_object_target(text_object)
|
|
1557
|
+
include_pair = text_object.start_with?("a")
|
|
1558
|
+
pair = VIBE_PAIR_TEXT_OBJECTS[text_object[1]]
|
|
1559
|
+
range = pair[0] == pair[1] ? vibe_quote_pair_range(pair[0]) : vibe_delimited_pair_range(pair[0], pair[1])
|
|
1560
|
+
return @editor_state.status = "No #{pair.join} pair around cursor" unless range
|
|
1561
|
+
|
|
1562
|
+
start_index, end_index = range
|
|
1563
|
+
start_index += 1 unless include_pair
|
|
1564
|
+
VibeOperatorTarget.new(
|
|
1565
|
+
type: :characterwise,
|
|
1566
|
+
start_index: start_index,
|
|
1567
|
+
end_index: include_pair ? end_index + 1 : end_index
|
|
1568
|
+
)
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def vibe_find_character(command, char, count)
|
|
1572
|
+
reverse = %w[F T].include?(command)
|
|
1573
|
+
before = %w[t T].include?(command)
|
|
1574
|
+
index = vibe_find_character_index(char, count, reverse: reverse)
|
|
1575
|
+
unless index
|
|
1576
|
+
@editor_state.status = "Character not found: #{char}"
|
|
1577
|
+
return false
|
|
1578
|
+
end
|
|
1579
|
+
|
|
1580
|
+
index += reverse ? 1 : -1 if before
|
|
1581
|
+
@editor_state.cursor = [[index, 0].max, @editor_state.buffer.length].min
|
|
1582
|
+
@editor_state.vibe_last_find = { command: command, char: char }
|
|
1583
|
+
true
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
def vibe_repeat_find_character(reverse: false)
|
|
1587
|
+
last_find = @editor_state.vibe_last_find
|
|
1588
|
+
return @editor_state.status = "No character find to repeat" unless last_find
|
|
1589
|
+
|
|
1590
|
+
command = last_find[:command]
|
|
1591
|
+
command = vibe_reverse_find_command(command) if reverse
|
|
1592
|
+
vibe_find_character(command, last_find[:char], 1)
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
def vibe_reverse_find_command(command)
|
|
1596
|
+
{ "f" => "F", "F" => "f", "t" => "T", "T" => "t" }.fetch(command)
|
|
1597
|
+
end
|
|
1598
|
+
|
|
1599
|
+
def vibe_find_character_index(char, count, reverse: false)
|
|
1600
|
+
line, = @editor_state.cursor_line_and_column
|
|
1601
|
+
line_range = @editor_state.line_range(line)
|
|
1602
|
+
line_start = line_range[0]
|
|
1603
|
+
line_end = line_range[1]
|
|
1604
|
+
line_end -= 1 if line_end > line_start && @editor_state.buffer[line_end - 1] == "\n"
|
|
1605
|
+
cursor = @editor_state.cursor
|
|
1606
|
+
count.times do
|
|
1607
|
+
cursor = if reverse
|
|
1608
|
+
@editor_state.buffer.rindex(char, cursor - 1)
|
|
1609
|
+
else
|
|
1610
|
+
@editor_state.buffer.index(char, cursor + 1)
|
|
1611
|
+
end
|
|
1612
|
+
return nil unless cursor && cursor >= line_start && cursor < line_end
|
|
1613
|
+
end
|
|
1614
|
+
cursor
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
def vibe_jump_to_matching_pair
|
|
1618
|
+
index = vibe_matching_pair_index(@editor_state.cursor)
|
|
1619
|
+
unless index
|
|
1620
|
+
@editor_state.status = "No matching pair under cursor"
|
|
1621
|
+
return false
|
|
1622
|
+
end
|
|
1623
|
+
|
|
1624
|
+
@editor_state.cursor = index
|
|
1625
|
+
true
|
|
1626
|
+
end
|
|
1627
|
+
|
|
1628
|
+
def vibe_matching_pair_index(index)
|
|
1629
|
+
pairs = VIBE_PAIR_TEXT_OBJECTS.values.uniq.reject { |open_char, close_char| open_char == close_char }
|
|
1630
|
+
pairs.each do |open_char, close_char|
|
|
1631
|
+
if @editor_state.buffer[index] == open_char
|
|
1632
|
+
return vibe_find_forward_pair(index, open_char, close_char)
|
|
1633
|
+
elsif @editor_state.buffer[index] == close_char
|
|
1634
|
+
return vibe_find_backward_pair(index, open_char, close_char)
|
|
1635
|
+
end
|
|
1636
|
+
end
|
|
1637
|
+
nil
|
|
1638
|
+
end
|
|
1639
|
+
|
|
1640
|
+
def vibe_find_forward_pair(open_index, open_char, close_char)
|
|
1641
|
+
depth = 0
|
|
1642
|
+
(open_index + 1...@editor_state.buffer.length).each do |index|
|
|
1643
|
+
char = @editor_state.buffer[index]
|
|
1644
|
+
depth += 1 if char == open_char
|
|
1645
|
+
if char == close_char
|
|
1646
|
+
return index if depth.zero?
|
|
1647
|
+
|
|
1648
|
+
depth -= 1
|
|
1649
|
+
end
|
|
1650
|
+
end
|
|
1651
|
+
open_index
|
|
1652
|
+
end
|
|
1653
|
+
|
|
1654
|
+
def vibe_find_backward_pair(close_index, open_char, close_char)
|
|
1655
|
+
depth = 0
|
|
1656
|
+
(close_index - 1).downto(0) do |index|
|
|
1657
|
+
char = @editor_state.buffer[index]
|
|
1658
|
+
depth += 1 if char == close_char
|
|
1659
|
+
if char == open_char
|
|
1660
|
+
return index if depth.zero?
|
|
1661
|
+
|
|
1662
|
+
depth -= 1
|
|
1663
|
+
end
|
|
1664
|
+
end
|
|
1665
|
+
close_index
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
def vibe_delimited_pair_range(open_char, close_char)
|
|
1669
|
+
buffer = @editor_state.buffer
|
|
1670
|
+
cursor = @editor_state.cursor
|
|
1671
|
+
depth = 0
|
|
1672
|
+
open_index = nil
|
|
1673
|
+
cursor.downto(0) do |index|
|
|
1674
|
+
char = buffer[index]
|
|
1675
|
+
depth += 1 if char == close_char
|
|
1676
|
+
if char == open_char
|
|
1677
|
+
if depth.zero?
|
|
1678
|
+
open_index = index
|
|
1679
|
+
break
|
|
1680
|
+
end
|
|
1681
|
+
depth -= 1
|
|
1682
|
+
end
|
|
1683
|
+
end
|
|
1684
|
+
return nil unless open_index
|
|
1685
|
+
|
|
1686
|
+
depth = 0
|
|
1687
|
+
close_index = nil
|
|
1688
|
+
(open_index + 1...buffer.length).each do |index|
|
|
1689
|
+
char = buffer[index]
|
|
1690
|
+
depth += 1 if char == open_char
|
|
1691
|
+
if char == close_char
|
|
1692
|
+
if depth.zero?
|
|
1693
|
+
close_index = index
|
|
1694
|
+
break
|
|
1695
|
+
end
|
|
1696
|
+
depth -= 1
|
|
1697
|
+
end
|
|
1698
|
+
end
|
|
1699
|
+
close_index ? [open_index, close_index] : nil
|
|
1700
|
+
end
|
|
1701
|
+
|
|
1702
|
+
def vibe_quote_pair_range(quote)
|
|
1703
|
+
quote_indexes = vibe_unescaped_quote_indexes(quote)
|
|
1704
|
+
cursor = @editor_state.cursor
|
|
1705
|
+
open_index = quote_indexes.select { |index| index <= cursor }.last
|
|
1706
|
+
return nil unless open_index
|
|
1707
|
+
|
|
1708
|
+
close_index = quote_indexes.find { |index| index > open_index }
|
|
1709
|
+
close_index ? [open_index, close_index] : nil
|
|
1710
|
+
end
|
|
1711
|
+
|
|
1712
|
+
def vibe_unescaped_quote_indexes(quote)
|
|
1713
|
+
indexes = []
|
|
1714
|
+
@editor_state.buffer.each_char.with_index do |char, index|
|
|
1715
|
+
indexes << index if char == quote && !vibe_escaped_character?(index)
|
|
1716
|
+
end
|
|
1717
|
+
indexes
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1720
|
+
def vibe_escaped_character?(index)
|
|
1721
|
+
backslashes = 0
|
|
1722
|
+
cursor = index - 1
|
|
1723
|
+
while cursor >= 0 && @editor_state.buffer[cursor] == "\\"
|
|
1724
|
+
backslashes += 1
|
|
1725
|
+
cursor -= 1
|
|
1726
|
+
end
|
|
1727
|
+
backslashes.odd?
|
|
1728
|
+
end
|
|
1729
|
+
|
|
1730
|
+
def vibe_inner_word_target
|
|
1731
|
+
range = vibe_word_range_at(@editor_state.cursor)
|
|
1732
|
+
return @editor_state.status = "No word under cursor" unless range
|
|
1733
|
+
|
|
1734
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: range[0], end_index: range[1])
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1737
|
+
def vibe_a_word_target
|
|
1738
|
+
range = vibe_word_range_at(@editor_state.cursor)
|
|
1739
|
+
return @editor_state.status = "No word under cursor" unless range
|
|
1740
|
+
|
|
1741
|
+
start_index, end_index = range
|
|
1742
|
+
if end_index < @editor_state.buffer.length && vibe_word_kind(@editor_state.buffer[end_index]) == :space
|
|
1743
|
+
end_index += 1 while end_index < @editor_state.buffer.length && vibe_word_kind(@editor_state.buffer[end_index]) == :space
|
|
1744
|
+
else
|
|
1745
|
+
start_index -= 1 while start_index.positive? && vibe_word_kind(@editor_state.buffer[start_index - 1]) == :space
|
|
1746
|
+
end
|
|
1747
|
+
VibeOperatorTarget.new(type: :characterwise, start_index: start_index, end_index: end_index)
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
def vibe_word_range_at(offset)
|
|
1751
|
+
buffer = @editor_state.buffer
|
|
1752
|
+
return nil if buffer.empty?
|
|
1753
|
+
|
|
1754
|
+
index = [[offset.to_i, 0].max, buffer.length - 1].min
|
|
1755
|
+
kind = vibe_word_kind(buffer[index])
|
|
1756
|
+
return nil if kind == :space
|
|
1757
|
+
|
|
1758
|
+
start_index = index
|
|
1759
|
+
start_index -= 1 while start_index.positive? && vibe_word_kind(buffer[start_index - 1]) == kind
|
|
1760
|
+
end_index = index + 1
|
|
1761
|
+
end_index += 1 while end_index < buffer.length && vibe_word_kind(buffer[end_index]) == kind
|
|
1762
|
+
[start_index, end_index]
|
|
1763
|
+
end
|
|
1764
|
+
|
|
1765
|
+
def vibe_operator_linewise(operator, count, command = nil)
|
|
1766
|
+
case operator
|
|
1767
|
+
when "d"
|
|
1768
|
+
vibe_delete_lines(count)
|
|
1769
|
+
vibe_remember_change(command)
|
|
1770
|
+
when "c"
|
|
1771
|
+
vibe_change_lines(count, command)
|
|
1772
|
+
else
|
|
1773
|
+
vibe_yank_lines(count)
|
|
1774
|
+
end
|
|
1775
|
+
end
|
|
1776
|
+
|
|
1777
|
+
def vibe_apply_cursor_motion(motion, count)
|
|
1778
|
+
case motion
|
|
1779
|
+
when "w"
|
|
1780
|
+
count.times { vibe_move_to_next_word_start }
|
|
1781
|
+
when "e"
|
|
1782
|
+
count.times { vibe_move_to_word_end }
|
|
1783
|
+
when "b"
|
|
1784
|
+
count.times { vibe_move_to_previous_word_start }
|
|
1785
|
+
else
|
|
1786
|
+
return vibe_apply_motion(motion, count)
|
|
1787
|
+
end
|
|
1788
|
+
true
|
|
1789
|
+
end
|
|
1790
|
+
|
|
1791
|
+
def vibe_apply_motion(motion, count)
|
|
1792
|
+
case motion
|
|
1793
|
+
when "w"
|
|
1794
|
+
count.times { @editor_state.move_to_next_word }
|
|
1795
|
+
when "e"
|
|
1796
|
+
count.times { @editor_state.move_to_word_end }
|
|
1797
|
+
when "b"
|
|
1798
|
+
count.times { @editor_state.move_to_previous_word }
|
|
1799
|
+
when "$"
|
|
1800
|
+
@editor_state.move_line_end
|
|
1801
|
+
when "0"
|
|
1802
|
+
@editor_state.move_line_start
|
|
1803
|
+
when "^"
|
|
1804
|
+
@editor_state.move_line_first_non_blank
|
|
1805
|
+
when "+", "\n", "\r"
|
|
1806
|
+
vibe_move_to_relative_line_first_non_blank(count)
|
|
1807
|
+
when "-"
|
|
1808
|
+
vibe_move_to_relative_line_first_non_blank(-count)
|
|
1809
|
+
when "_"
|
|
1810
|
+
vibe_move_to_relative_line_first_non_blank(count - 1)
|
|
1811
|
+
when "h", "\b", "\x7F"
|
|
1812
|
+
count.times { @editor_state.move_left }
|
|
1813
|
+
when "j"
|
|
1814
|
+
count.times { editor_move_down }
|
|
1815
|
+
when "k"
|
|
1816
|
+
count.times { editor_move_up }
|
|
1817
|
+
when "l", " "
|
|
1818
|
+
count.times { @editor_state.move_right }
|
|
1819
|
+
when "}"
|
|
1820
|
+
count.times { vibe_move_paragraph_forward }
|
|
1821
|
+
when "{"
|
|
1822
|
+
count.times { vibe_move_paragraph_backward }
|
|
1823
|
+
else
|
|
1824
|
+
@editor_state.status = "Unsupported motion: #{motion}"
|
|
1825
|
+
return false
|
|
1826
|
+
end
|
|
1827
|
+
true
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
def vibe_move_paragraph_forward
|
|
1831
|
+
line, = @editor_state.cursor_line_and_column
|
|
1832
|
+
lines = @editor_state.lines
|
|
1833
|
+
line += 1 while line < lines.length - 1 && !lines[line].to_s.strip.empty?
|
|
1834
|
+
line += 1 while line < lines.length - 1 && lines[line].to_s.strip.empty?
|
|
1835
|
+
@editor_state.set_cursor_line_and_column(line, 0)
|
|
1836
|
+
end
|
|
1837
|
+
|
|
1838
|
+
def vibe_move_paragraph_backward
|
|
1839
|
+
line, = @editor_state.cursor_line_and_column
|
|
1840
|
+
lines = @editor_state.lines
|
|
1841
|
+
line -= 1 if line.positive?
|
|
1842
|
+
line -= 1 while line.positive? && lines[line].to_s.strip.empty?
|
|
1843
|
+
line -= 1 while line.positive? && !lines[line - 1].to_s.strip.empty?
|
|
1844
|
+
@editor_state.set_cursor_line_and_column(line, 0)
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
def vibe_move_to_next_word_start
|
|
1848
|
+
cursor = @editor_state.cursor
|
|
1849
|
+
buffer = @editor_state.buffer
|
|
1850
|
+
return if cursor >= buffer.length
|
|
1851
|
+
|
|
1852
|
+
current_kind = vibe_word_kind(buffer[cursor])
|
|
1853
|
+
cursor += 1 while cursor < buffer.length && vibe_word_kind(buffer[cursor]) == current_kind
|
|
1854
|
+
cursor += 1 while cursor < buffer.length && vibe_word_kind(buffer[cursor]) == :space
|
|
1855
|
+
@editor_state.cursor = cursor
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
def vibe_move_to_word_end
|
|
1859
|
+
cursor = @editor_state.cursor
|
|
1860
|
+
buffer = @editor_state.buffer
|
|
1861
|
+
return if buffer.empty? || cursor >= buffer.length
|
|
1862
|
+
|
|
1863
|
+
current_kind = vibe_word_kind(buffer[cursor])
|
|
1864
|
+
next_kind = cursor < buffer.length - 1 ? vibe_word_kind(buffer[cursor + 1]) : nil
|
|
1865
|
+
cursor += 1 if current_kind != :space && next_kind && next_kind != current_kind
|
|
1866
|
+
cursor += 1 while cursor < buffer.length && vibe_word_kind(buffer[cursor]) == :space
|
|
1867
|
+
return @editor_state.cursor = cursor if cursor >= buffer.length
|
|
1868
|
+
|
|
1869
|
+
current_kind = vibe_word_kind(buffer[cursor])
|
|
1870
|
+
cursor += 1 while cursor < buffer.length - 1 && vibe_word_kind(buffer[cursor + 1]) == current_kind
|
|
1871
|
+
@editor_state.cursor = cursor
|
|
1872
|
+
end
|
|
1873
|
+
|
|
1874
|
+
def vibe_move_to_previous_word_start
|
|
1875
|
+
cursor = @editor_state.cursor
|
|
1876
|
+
buffer = @editor_state.buffer
|
|
1877
|
+
return if cursor.zero? || buffer.empty?
|
|
1878
|
+
|
|
1879
|
+
cursor -= 1
|
|
1880
|
+
cursor -= 1 while cursor.positive? && vibe_word_kind(buffer[cursor]) == :space
|
|
1881
|
+
current_kind = vibe_word_kind(buffer[cursor])
|
|
1882
|
+
cursor -= 1 while cursor.positive? && vibe_word_kind(buffer[cursor - 1]) == current_kind
|
|
1883
|
+
@editor_state.cursor = cursor
|
|
1884
|
+
end
|
|
1885
|
+
|
|
1886
|
+
def vibe_word_kind(char)
|
|
1887
|
+
case char.to_s
|
|
1888
|
+
when /\s/
|
|
1889
|
+
:space
|
|
1890
|
+
when /[[:alnum:]_]/
|
|
1891
|
+
:keyword
|
|
1892
|
+
else
|
|
1893
|
+
:punctuation
|
|
1894
|
+
end
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
def vibe_copy_range(start_index, end_index, status)
|
|
1898
|
+
@editor_state.copy_range(start_index, end_index)
|
|
1899
|
+
@output_io.print("\e]52;c;#{Base64.strict_encode64(@editor_state.kill_buffer)}\a")
|
|
1900
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
1901
|
+
@editor_state.status = status
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1904
|
+
def vibe_opposite_search_direction
|
|
1905
|
+
@editor_state.search_direction == :backward ? :forward : :backward
|
|
1906
|
+
end
|
|
1907
|
+
|
|
1908
|
+
def vibe_restore_current_line
|
|
1909
|
+
line, = @editor_state.cursor_line_and_column
|
|
1910
|
+
start_index = @editor_state.line_start_offset(line)
|
|
1911
|
+
end_index = start_index + @editor_state.lines[line].to_s.length
|
|
1912
|
+
original_line = @editor_state.original_content.split("\n", -1)[line].to_s
|
|
1913
|
+
vibe_record_undo { @editor_state.replace_range(start_index, end_index, original_line) }
|
|
1914
|
+
@editor_state.status = "Restored line"
|
|
1915
|
+
end
|
|
1916
|
+
|
|
1917
|
+
def handle_vibe_repeat_change
|
|
1918
|
+
change = @editor_state.vibe_last_change
|
|
1919
|
+
return @editor_state.status = "No change to repeat" unless change
|
|
1920
|
+
|
|
1921
|
+
change.dup.each { |key| handle_vibe_key(key) }
|
|
1922
|
+
true
|
|
1923
|
+
end
|
|
1924
|
+
|
|
1925
|
+
def vibe_begin_change_recording(command)
|
|
1926
|
+
@editor_state.vibe_last_change = vibe_change_keys(command)
|
|
1927
|
+
end
|
|
1928
|
+
|
|
1929
|
+
def vibe_record_insert_change_key(key)
|
|
1930
|
+
return unless @editor_state.vibe_last_change
|
|
1931
|
+
return if ["\x03"].include?(key)
|
|
1932
|
+
|
|
1933
|
+
@editor_state.vibe_last_change << key
|
|
1934
|
+
end
|
|
1935
|
+
|
|
1936
|
+
def vibe_remember_change(command)
|
|
1937
|
+
@editor_state.vibe_last_change = vibe_change_keys(command) if command
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
def vibe_build_change_command(operator, motion, count, motion_count)
|
|
1941
|
+
command = +""
|
|
1942
|
+
command << count.to_s if count > 1 && motion_count.zero?
|
|
1943
|
+
command << operator
|
|
1944
|
+
command << motion_count.to_s if motion_count.positive?
|
|
1945
|
+
command << motion
|
|
1946
|
+
vibe_change_keys(command)
|
|
1947
|
+
end
|
|
1948
|
+
|
|
1949
|
+
def vibe_change_keys(command)
|
|
1950
|
+
Array(command).flat_map { |key| key.is_a?(String) ? key.each_char.to_a : key }
|
|
1951
|
+
end
|
|
1952
|
+
|
|
1953
|
+
def vibe_record_undo
|
|
1954
|
+
before = @editor_state.buffer.dup
|
|
1955
|
+
@editor_state.push_undo
|
|
1956
|
+
yield
|
|
1957
|
+
@editor_state.undo_stack.pop if @editor_state.buffer == before
|
|
1958
|
+
end
|
|
1959
|
+
|
|
1960
|
+
end
|
|
1961
|
+
end
|
|
1962
|
+
end
|