ruvim 0.4.0 → 0.6.1
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/AGENTS.md +53 -4
- data/README.md +15 -6
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +3 -2
- data/docs/command.md +81 -9
- data/docs/done.md +23 -0
- data/docs/spec.md +105 -19
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +9 -1
- data/docs/vim_diff.md +13 -0
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +217 -2778
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +39 -28
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +64 -81
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +16 -6
- data/lib/ruvim/git/branch.rb +20 -5
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +42 -1
- data/lib/ruvim/global_commands.rb +175 -35
- data/lib/ruvim/highlighter.rb +4 -13
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +2 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +6 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -0
- data/lib/ruvim/rich_view/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +14 -7
- data/lib/ruvim/screen.rb +126 -72
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +23 -6
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +43 -19
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +15 -10
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +63 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/config_dsl_test.rb +47 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/ex_command_registry_test.rb +106 -0
- data/test/follow_test.rb +20 -21
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +96 -17
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +125 -0
- data/test/indent_test.rb +137 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +42 -42
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/stream_test.rb +165 -0
- data/test/window_test.rb +59 -0
- metadata +66 -2
|
@@ -0,0 +1,1510 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class KeyHandler
|
|
5
|
+
# Rich mode: delegates to normal mode key handling but blocks mutating operations.
|
|
6
|
+
RICH_MODE_BLOCKED_COMMANDS = %w[
|
|
7
|
+
mode.insert mode.append mode.append_line_end mode.insert_nonblank
|
|
8
|
+
mode.open_below mode.open_above
|
|
9
|
+
buffer.delete_char buffer.delete_line buffer.delete_motion
|
|
10
|
+
buffer.change_motion buffer.change_line
|
|
11
|
+
buffer.paste_after buffer.paste_before
|
|
12
|
+
buffer.replace_char
|
|
13
|
+
buffer.visual_delete
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_accessor :paste_batch
|
|
17
|
+
|
|
18
|
+
def initialize(editor:, dispatcher:, keymaps:, terminal:, screen:, completion:, stream_mixer:)
|
|
19
|
+
@editor = editor
|
|
20
|
+
@dispatcher = dispatcher
|
|
21
|
+
@keymaps = keymaps
|
|
22
|
+
@terminal = terminal
|
|
23
|
+
@screen = screen
|
|
24
|
+
@completion = completion
|
|
25
|
+
@stream_mixer = stream_mixer
|
|
26
|
+
|
|
27
|
+
@pending_key_deadline = nil
|
|
28
|
+
@pending_ambiguous_invocation = nil
|
|
29
|
+
@pending_keys = nil
|
|
30
|
+
@insert_start_location = nil
|
|
31
|
+
@paste_batch = false
|
|
32
|
+
|
|
33
|
+
# Pending state flags
|
|
34
|
+
@operator_pending = nil
|
|
35
|
+
@register_pending = false
|
|
36
|
+
@mark_pending = false
|
|
37
|
+
@jump_pending = nil
|
|
38
|
+
@find_pending = nil
|
|
39
|
+
@replace_pending = nil
|
|
40
|
+
@macro_record_pending = false
|
|
41
|
+
@macro_play_pending = false
|
|
42
|
+
@visual_pending = nil
|
|
43
|
+
@skip_record_for_current_key = false
|
|
44
|
+
@last_macro_name = nil
|
|
45
|
+
@macro_play_stack = nil
|
|
46
|
+
@suspend_macro_recording_depth = nil
|
|
47
|
+
|
|
48
|
+
# Dot repeat state
|
|
49
|
+
@dot_change_capture_active = false
|
|
50
|
+
@dot_change_capture_keys = nil
|
|
51
|
+
@last_change_keys = nil
|
|
52
|
+
@dot_replay_depth = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns true if redraw is needed due to timeout/transient message handling
|
|
56
|
+
def handle_idle_timeout
|
|
57
|
+
redraw = false
|
|
58
|
+
if pending_key_timeout_expired?
|
|
59
|
+
handle_pending_key_timeout
|
|
60
|
+
redraw = true
|
|
61
|
+
end
|
|
62
|
+
redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
|
|
63
|
+
redraw
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pending_key_timeout_seconds
|
|
67
|
+
return nil unless @pending_key_deadline
|
|
68
|
+
|
|
69
|
+
[@pending_key_deadline - monotonic_now, 0.0].max
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def loop_timeout_seconds
|
|
73
|
+
now = monotonic_now
|
|
74
|
+
timeouts = []
|
|
75
|
+
if @pending_key_deadline
|
|
76
|
+
timeouts << [@pending_key_deadline - now, 0.0].max
|
|
77
|
+
end
|
|
78
|
+
if (msg_to = @editor.transient_message_timeout_seconds(now:))
|
|
79
|
+
timeouts << msg_to
|
|
80
|
+
end
|
|
81
|
+
timeouts.min
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def escape_sequence_timeout_seconds
|
|
85
|
+
ms = @editor.global_options["ttimeoutlen"].to_i
|
|
86
|
+
ms = 50 if ms <= 0
|
|
87
|
+
ms / 1000.0
|
|
88
|
+
rescue StandardError
|
|
89
|
+
0.005
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns true if redraw is needed
|
|
93
|
+
def handle(key)
|
|
94
|
+
mode_before = @editor.mode
|
|
95
|
+
clear_stale_message_before_key(key)
|
|
96
|
+
@skip_record_for_current_key = false
|
|
97
|
+
append_dot_change_capture_key(key)
|
|
98
|
+
if key == :ctrl_z
|
|
99
|
+
suspend_to_shell
|
|
100
|
+
track_mode_transition(mode_before)
|
|
101
|
+
return true
|
|
102
|
+
end
|
|
103
|
+
if key == :ctrl_c && @editor.mode != :normal
|
|
104
|
+
handle_ctrl_c
|
|
105
|
+
track_mode_transition(mode_before)
|
|
106
|
+
record_macro_key_if_needed(key)
|
|
107
|
+
return false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
case @editor.mode
|
|
111
|
+
when :hit_enter
|
|
112
|
+
handle_hit_enter_key(key)
|
|
113
|
+
when :insert
|
|
114
|
+
handle_insert_key(key)
|
|
115
|
+
when :command_line
|
|
116
|
+
handle_command_line_key(key)
|
|
117
|
+
when :visual_char, :visual_line, :visual_block
|
|
118
|
+
handle_visual_key(key)
|
|
119
|
+
when :rich
|
|
120
|
+
handle_rich_key(key)
|
|
121
|
+
else
|
|
122
|
+
handle_normal_key(key)
|
|
123
|
+
end
|
|
124
|
+
track_mode_transition(mode_before)
|
|
125
|
+
record_macro_key_if_needed(key)
|
|
126
|
+
false
|
|
127
|
+
rescue RuVim::CommandError => e
|
|
128
|
+
@editor.echo_error(e.message)
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def handle_editor_app_action(name, **kwargs)
|
|
133
|
+
if @editor.rich_mode?
|
|
134
|
+
case name
|
|
135
|
+
when :normal_operator_start
|
|
136
|
+
op = kwargs[:name] || kwargs["name"]
|
|
137
|
+
return if op == :delete || op == :change
|
|
138
|
+
when :normal_replace_pending_start, :normal_change_repeat
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
case name
|
|
144
|
+
when :normal_register_pending_start
|
|
145
|
+
start_register_pending
|
|
146
|
+
when :normal_operator_start
|
|
147
|
+
start_operator_pending(kwargs[:name] || kwargs["name"])
|
|
148
|
+
when :normal_replace_pending_start
|
|
149
|
+
start_replace_pending
|
|
150
|
+
when :normal_find_pending_start
|
|
151
|
+
start_find_pending((kwargs[:token] || kwargs["token"]).to_s)
|
|
152
|
+
when :normal_find_repeat
|
|
153
|
+
repeat_last_find(reverse: !!(kwargs[:reverse] || kwargs["reverse"]))
|
|
154
|
+
when :normal_change_repeat
|
|
155
|
+
repeat_last_change
|
|
156
|
+
when :normal_macro_record_toggle
|
|
157
|
+
toggle_macro_recording_or_start_pending
|
|
158
|
+
when :normal_macro_play_pending_start
|
|
159
|
+
start_macro_play_pending
|
|
160
|
+
when :normal_mark_pending_start
|
|
161
|
+
start_mark_pending
|
|
162
|
+
when :normal_jump_pending_start
|
|
163
|
+
start_jump_pending(
|
|
164
|
+
linewise: !!(kwargs[:linewise] || kwargs["linewise"]),
|
|
165
|
+
repeat_token: (kwargs[:repeat_token] || kwargs["repeat_token"]).to_s
|
|
166
|
+
)
|
|
167
|
+
when :follow_toggle
|
|
168
|
+
@stream_mixer.ex_follow_toggle
|
|
169
|
+
when :normal_ctrl_c
|
|
170
|
+
handle_normal_ctrl_c
|
|
171
|
+
else
|
|
172
|
+
raise RuVim::CommandError, "Unknown app action: #{name}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_normal_ctrl_c
|
|
177
|
+
clear_pending_key_timeout
|
|
178
|
+
@editor.pending_count = nil
|
|
179
|
+
@pending_keys = []
|
|
180
|
+
@operator_pending = nil
|
|
181
|
+
@replace_pending = nil
|
|
182
|
+
@register_pending = false
|
|
183
|
+
@mark_pending = false
|
|
184
|
+
@jump_pending = nil
|
|
185
|
+
@macro_record_pending = false
|
|
186
|
+
@macro_play_pending = false
|
|
187
|
+
buf = @editor.current_buffer
|
|
188
|
+
if buf && @stream_mixer.follow_active?(buf)
|
|
189
|
+
@stream_mixer.stop_follow!(buf)
|
|
190
|
+
else
|
|
191
|
+
@editor.clear_message
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def monotonic_now
|
|
198
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
199
|
+
rescue StandardError
|
|
200
|
+
Time.now.to_f
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def arm_pending_key_timeout
|
|
204
|
+
ms = @editor.global_options["timeoutlen"].to_i
|
|
205
|
+
ms = 1000 if ms <= 0
|
|
206
|
+
@pending_key_deadline = monotonic_now + (ms / 1000.0)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def clear_pending_key_timeout
|
|
210
|
+
@pending_key_deadline = nil
|
|
211
|
+
@pending_ambiguous_invocation = nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def pending_key_timeout_expired?
|
|
215
|
+
@pending_key_deadline && monotonic_now >= @pending_key_deadline
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def handle_pending_key_timeout
|
|
219
|
+
inv = @pending_ambiguous_invocation
|
|
220
|
+
clear_pending_key_timeout
|
|
221
|
+
if inv
|
|
222
|
+
@dispatcher.dispatch(@editor, dup_invocation(inv))
|
|
223
|
+
elsif @pending_keys && !@pending_keys.empty?
|
|
224
|
+
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
225
|
+
end
|
|
226
|
+
@editor.pending_count = nil
|
|
227
|
+
@pending_keys = []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def clear_stale_message_before_key(key)
|
|
231
|
+
return if @editor.message.to_s.empty?
|
|
232
|
+
return if @editor.command_line_active?
|
|
233
|
+
return if @editor.hit_enter_active?
|
|
234
|
+
return if key == :ctrl_c
|
|
235
|
+
|
|
236
|
+
@editor.clear_message
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# --- Normal mode ---
|
|
240
|
+
|
|
241
|
+
def handle_normal_key(key)
|
|
242
|
+
case
|
|
243
|
+
when handle_normal_key_pre_dispatch(key)
|
|
244
|
+
when (token = normalize_key_token(key)).nil?
|
|
245
|
+
when handle_normal_pending_state(token)
|
|
246
|
+
when handle_normal_direct_token(token)
|
|
247
|
+
else
|
|
248
|
+
@pending_keys ||= []
|
|
249
|
+
@pending_keys << token
|
|
250
|
+
resolve_normal_key_sequence
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def handle_normal_key_pre_dispatch(key)
|
|
255
|
+
case
|
|
256
|
+
when key == :enter && handle_list_window_enter
|
|
257
|
+
when digit_key?(key) && count_digit_allowed?(key)
|
|
258
|
+
@editor.pending_count = (@editor.pending_count.to_s + key).to_i
|
|
259
|
+
@editor.echo(@editor.pending_count.to_s)
|
|
260
|
+
@pending_keys = []
|
|
261
|
+
else
|
|
262
|
+
return false
|
|
263
|
+
end
|
|
264
|
+
true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def handle_normal_pending_state(token)
|
|
268
|
+
case
|
|
269
|
+
when @pending_keys && !@pending_keys.empty?
|
|
270
|
+
@pending_keys << token
|
|
271
|
+
resolve_normal_key_sequence
|
|
272
|
+
when @operator_pending
|
|
273
|
+
handle_operator_pending_key(token)
|
|
274
|
+
when @register_pending
|
|
275
|
+
finish_register_pending(token)
|
|
276
|
+
when @mark_pending
|
|
277
|
+
finish_mark_pending(token)
|
|
278
|
+
when @jump_pending
|
|
279
|
+
finish_jump_pending(token)
|
|
280
|
+
when @macro_record_pending
|
|
281
|
+
finish_macro_record_pending(token)
|
|
282
|
+
when @macro_play_pending
|
|
283
|
+
finish_macro_play_pending(token)
|
|
284
|
+
when @replace_pending
|
|
285
|
+
handle_replace_pending_key(token)
|
|
286
|
+
when @find_pending
|
|
287
|
+
finish_find_pending(token)
|
|
288
|
+
else
|
|
289
|
+
return false
|
|
290
|
+
end
|
|
291
|
+
true
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def handle_normal_direct_token(token)
|
|
295
|
+
false
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def resolve_normal_key_sequence
|
|
299
|
+
match = @keymaps.resolve_with_context(:normal, @pending_keys, editor: @editor)
|
|
300
|
+
case match.status
|
|
301
|
+
when :pending, :ambiguous
|
|
302
|
+
if match.status == :ambiguous && match.invocation
|
|
303
|
+
inv = dup_invocation(match.invocation)
|
|
304
|
+
inv.count = @editor.pending_count
|
|
305
|
+
@pending_ambiguous_invocation = inv
|
|
306
|
+
else
|
|
307
|
+
@pending_ambiguous_invocation = nil
|
|
308
|
+
end
|
|
309
|
+
arm_pending_key_timeout
|
|
310
|
+
return
|
|
311
|
+
when :match
|
|
312
|
+
clear_pending_key_timeout
|
|
313
|
+
matched_keys = @pending_keys.dup
|
|
314
|
+
repeat_count = @editor.pending_count
|
|
315
|
+
@pending_keys = []
|
|
316
|
+
invocation = dup_invocation(match.invocation)
|
|
317
|
+
invocation.count = repeat_count
|
|
318
|
+
if @editor.rich_mode? && rich_mode_block_command?(invocation.id)
|
|
319
|
+
@editor.pending_count = nil
|
|
320
|
+
@pending_keys = []
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
@dispatcher.dispatch(@editor, invocation)
|
|
324
|
+
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
325
|
+
else
|
|
326
|
+
clear_pending_key_timeout
|
|
327
|
+
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
328
|
+
end
|
|
329
|
+
@editor.pending_count = nil
|
|
330
|
+
@pending_keys = []
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# --- Insert mode ---
|
|
334
|
+
|
|
335
|
+
def handle_insert_key(key)
|
|
336
|
+
case key
|
|
337
|
+
when :escape
|
|
338
|
+
finish_insert_change_group
|
|
339
|
+
finish_dot_change_capture
|
|
340
|
+
@completion.clear_insert_completion
|
|
341
|
+
@editor.enter_normal_mode
|
|
342
|
+
@editor.echo("")
|
|
343
|
+
when :backspace
|
|
344
|
+
@completion.clear_insert_completion
|
|
345
|
+
return unless insert_backspace_allowed?
|
|
346
|
+
insert_backspace_in_insert_mode
|
|
347
|
+
when :ctrl_n
|
|
348
|
+
@completion.insert_complete(+1)
|
|
349
|
+
when :ctrl_p
|
|
350
|
+
@completion.insert_complete(-1)
|
|
351
|
+
when :ctrl_i
|
|
352
|
+
@completion.clear_insert_completion
|
|
353
|
+
insert_tab_in_insert_mode
|
|
354
|
+
when :enter
|
|
355
|
+
@completion.clear_insert_completion
|
|
356
|
+
y, x = @editor.current_buffer.insert_newline(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
|
|
357
|
+
x = apply_insert_autoindent(y, x, previous_row: y - 1)
|
|
358
|
+
@editor.current_window.cursor_y = y
|
|
359
|
+
@editor.current_window.cursor_x = x
|
|
360
|
+
when :left
|
|
361
|
+
@completion.clear_insert_completion
|
|
362
|
+
dispatch_insert_cursor_motion("cursor.left")
|
|
363
|
+
when :right
|
|
364
|
+
@completion.clear_insert_completion
|
|
365
|
+
dispatch_insert_cursor_motion("cursor.right")
|
|
366
|
+
when :up
|
|
367
|
+
@completion.clear_insert_completion
|
|
368
|
+
@editor.current_window.move_up(@editor.current_buffer, 1)
|
|
369
|
+
when :down
|
|
370
|
+
@completion.clear_insert_completion
|
|
371
|
+
@editor.current_window.move_down(@editor.current_buffer, 1)
|
|
372
|
+
when :pageup, :pagedown
|
|
373
|
+
@completion.clear_insert_completion
|
|
374
|
+
invoke_page_key(key)
|
|
375
|
+
else
|
|
376
|
+
return unless key.is_a?(String)
|
|
377
|
+
|
|
378
|
+
@completion.clear_insert_completion
|
|
379
|
+
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
380
|
+
@editor.current_window.cursor_x += 1
|
|
381
|
+
maybe_showmatch_after_insert(key)
|
|
382
|
+
maybe_dedent_after_insert(key)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# --- Visual mode ---
|
|
387
|
+
|
|
388
|
+
def handle_visual_key(key)
|
|
389
|
+
if arrow_key?(key)
|
|
390
|
+
invoke_arrow(key)
|
|
391
|
+
return
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
if paging_key?(key)
|
|
395
|
+
invoke_page_key(key)
|
|
396
|
+
return
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
token = normalize_key_token(key)
|
|
400
|
+
return if token.nil?
|
|
401
|
+
|
|
402
|
+
case token
|
|
403
|
+
when "\e"
|
|
404
|
+
@register_pending = false
|
|
405
|
+
@visual_pending = nil
|
|
406
|
+
@editor.enter_normal_mode
|
|
407
|
+
when "v"
|
|
408
|
+
if @editor.mode == :visual_char
|
|
409
|
+
@editor.enter_normal_mode
|
|
410
|
+
else
|
|
411
|
+
@editor.enter_visual(:visual_char)
|
|
412
|
+
end
|
|
413
|
+
when "V"
|
|
414
|
+
if @editor.mode == :visual_line
|
|
415
|
+
@editor.enter_normal_mode
|
|
416
|
+
else
|
|
417
|
+
@editor.enter_visual(:visual_line)
|
|
418
|
+
end
|
|
419
|
+
when "<C-v>"
|
|
420
|
+
if @editor.mode == :visual_block
|
|
421
|
+
@editor.enter_normal_mode
|
|
422
|
+
else
|
|
423
|
+
@editor.enter_visual(:visual_block)
|
|
424
|
+
end
|
|
425
|
+
when "y"
|
|
426
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_yank"))
|
|
427
|
+
when "d"
|
|
428
|
+
@visual_pending = nil
|
|
429
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
|
|
430
|
+
when "="
|
|
431
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_indent"))
|
|
432
|
+
when "\""
|
|
433
|
+
start_register_pending
|
|
434
|
+
when "i", "a"
|
|
435
|
+
@visual_pending = token
|
|
436
|
+
else
|
|
437
|
+
if @register_pending
|
|
438
|
+
finish_register_pending(token)
|
|
439
|
+
return
|
|
440
|
+
end
|
|
441
|
+
if @visual_pending
|
|
442
|
+
if @editor.mode == :visual_block
|
|
443
|
+
@visual_pending = nil
|
|
444
|
+
@editor.echo_error("text object in Visual block not supported yet")
|
|
445
|
+
return
|
|
446
|
+
end
|
|
447
|
+
motion = "#{@visual_pending}#{token}"
|
|
448
|
+
@visual_pending = nil
|
|
449
|
+
inv = CommandInvocation.new(id: "buffer.visual_select_text_object", kwargs: { motion: motion })
|
|
450
|
+
@dispatcher.dispatch(@editor, inv)
|
|
451
|
+
else
|
|
452
|
+
handle_visual_motion_token(token)
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
@editor.pending_count = nil
|
|
456
|
+
@pending_keys = []
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def handle_visual_motion_token(token)
|
|
460
|
+
id = {
|
|
461
|
+
"h" => "cursor.left",
|
|
462
|
+
"j" => "cursor.down",
|
|
463
|
+
"k" => "cursor.up",
|
|
464
|
+
"l" => "cursor.right",
|
|
465
|
+
"0" => "cursor.line_start",
|
|
466
|
+
"$" => "cursor.line_end",
|
|
467
|
+
"^" => "cursor.first_nonblank",
|
|
468
|
+
"w" => "cursor.word_forward",
|
|
469
|
+
"b" => "cursor.word_backward",
|
|
470
|
+
"e" => "cursor.word_end",
|
|
471
|
+
"G" => "cursor.buffer_end"
|
|
472
|
+
}[token]
|
|
473
|
+
|
|
474
|
+
if token == "g"
|
|
475
|
+
@pending_keys ||= []
|
|
476
|
+
@pending_keys << token
|
|
477
|
+
arm_pending_key_timeout
|
|
478
|
+
return
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
if @pending_keys == ["g"] && token == "g"
|
|
482
|
+
id = "cursor.buffer_start"
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
if id
|
|
486
|
+
clear_pending_key_timeout
|
|
487
|
+
count = @editor.pending_count
|
|
488
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
489
|
+
else
|
|
490
|
+
clear_pending_key_timeout
|
|
491
|
+
@editor.echo_error("Unknown visual key: #{token}")
|
|
492
|
+
end
|
|
493
|
+
ensure
|
|
494
|
+
@pending_keys = [] unless token == "g"
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# --- Command-line mode ---
|
|
498
|
+
|
|
499
|
+
def handle_command_line_key(key)
|
|
500
|
+
cmd = @editor.command_line
|
|
501
|
+
case key
|
|
502
|
+
when :escape
|
|
503
|
+
@completion.clear_command_line_completion
|
|
504
|
+
@completion.cancel_incsearch_preview_if_any
|
|
505
|
+
@editor.cancel_command_line
|
|
506
|
+
when :enter
|
|
507
|
+
@completion.clear_command_line_completion
|
|
508
|
+
line = cmd.text.dup
|
|
509
|
+
@completion.push_history(cmd.prefix, line)
|
|
510
|
+
handle_command_line_submit(cmd.prefix, line)
|
|
511
|
+
when :backspace
|
|
512
|
+
@completion.clear_command_line_completion
|
|
513
|
+
if cmd.text.empty? && cmd.cursor.zero?
|
|
514
|
+
@completion.cancel_incsearch_preview_if_any
|
|
515
|
+
@editor.cancel_command_line
|
|
516
|
+
return
|
|
517
|
+
end
|
|
518
|
+
cmd.backspace
|
|
519
|
+
when :up
|
|
520
|
+
@completion.clear_command_line_completion
|
|
521
|
+
@completion.history_move(-1)
|
|
522
|
+
when :down
|
|
523
|
+
@completion.clear_command_line_completion
|
|
524
|
+
@completion.history_move(1)
|
|
525
|
+
when :left
|
|
526
|
+
@completion.clear_command_line_completion
|
|
527
|
+
cmd.move_left
|
|
528
|
+
when :right
|
|
529
|
+
@completion.clear_command_line_completion
|
|
530
|
+
cmd.move_right
|
|
531
|
+
else
|
|
532
|
+
if key == :ctrl_i
|
|
533
|
+
@completion.command_line_complete
|
|
534
|
+
elsif key.is_a?(String)
|
|
535
|
+
@completion.clear_command_line_completion
|
|
536
|
+
@completion.reset_history_index!
|
|
537
|
+
cmd.insert(key)
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
@completion.update_incsearch_preview_if_needed
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# --- Hit-enter, rich mode ---
|
|
544
|
+
|
|
545
|
+
def handle_hit_enter_key(key)
|
|
546
|
+
token = normalize_key_token(key)
|
|
547
|
+
case token
|
|
548
|
+
when ":"
|
|
549
|
+
@editor.exit_hit_enter_mode
|
|
550
|
+
@editor.enter_command_line_mode(":")
|
|
551
|
+
when "/", "?"
|
|
552
|
+
@editor.exit_hit_enter_mode
|
|
553
|
+
@editor.enter_command_line_mode(token)
|
|
554
|
+
else
|
|
555
|
+
@editor.exit_hit_enter_mode
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def handle_rich_key(key)
|
|
560
|
+
token = normalize_key_token(key)
|
|
561
|
+
if token == "\e"
|
|
562
|
+
RuVim::RichView.close!(@editor)
|
|
563
|
+
return
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
handle_normal_key(key)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def rich_mode_block_command?(command_id)
|
|
570
|
+
RICH_MODE_BLOCKED_COMMANDS.include?(command_id.to_s)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# --- Ctrl-C / suspend ---
|
|
574
|
+
|
|
575
|
+
def handle_ctrl_c
|
|
576
|
+
case @editor.mode
|
|
577
|
+
when :hit_enter
|
|
578
|
+
@editor.exit_hit_enter_mode
|
|
579
|
+
when :insert
|
|
580
|
+
finish_insert_change_group
|
|
581
|
+
finish_dot_change_capture
|
|
582
|
+
@completion.clear_insert_completion
|
|
583
|
+
clear_pending_key_timeout
|
|
584
|
+
@editor.enter_normal_mode
|
|
585
|
+
@editor.echo("")
|
|
586
|
+
when :command_line
|
|
587
|
+
clear_pending_key_timeout
|
|
588
|
+
@completion.cancel_incsearch_preview_if_any
|
|
589
|
+
@editor.cancel_command_line
|
|
590
|
+
when :visual_char, :visual_line, :visual_block
|
|
591
|
+
@visual_pending = nil
|
|
592
|
+
@register_pending = false
|
|
593
|
+
@mark_pending = false
|
|
594
|
+
@jump_pending = nil
|
|
595
|
+
clear_pending_key_timeout
|
|
596
|
+
@editor.enter_normal_mode
|
|
597
|
+
when :rich
|
|
598
|
+
clear_pending_key_timeout
|
|
599
|
+
@editor.pending_count = nil
|
|
600
|
+
@pending_keys = []
|
|
601
|
+
@operator_pending = nil
|
|
602
|
+
@replace_pending = nil
|
|
603
|
+
@register_pending = false
|
|
604
|
+
@mark_pending = false
|
|
605
|
+
@jump_pending = nil
|
|
606
|
+
@macro_record_pending = false
|
|
607
|
+
@macro_play_pending = false
|
|
608
|
+
RuVim::RichView.close!(@editor)
|
|
609
|
+
else
|
|
610
|
+
clear_pending_key_timeout
|
|
611
|
+
@editor.pending_count = nil
|
|
612
|
+
@pending_keys = []
|
|
613
|
+
@operator_pending = nil
|
|
614
|
+
@replace_pending = nil
|
|
615
|
+
@register_pending = false
|
|
616
|
+
@mark_pending = false
|
|
617
|
+
@jump_pending = nil
|
|
618
|
+
@macro_record_pending = false
|
|
619
|
+
@macro_play_pending = false
|
|
620
|
+
@editor.clear_message
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def suspend_to_shell
|
|
625
|
+
@terminal.suspend_for_tstp
|
|
626
|
+
@screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
|
|
627
|
+
rescue StandardError => e
|
|
628
|
+
@editor.echo_error("suspend failed: #{e.message}")
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# --- Command-line submit ---
|
|
632
|
+
|
|
633
|
+
def handle_command_line_submit(prefix, line)
|
|
634
|
+
@completion.clear_incsearch_preview_state(apply: false) if %w[/ ?].include?(prefix)
|
|
635
|
+
case prefix
|
|
636
|
+
when ":"
|
|
637
|
+
@dispatcher.dispatch_ex(@editor, line)
|
|
638
|
+
when "/"
|
|
639
|
+
submit_search(line, direction: :forward)
|
|
640
|
+
when "?"
|
|
641
|
+
submit_search(line, direction: :backward)
|
|
642
|
+
else
|
|
643
|
+
@editor.echo_error("Unknown command-line prefix: #{prefix}")
|
|
644
|
+
@editor.enter_normal_mode
|
|
645
|
+
end
|
|
646
|
+
@completion.reset_history_index!
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# --- List/quickfix/filter enter handlers ---
|
|
650
|
+
|
|
651
|
+
def handle_list_window_enter
|
|
652
|
+
buffer = @editor.current_buffer
|
|
653
|
+
return handle_filter_buffer_enter if buffer.kind == :filter
|
|
654
|
+
return handle_git_status_enter if buffer.kind == :git_status
|
|
655
|
+
return handle_git_diff_enter if buffer.kind == :git_diff || buffer.kind == :git_log
|
|
656
|
+
return handle_git_grep_enter if buffer.kind == :git_grep
|
|
657
|
+
return handle_git_branch_enter if buffer.kind == :git_branch
|
|
658
|
+
return false unless buffer.kind == :quickfix || buffer.kind == :location_list
|
|
659
|
+
|
|
660
|
+
item_index = @editor.current_window.cursor_y - 2
|
|
661
|
+
if item_index.negative?
|
|
662
|
+
@editor.echo_error("No list item on this line")
|
|
663
|
+
return true
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
source_window_id = buffer.options["ruvim_list_source_window_id"]
|
|
667
|
+
source_window_id = source_window_id.to_i if source_window_id
|
|
668
|
+
source_window_id = nil unless source_window_id && @editor.windows.key?(source_window_id)
|
|
669
|
+
|
|
670
|
+
item =
|
|
671
|
+
if buffer.kind == :quickfix
|
|
672
|
+
@editor.select_quickfix(item_index)
|
|
673
|
+
else
|
|
674
|
+
owner_window_id = source_window_id || @editor.current_window_id
|
|
675
|
+
@editor.select_location_list(item_index, window_id: owner_window_id)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
unless item
|
|
679
|
+
@editor.echo_error("#{buffer.kind == :quickfix ? 'quickfix' : 'location list'} item not found")
|
|
680
|
+
return true
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
if source_window_id
|
|
684
|
+
@editor.current_window_id = source_window_id
|
|
685
|
+
end
|
|
686
|
+
@editor.jump_to_location(item)
|
|
687
|
+
@editor.echo(
|
|
688
|
+
if buffer.kind == :quickfix
|
|
689
|
+
"qf #{@editor.quickfix_index.to_i + 1}/#{@editor.quickfix_items.length}"
|
|
690
|
+
else
|
|
691
|
+
owner_window_id = source_window_id || @editor.current_window_id
|
|
692
|
+
list = @editor.location_list(owner_window_id)
|
|
693
|
+
"ll #{list[:index].to_i + 1}/#{list[:items].length}"
|
|
694
|
+
end
|
|
695
|
+
)
|
|
696
|
+
true
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def handle_filter_buffer_enter
|
|
700
|
+
buffer = @editor.current_buffer
|
|
701
|
+
origins = buffer.options["filter_origins"]
|
|
702
|
+
return false unless origins
|
|
703
|
+
|
|
704
|
+
row = @editor.current_window.cursor_y
|
|
705
|
+
origin = origins[row]
|
|
706
|
+
unless origin
|
|
707
|
+
@editor.echo_error("No filter item on this line")
|
|
708
|
+
return true
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
target_buffer_id = origin[:buffer_id]
|
|
712
|
+
target_row = origin[:row]
|
|
713
|
+
filter_buf_id = buffer.id
|
|
714
|
+
|
|
715
|
+
@editor.delete_buffer(filter_buf_id)
|
|
716
|
+
target_buf = @editor.buffers[target_buffer_id]
|
|
717
|
+
if target_buf
|
|
718
|
+
@editor.switch_to_buffer(target_buffer_id) unless @editor.current_buffer.id == target_buffer_id
|
|
719
|
+
@editor.current_window.cursor_y = [target_row, target_buf.lines.length - 1].min
|
|
720
|
+
@editor.current_window.cursor_x = 0
|
|
721
|
+
end
|
|
722
|
+
true
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def handle_git_status_enter
|
|
726
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.status.open_file"))
|
|
727
|
+
true
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def handle_git_diff_enter
|
|
731
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.diff.open_file"))
|
|
732
|
+
true
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def handle_git_grep_enter
|
|
736
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.grep.open_file"))
|
|
737
|
+
true
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def handle_git_branch_enter
|
|
741
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.branch.checkout"))
|
|
742
|
+
true
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# --- Key helpers ---
|
|
746
|
+
|
|
747
|
+
def arrow_key?(key)
|
|
748
|
+
%i[left right up down].include?(key)
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def paging_key?(key)
|
|
752
|
+
%i[pageup pagedown].include?(key)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def invoke_arrow(key)
|
|
756
|
+
id = {
|
|
757
|
+
left: "cursor.left",
|
|
758
|
+
right: "cursor.right",
|
|
759
|
+
up: "cursor.up",
|
|
760
|
+
down: "cursor.down"
|
|
761
|
+
}.fetch(key)
|
|
762
|
+
inv = CommandInvocation.new(id:, count: @editor.pending_count)
|
|
763
|
+
@dispatcher.dispatch(@editor, inv)
|
|
764
|
+
@editor.pending_count = nil
|
|
765
|
+
@pending_keys = []
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def invoke_page_key(key)
|
|
769
|
+
id = (key == :pageup ? "cursor.page_up" : "cursor.page_down")
|
|
770
|
+
inv = CommandInvocation.new(
|
|
771
|
+
id: id,
|
|
772
|
+
count: @editor.pending_count,
|
|
773
|
+
kwargs: { page_lines: current_page_step_lines }
|
|
774
|
+
)
|
|
775
|
+
@dispatcher.dispatch(@editor, inv)
|
|
776
|
+
@editor.pending_count = nil
|
|
777
|
+
@pending_keys = []
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def digit_key?(key)
|
|
781
|
+
key.is_a?(String) && key.match?(/\A\d\z/)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def count_digit_allowed?(key)
|
|
785
|
+
return false unless @editor.mode == :normal
|
|
786
|
+
return true unless @editor.pending_count.nil?
|
|
787
|
+
|
|
788
|
+
key != "0"
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def normalize_key_token(key)
|
|
792
|
+
case key
|
|
793
|
+
when String then key
|
|
794
|
+
when :escape then "\e"
|
|
795
|
+
when :ctrl_r then "<C-r>"
|
|
796
|
+
when :ctrl_d then "<C-d>"
|
|
797
|
+
when :ctrl_u then "<C-u>"
|
|
798
|
+
when :ctrl_f then "<C-f>"
|
|
799
|
+
when :ctrl_b then "<C-b>"
|
|
800
|
+
when :ctrl_e then "<C-e>"
|
|
801
|
+
when :ctrl_y then "<C-y>"
|
|
802
|
+
when :ctrl_v then "<C-v>"
|
|
803
|
+
when :ctrl_i then "<C-i>"
|
|
804
|
+
when :ctrl_o then "<C-o>"
|
|
805
|
+
when :ctrl_w then "<C-w>"
|
|
806
|
+
when :ctrl_l then "<C-l>"
|
|
807
|
+
when :ctrl_c then "<C-c>"
|
|
808
|
+
when :ctrl_g then "<C-g>"
|
|
809
|
+
when :left then "<Left>"
|
|
810
|
+
when :right then "<Right>"
|
|
811
|
+
when :up then "<Up>"
|
|
812
|
+
when :down then "<Down>"
|
|
813
|
+
when :home then "<Home>"
|
|
814
|
+
when :end then "<End>"
|
|
815
|
+
when :pageup then "<PageUp>"
|
|
816
|
+
when :pagedown then "<PageDown>"
|
|
817
|
+
when :shift_up then "<S-Up>"
|
|
818
|
+
when :shift_down then "<S-Down>"
|
|
819
|
+
when :shift_left then "<S-Left>"
|
|
820
|
+
when :shift_right then "<S-Right>"
|
|
821
|
+
else nil
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def dup_invocation(inv)
|
|
826
|
+
CommandInvocation.new(
|
|
827
|
+
id: inv.id,
|
|
828
|
+
argv: inv.argv.dup,
|
|
829
|
+
kwargs: inv.kwargs.dup,
|
|
830
|
+
count: inv.count,
|
|
831
|
+
bang: inv.bang,
|
|
832
|
+
raw_keys: inv.raw_keys&.dup
|
|
833
|
+
)
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# --- Mode transition tracking ---
|
|
837
|
+
|
|
838
|
+
def track_mode_transition(mode_before)
|
|
839
|
+
mode_after = @editor.mode
|
|
840
|
+
if mode_before != :insert && mode_after == :insert
|
|
841
|
+
@insert_start_location = @editor.current_location
|
|
842
|
+
elsif mode_before == :insert && mode_after != :insert
|
|
843
|
+
@insert_start_location = nil
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
if mode_before != :command_line && mode_after == :command_line
|
|
847
|
+
@completion.clear_incsearch_preview_state(apply: false) rescue nil
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def finish_insert_change_group
|
|
852
|
+
@editor.current_buffer.end_change_group
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
# --- Insert editing helpers ---
|
|
856
|
+
|
|
857
|
+
def insert_backspace_allowed?
|
|
858
|
+
buf = @editor.current_buffer
|
|
859
|
+
win = @editor.current_window
|
|
860
|
+
row = win.cursor_y
|
|
861
|
+
col = win.cursor_x
|
|
862
|
+
return false if row.zero? && col.zero?
|
|
863
|
+
|
|
864
|
+
opt = @editor.effective_option("backspace", window: win, buffer: buf).to_s
|
|
865
|
+
allow = opt.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
866
|
+
allow_all = allow.include?("2")
|
|
867
|
+
allow_indent = allow_all || allow.include?("indent")
|
|
868
|
+
|
|
869
|
+
if col.zero? && row.positive?
|
|
870
|
+
return true if allow_all || allow.include?("eol")
|
|
871
|
+
|
|
872
|
+
@editor.echo_error("backspace=eol required")
|
|
873
|
+
return false
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
if @insert_start_location
|
|
877
|
+
same_buf = @insert_start_location[:buffer_id] == buf.id
|
|
878
|
+
if same_buf && (row < @insert_start_location[:row] || (row == @insert_start_location[:row] && col <= @insert_start_location[:col]))
|
|
879
|
+
if allow_all || allow.include?("start")
|
|
880
|
+
return true
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
if allow_indent && same_row_autoindent_backspace?(buf, row, col)
|
|
884
|
+
return true
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
@editor.echo_error("backspace=start required")
|
|
888
|
+
return false
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
true
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def insert_backspace_in_insert_mode
|
|
896
|
+
buf = @editor.current_buffer
|
|
897
|
+
win = @editor.current_window
|
|
898
|
+
row = win.cursor_y
|
|
899
|
+
col = win.cursor_x
|
|
900
|
+
|
|
901
|
+
if row >= 0 && col.positive? && try_softtabstop_backspace(buf, win)
|
|
902
|
+
return
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
y, x = buf.backspace(row, col)
|
|
906
|
+
win.cursor_y = y
|
|
907
|
+
win.cursor_x = x
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def dispatch_insert_cursor_motion(id)
|
|
911
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: id, count: 1))
|
|
912
|
+
rescue StandardError => e
|
|
913
|
+
@editor.echo_error("Motion error: #{e.message}")
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def try_softtabstop_backspace(buf, win)
|
|
917
|
+
row = win.cursor_y
|
|
918
|
+
col = win.cursor_x
|
|
919
|
+
line = buf.line_at(row)
|
|
920
|
+
return false unless line
|
|
921
|
+
return false unless @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
922
|
+
|
|
923
|
+
sts = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
924
|
+
sts = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if sts <= 0
|
|
925
|
+
return false if sts <= 0
|
|
926
|
+
|
|
927
|
+
prefix = line[0...col].to_s
|
|
928
|
+
m = prefix.match(/ +\z/)
|
|
929
|
+
return false unless m
|
|
930
|
+
|
|
931
|
+
run = m[0].length
|
|
932
|
+
return false if run <= 1
|
|
933
|
+
|
|
934
|
+
tabstop = effective_tabstop(win, buf)
|
|
935
|
+
cur_screen = RuVim::TextMetrics.screen_col_for_char_index(line, col, tabstop:)
|
|
936
|
+
target_screen = [cur_screen - sts, 0].max
|
|
937
|
+
target_col = RuVim::TextMetrics.char_index_for_screen_col(line, target_screen, tabstop:, align: :floor)
|
|
938
|
+
delete_cols = col - target_col
|
|
939
|
+
delete_cols = [delete_cols, run, sts].min
|
|
940
|
+
return false if delete_cols <= 1
|
|
941
|
+
|
|
942
|
+
run_start = col - run
|
|
943
|
+
target_col = [target_col, run_start].max
|
|
944
|
+
delete_cols = col - target_col
|
|
945
|
+
return false if delete_cols <= 1
|
|
946
|
+
|
|
947
|
+
buf.delete_span(row, target_col, row, col)
|
|
948
|
+
win.cursor_x = target_col
|
|
949
|
+
true
|
|
950
|
+
rescue StandardError
|
|
951
|
+
false
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def same_row_autoindent_backspace?(buf, row, col)
|
|
955
|
+
return false unless @insert_start_location
|
|
956
|
+
return false unless row == @insert_start_location[:row]
|
|
957
|
+
return false unless col <= @insert_start_location[:col]
|
|
958
|
+
|
|
959
|
+
line = buf.line_at(row)
|
|
960
|
+
line[0...@insert_start_location[:col]].to_s.match?(/\A[ \t]*\z/)
|
|
961
|
+
rescue StandardError
|
|
962
|
+
false
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def insert_tab_in_insert_mode
|
|
966
|
+
buf = @editor.current_buffer
|
|
967
|
+
win = @editor.current_window
|
|
968
|
+
if @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
969
|
+
width = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
970
|
+
width = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if width <= 0
|
|
971
|
+
width = 2 if width <= 0
|
|
972
|
+
line = buf.line_at(win.cursor_y)
|
|
973
|
+
current_col = RuVim::TextMetrics.screen_col_for_char_index(line, win.cursor_x, tabstop: effective_tabstop(win, buf))
|
|
974
|
+
spaces = width - (current_col % width)
|
|
975
|
+
spaces = width if spaces <= 0
|
|
976
|
+
_y, x = buf.insert_text(win.cursor_y, win.cursor_x, " " * spaces)
|
|
977
|
+
win.cursor_x = x
|
|
978
|
+
else
|
|
979
|
+
buf.insert_char(win.cursor_y, win.cursor_x, "\t")
|
|
980
|
+
win.cursor_x += 1
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def apply_insert_autoindent(row, x, previous_row:)
|
|
985
|
+
return x if @paste_batch
|
|
986
|
+
buf = @editor.current_buffer
|
|
987
|
+
win = @editor.current_window
|
|
988
|
+
return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
|
|
989
|
+
return x if previous_row.negative?
|
|
990
|
+
|
|
991
|
+
prev = buf.line_at(previous_row)
|
|
992
|
+
indent = prev[/\A[ \t]*/].to_s
|
|
993
|
+
if @editor.effective_option("smartindent", window: win, buffer: buf)
|
|
994
|
+
trimmed = prev.rstrip
|
|
995
|
+
needs_indent = trimmed.end_with?("{", "[", "(")
|
|
996
|
+
if !needs_indent
|
|
997
|
+
needs_indent = buf.lang_module.indent_trigger?(trimmed)
|
|
998
|
+
end
|
|
999
|
+
if needs_indent
|
|
1000
|
+
sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
|
|
1001
|
+
sw = effective_tabstop(win, buf) if sw <= 0
|
|
1002
|
+
sw = 2 if sw <= 0
|
|
1003
|
+
indent += " " * sw
|
|
1004
|
+
end
|
|
1005
|
+
end
|
|
1006
|
+
return x if indent.empty?
|
|
1007
|
+
|
|
1008
|
+
_y, new_x = buf.insert_text(row, x, indent)
|
|
1009
|
+
new_x
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def maybe_showmatch_after_insert(key)
|
|
1013
|
+
return unless [")", "]", "}"].include?(key)
|
|
1014
|
+
return unless @editor.effective_option("showmatch")
|
|
1015
|
+
|
|
1016
|
+
mt = @editor.effective_option("matchtime").to_i
|
|
1017
|
+
mt = 5 if mt <= 0
|
|
1018
|
+
@editor.echo_temporary("match", duration_seconds: mt * 0.1)
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
def maybe_dedent_after_insert(key)
|
|
1022
|
+
return unless @editor.effective_option("smartindent", window: @editor.current_window, buffer: @editor.current_buffer)
|
|
1023
|
+
|
|
1024
|
+
buf = @editor.current_buffer
|
|
1025
|
+
lang_mod = buf.lang_module
|
|
1026
|
+
|
|
1027
|
+
pattern = lang_mod.dedent_trigger(key)
|
|
1028
|
+
return unless pattern
|
|
1029
|
+
|
|
1030
|
+
row = @editor.current_window.cursor_y
|
|
1031
|
+
line = buf.line_at(row)
|
|
1032
|
+
m = line.match(pattern)
|
|
1033
|
+
return unless m
|
|
1034
|
+
|
|
1035
|
+
sw = @editor.effective_option("shiftwidth", buffer: buf).to_i
|
|
1036
|
+
sw = 2 if sw <= 0
|
|
1037
|
+
target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
|
|
1038
|
+
return unless target_indent
|
|
1039
|
+
|
|
1040
|
+
current_indent = m[1].length
|
|
1041
|
+
return if current_indent == target_indent
|
|
1042
|
+
|
|
1043
|
+
stripped = line.strip
|
|
1044
|
+
buf.delete_span(row, 0, row, current_indent) if current_indent > 0
|
|
1045
|
+
buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
|
|
1046
|
+
@editor.current_window.cursor_x = target_indent + stripped.length
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def effective_tabstop(window = @editor.current_window, buffer = @editor.current_buffer)
|
|
1050
|
+
v = @editor.effective_option("tabstop", window:, buffer:).to_i
|
|
1051
|
+
v.positive? ? v : 2
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
# --- Operator pending ---
|
|
1055
|
+
|
|
1056
|
+
def start_operator_pending(name)
|
|
1057
|
+
@operator_pending = { name:, count: @editor.pending_count }
|
|
1058
|
+
@editor.pending_count = nil
|
|
1059
|
+
@pending_keys = []
|
|
1060
|
+
@editor.echo(name == :delete ? "d" : name.to_s)
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
def handle_operator_pending_key(token)
|
|
1064
|
+
op = @operator_pending
|
|
1065
|
+
if %w[i a g].include?(token) && !op[:motion_prefix]
|
|
1066
|
+
@operator_pending[:motion_prefix] = token
|
|
1067
|
+
@editor.echo("#{op[:name].to_s[0]}#{token}")
|
|
1068
|
+
return
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
motion = [op[:motion_prefix], token].compact.join
|
|
1072
|
+
@operator_pending = nil
|
|
1073
|
+
|
|
1074
|
+
if token == "\e"
|
|
1075
|
+
@editor.clear_message
|
|
1076
|
+
return
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
if op[:name] == :delete && motion == "d"
|
|
1080
|
+
inv = CommandInvocation.new(id: "buffer.delete_line", count: op[:count])
|
|
1081
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1082
|
+
record_last_change_keys(count_prefixed_keys(op[:count], ["d", "d"]))
|
|
1083
|
+
return
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
if op[:name] == :delete
|
|
1087
|
+
inv = CommandInvocation.new(id: "buffer.delete_motion", count: op[:count], kwargs: { motion: motion })
|
|
1088
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1089
|
+
record_last_change_keys(count_prefixed_keys(op[:count], ["d", *motion.each_char.to_a]))
|
|
1090
|
+
return
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
if op[:name] == :yank && motion == "y"
|
|
1094
|
+
inv = CommandInvocation.new(id: "buffer.yank_line", count: op[:count])
|
|
1095
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1096
|
+
return
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
if op[:name] == :yank
|
|
1100
|
+
inv = CommandInvocation.new(id: "buffer.yank_motion", count: op[:count], kwargs: { motion: motion })
|
|
1101
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1102
|
+
return
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
if op[:name] == :indent && motion == "="
|
|
1106
|
+
inv = CommandInvocation.new(id: "buffer.indent_lines", count: op[:count])
|
|
1107
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1108
|
+
return
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
if op[:name] == :indent
|
|
1112
|
+
inv = CommandInvocation.new(id: "buffer.indent_motion", count: op[:count], kwargs: { motion: motion })
|
|
1113
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1114
|
+
return
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
if op[:name] == :change && motion == "c"
|
|
1118
|
+
inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
|
|
1119
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1120
|
+
begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", "c"])) if @editor.mode == :insert
|
|
1121
|
+
return
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
if op[:name] == :change
|
|
1125
|
+
inv = CommandInvocation.new(id: "buffer.change_motion", count: op[:count], kwargs: { motion: motion })
|
|
1126
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1127
|
+
begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", *motion.each_char.to_a])) if @editor.mode == :insert
|
|
1128
|
+
return
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
@editor.echo_error("Unknown operator")
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
# --- Register / mark / jump pending ---
|
|
1135
|
+
|
|
1136
|
+
def start_register_pending
|
|
1137
|
+
@register_pending = true
|
|
1138
|
+
@editor.echo('"')
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def finish_register_pending(token)
|
|
1142
|
+
@register_pending = false
|
|
1143
|
+
if token.is_a?(String) && token.length == 1
|
|
1144
|
+
@editor.set_active_register(token)
|
|
1145
|
+
@editor.echo(%("#{token}))
|
|
1146
|
+
else
|
|
1147
|
+
@editor.echo_error("Invalid register")
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
def start_mark_pending
|
|
1152
|
+
@mark_pending = true
|
|
1153
|
+
@editor.echo("m")
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
def finish_mark_pending(token)
|
|
1157
|
+
@mark_pending = false
|
|
1158
|
+
if token == "\e"
|
|
1159
|
+
@editor.clear_message
|
|
1160
|
+
return
|
|
1161
|
+
end
|
|
1162
|
+
unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
|
|
1163
|
+
@editor.echo_error("Invalid mark")
|
|
1164
|
+
return
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
inv = CommandInvocation.new(id: "mark.set", kwargs: { mark: token })
|
|
1168
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
def start_jump_pending(linewise:, repeat_token:)
|
|
1172
|
+
@jump_pending = { linewise: linewise, repeat_token: repeat_token }
|
|
1173
|
+
@editor.echo(repeat_token)
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
def finish_jump_pending(token)
|
|
1177
|
+
pending = @jump_pending
|
|
1178
|
+
@jump_pending = nil
|
|
1179
|
+
return unless pending
|
|
1180
|
+
if token == "\e"
|
|
1181
|
+
@editor.clear_message
|
|
1182
|
+
return
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
if token == pending[:repeat_token]
|
|
1186
|
+
inv = CommandInvocation.new(id: "jump.older", kwargs: { linewise: pending[:linewise] })
|
|
1187
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1188
|
+
return
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
|
|
1192
|
+
@editor.echo_error("Invalid mark")
|
|
1193
|
+
return
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
inv = CommandInvocation.new(id: "mark.jump", kwargs: { mark: token, linewise: pending[:linewise] })
|
|
1197
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
# --- Replace pending ---
|
|
1201
|
+
|
|
1202
|
+
def start_replace_pending
|
|
1203
|
+
@replace_pending = { count: @editor.pending_count }
|
|
1204
|
+
@editor.pending_count = nil
|
|
1205
|
+
@pending_keys = []
|
|
1206
|
+
@editor.echo("r")
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
def handle_replace_pending_key(token)
|
|
1210
|
+
pending = @replace_pending
|
|
1211
|
+
@replace_pending = nil
|
|
1212
|
+
if token == "\e"
|
|
1213
|
+
@editor.clear_message
|
|
1214
|
+
return
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
if token.is_a?(String) && !token.empty?
|
|
1218
|
+
inv = CommandInvocation.new(id: "buffer.replace_char", argv: [token], count: pending[:count])
|
|
1219
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1220
|
+
record_last_change_keys(count_prefixed_keys(pending[:count], ["r", token]))
|
|
1221
|
+
else
|
|
1222
|
+
@editor.echo("r expects one character")
|
|
1223
|
+
end
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
# --- Macro recording / playback ---
|
|
1227
|
+
|
|
1228
|
+
def start_macro_record_pending
|
|
1229
|
+
@macro_record_pending = true
|
|
1230
|
+
@editor.echo("q")
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
def toggle_macro_recording_or_start_pending
|
|
1234
|
+
if @editor.macro_recording?
|
|
1235
|
+
stop_macro_recording
|
|
1236
|
+
else
|
|
1237
|
+
start_macro_record_pending
|
|
1238
|
+
end
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
def finish_macro_record_pending(token)
|
|
1242
|
+
@macro_record_pending = false
|
|
1243
|
+
if token == "\e"
|
|
1244
|
+
@editor.clear_message
|
|
1245
|
+
return
|
|
1246
|
+
end
|
|
1247
|
+
unless token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
|
|
1248
|
+
@editor.echo_error("Invalid macro register")
|
|
1249
|
+
return
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
unless @editor.start_macro_recording(token)
|
|
1253
|
+
@editor.echo("Failed to start recording")
|
|
1254
|
+
return
|
|
1255
|
+
end
|
|
1256
|
+
@skip_record_for_current_key = true
|
|
1257
|
+
@editor.echo("recording @#{token}")
|
|
1258
|
+
end
|
|
1259
|
+
|
|
1260
|
+
def stop_macro_recording
|
|
1261
|
+
reg = @editor.macro_recording_name
|
|
1262
|
+
@editor.stop_macro_recording
|
|
1263
|
+
@editor.echo("recording @#{reg} stopped")
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
def start_macro_play_pending
|
|
1267
|
+
@macro_play_pending = true
|
|
1268
|
+
@editor.echo("@")
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
def finish_macro_play_pending(token)
|
|
1272
|
+
@macro_play_pending = false
|
|
1273
|
+
if token == "\e"
|
|
1274
|
+
@editor.clear_message
|
|
1275
|
+
return
|
|
1276
|
+
end
|
|
1277
|
+
name =
|
|
1278
|
+
if token == "@"
|
|
1279
|
+
@last_macro_name
|
|
1280
|
+
elsif token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
|
|
1281
|
+
token
|
|
1282
|
+
end
|
|
1283
|
+
unless name
|
|
1284
|
+
@editor.echo_error("Invalid macro register")
|
|
1285
|
+
return
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
count = @editor.pending_count
|
|
1289
|
+
@editor.pending_count = nil
|
|
1290
|
+
play_macro(name, count:)
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
def play_macro(name, count:)
|
|
1294
|
+
reg = name.to_s.downcase
|
|
1295
|
+
keys = @editor.macro_keys(reg)
|
|
1296
|
+
if keys.nil? || keys.empty?
|
|
1297
|
+
@editor.echo("Macro empty: #{reg}")
|
|
1298
|
+
return
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
@macro_play_stack ||= []
|
|
1302
|
+
if @macro_play_stack.include?(reg) || @macro_play_stack.length >= 20
|
|
1303
|
+
@editor.echo("Macro recursion blocked: #{reg}")
|
|
1304
|
+
return
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
@last_macro_name = reg
|
|
1308
|
+
@macro_play_stack << reg
|
|
1309
|
+
@suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
|
|
1310
|
+
[count.to_i, 1].max.times do
|
|
1311
|
+
keys.each { |k| handle(dup_macro_runtime_key(k)) }
|
|
1312
|
+
end
|
|
1313
|
+
@editor.echo("@#{reg}")
|
|
1314
|
+
ensure
|
|
1315
|
+
@suspend_macro_recording_depth = [(@suspend_macro_recording_depth || 1) - 1, 0].max
|
|
1316
|
+
@macro_play_stack.pop if @macro_play_stack && !@macro_play_stack.empty?
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
def record_macro_key_if_needed(key)
|
|
1320
|
+
return if @skip_record_for_current_key
|
|
1321
|
+
return unless @editor.macro_recording?
|
|
1322
|
+
return if (@suspend_macro_recording_depth || 0).positive?
|
|
1323
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1324
|
+
|
|
1325
|
+
@editor.record_macro_key(key)
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1328
|
+
def dup_macro_runtime_key(key)
|
|
1329
|
+
case key
|
|
1330
|
+
when String
|
|
1331
|
+
key.dup
|
|
1332
|
+
when Array
|
|
1333
|
+
key.map { |v| v.is_a?(String) ? v.dup : v }
|
|
1334
|
+
else
|
|
1335
|
+
key
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
# --- Dot repeat ---
|
|
1340
|
+
|
|
1341
|
+
def repeat_last_change
|
|
1342
|
+
keys = @last_change_keys
|
|
1343
|
+
if keys.nil? || keys.empty?
|
|
1344
|
+
@editor.echo("No previous change")
|
|
1345
|
+
return
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
@dot_replay_depth = (@dot_replay_depth || 0) + 1
|
|
1349
|
+
keys.each { |k| handle(dup_macro_runtime_key(k)) }
|
|
1350
|
+
@editor.echo(".")
|
|
1351
|
+
ensure
|
|
1352
|
+
@dot_replay_depth = [(@dot_replay_depth || 1) - 1, 0].max
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
def maybe_record_simple_dot_change(invocation, matched_keys, count)
|
|
1356
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1357
|
+
|
|
1358
|
+
case invocation.id
|
|
1359
|
+
when "buffer.delete_char", "buffer.delete_motion", "buffer.join_lines", "buffer.swapcase_char", "buffer.paste_after", "buffer.paste_before"
|
|
1360
|
+
record_last_change_keys(count_prefixed_keys(count, matched_keys))
|
|
1361
|
+
when "mode.insert", "mode.append", "mode.append_line_end", "mode.insert_nonblank", "mode.open_below", "mode.open_above", "buffer.substitute_char", "buffer.change_motion", "buffer.change_line"
|
|
1362
|
+
begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
|
|
1363
|
+
end
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
def begin_dot_change_capture(prefix_keys)
|
|
1367
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1368
|
+
|
|
1369
|
+
@dot_change_capture_keys = Array(prefix_keys).map { |k| dup_macro_runtime_key(k) }
|
|
1370
|
+
@dot_change_capture_active = true
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def append_dot_change_capture_key(key)
|
|
1374
|
+
return unless @dot_change_capture_active
|
|
1375
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1376
|
+
|
|
1377
|
+
@dot_change_capture_keys ||= []
|
|
1378
|
+
@dot_change_capture_keys << dup_macro_runtime_key(key)
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
def finish_dot_change_capture
|
|
1382
|
+
return unless @dot_change_capture_active
|
|
1383
|
+
|
|
1384
|
+
keys = Array(@dot_change_capture_keys)
|
|
1385
|
+
@dot_change_capture_active = false
|
|
1386
|
+
@dot_change_capture_keys = nil
|
|
1387
|
+
record_last_change_keys(keys)
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
def record_last_change_keys(keys)
|
|
1391
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1392
|
+
|
|
1393
|
+
@last_change_keys = Array(keys).map { |k| dup_macro_runtime_key(k) }
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
def count_prefixed_keys(count, keys)
|
|
1397
|
+
c = count.to_i
|
|
1398
|
+
prefix = c > 1 ? c.to_s.each_char.to_a : []
|
|
1399
|
+
prefix + Array(keys)
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
# --- Find character on line ---
|
|
1403
|
+
|
|
1404
|
+
def start_find_pending(token)
|
|
1405
|
+
@find_pending = {
|
|
1406
|
+
direction: (token == "f" || token == "t") ? :forward : :backward,
|
|
1407
|
+
till: (token == "t" || token == "T"),
|
|
1408
|
+
count: @editor.pending_count
|
|
1409
|
+
}
|
|
1410
|
+
@editor.pending_count = nil
|
|
1411
|
+
@pending_keys = []
|
|
1412
|
+
@editor.echo(token)
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1415
|
+
def finish_find_pending(token)
|
|
1416
|
+
pending = @find_pending
|
|
1417
|
+
@find_pending = nil
|
|
1418
|
+
if token == "\e"
|
|
1419
|
+
@editor.clear_message
|
|
1420
|
+
return
|
|
1421
|
+
end
|
|
1422
|
+
unless token.is_a?(String) && !token.empty?
|
|
1423
|
+
@editor.echo("find expects one character")
|
|
1424
|
+
return
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
moved = perform_find_on_line(
|
|
1428
|
+
char: token,
|
|
1429
|
+
direction: pending[:direction],
|
|
1430
|
+
till: pending[:till],
|
|
1431
|
+
count: pending[:count]
|
|
1432
|
+
)
|
|
1433
|
+
if moved
|
|
1434
|
+
@editor.set_last_find(char: token, direction: pending[:direction], till: pending[:till])
|
|
1435
|
+
else
|
|
1436
|
+
@editor.echo("Char not found: #{token}")
|
|
1437
|
+
end
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
def repeat_last_find(reverse:)
|
|
1441
|
+
last = @editor.last_find
|
|
1442
|
+
unless last
|
|
1443
|
+
@editor.echo("No previous f/t")
|
|
1444
|
+
return
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
direction =
|
|
1448
|
+
if reverse
|
|
1449
|
+
last[:direction] == :forward ? :backward : :forward
|
|
1450
|
+
else
|
|
1451
|
+
last[:direction]
|
|
1452
|
+
end
|
|
1453
|
+
count = @editor.pending_count
|
|
1454
|
+
@editor.pending_count = nil
|
|
1455
|
+
@pending_keys = []
|
|
1456
|
+
moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
|
|
1457
|
+
@editor.echo("Char not found: #{last[:char]}") unless moved
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
def perform_find_on_line(char:, direction:, till:, count:)
|
|
1461
|
+
win = @editor.current_window
|
|
1462
|
+
buf = @editor.current_buffer
|
|
1463
|
+
line = buf.line_at(win.cursor_y)
|
|
1464
|
+
pos = win.cursor_x
|
|
1465
|
+
target = nil
|
|
1466
|
+
|
|
1467
|
+
[count.to_i, 1].max.times do
|
|
1468
|
+
idx =
|
|
1469
|
+
if direction == :forward
|
|
1470
|
+
line.index(char, pos + 1)
|
|
1471
|
+
else
|
|
1472
|
+
rindex_from(line, char, pos - 1)
|
|
1473
|
+
end
|
|
1474
|
+
return false if idx.nil?
|
|
1475
|
+
|
|
1476
|
+
target = idx
|
|
1477
|
+
pos = idx
|
|
1478
|
+
end
|
|
1479
|
+
|
|
1480
|
+
if till
|
|
1481
|
+
target =
|
|
1482
|
+
if direction == :forward
|
|
1483
|
+
RuVim::TextMetrics.previous_grapheme_char_index(line, target)
|
|
1484
|
+
else
|
|
1485
|
+
RuVim::TextMetrics.next_grapheme_char_index(line, target)
|
|
1486
|
+
end
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
win.cursor_x = target
|
|
1490
|
+
win.clamp_to_buffer(buf)
|
|
1491
|
+
true
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
def rindex_from(line, char, pos)
|
|
1495
|
+
return nil if pos.negative?
|
|
1496
|
+
|
|
1497
|
+
line.rindex(char, pos)
|
|
1498
|
+
end
|
|
1499
|
+
|
|
1500
|
+
def submit_search(line, direction:)
|
|
1501
|
+
inv = CommandInvocation.new(id: "__search_submit__", argv: [line], kwargs: { pattern: line, direction: direction })
|
|
1502
|
+
ctx = Context.new(editor: @editor, invocation: inv)
|
|
1503
|
+
GlobalCommands.instance.submit_search(ctx, pattern: line, direction: direction)
|
|
1504
|
+
@editor.enter_normal_mode
|
|
1505
|
+
rescue StandardError => e
|
|
1506
|
+
@editor.echo_error("Error: #{e.message}")
|
|
1507
|
+
@editor.enter_normal_mode
|
|
1508
|
+
end
|
|
1509
|
+
end
|
|
1510
|
+
end
|