ruvim 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/binding.md +6 -0
- data/docs/command.md +16 -0
- data/docs/config.md +203 -84
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +39 -22
- data/docs/todo.md +187 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +2 -1
- data/lib/ruvim/app.rb +681 -123
- data/lib/ruvim/config_loader.rb +19 -5
- data/lib/ruvim/context.rb +0 -7
- data/lib/ruvim/dispatcher.rb +10 -0
- data/lib/ruvim/display_width.rb +25 -2
- data/lib/ruvim/editor.rb +173 -4
- data/lib/ruvim/global_commands.rb +500 -55
- data/lib/ruvim/input.rb +22 -10
- data/lib/ruvim/keyword_chars.rb +46 -0
- data/lib/ruvim/screen.rb +388 -53
- data/lib/ruvim/text_metrics.rb +26 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +35 -10
- data/lib/ruvim.rb +1 -0
- data/test/app_completion_test.rb +101 -0
- data/test/app_motion_test.rb +97 -2
- data/test/app_scenario_test.rb +270 -0
- data/test/app_startup_test.rb +10 -0
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +116 -0
- data/test/display_width_test.rb +18 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/input_screen_integration_test.rb +26 -13
- data/test/screen_test.rb +166 -0
- data/test/window_test.rb +26 -0
- metadata +5 -1
data/lib/ruvim/app.rb
CHANGED
|
@@ -10,6 +10,11 @@ module RuVim
|
|
|
10
10
|
@signal_r, @signal_w = IO.pipe
|
|
11
11
|
@cmdline_history = Hash.new { |h, k| h[k] = [] }
|
|
12
12
|
@cmdline_history_index = nil
|
|
13
|
+
@cmdline_completion = nil
|
|
14
|
+
@pending_key_deadline = nil
|
|
15
|
+
@pending_ambiguous_invocation = nil
|
|
16
|
+
@insert_start_location = nil
|
|
17
|
+
@incsearch_preview = nil
|
|
13
18
|
@needs_redraw = true
|
|
14
19
|
@clean_mode = clean
|
|
15
20
|
@skip_user_config = skip_user_config
|
|
@@ -71,8 +76,16 @@ module RuVim
|
|
|
71
76
|
end
|
|
72
77
|
break unless @editor.running?
|
|
73
78
|
|
|
74
|
-
key = @input.read_key(
|
|
75
|
-
|
|
79
|
+
key = @input.read_key(
|
|
80
|
+
wakeup_ios: [@signal_r],
|
|
81
|
+
timeout: loop_timeout_seconds,
|
|
82
|
+
esc_timeout: escape_sequence_timeout_seconds
|
|
83
|
+
)
|
|
84
|
+
if key.nil?
|
|
85
|
+
handle_pending_key_timeout if pending_key_timeout_expired?
|
|
86
|
+
clear_expired_transient_message_if_any
|
|
87
|
+
next
|
|
88
|
+
end
|
|
76
89
|
|
|
77
90
|
handle_key(key)
|
|
78
91
|
@needs_redraw = true
|
|
@@ -89,6 +102,59 @@ module RuVim
|
|
|
89
102
|
|
|
90
103
|
private
|
|
91
104
|
|
|
105
|
+
def pending_key_timeout_seconds
|
|
106
|
+
return nil unless @pending_key_deadline
|
|
107
|
+
|
|
108
|
+
[@pending_key_deadline - monotonic_now, 0.0].max
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def loop_timeout_seconds
|
|
112
|
+
now = monotonic_now
|
|
113
|
+
timeouts = []
|
|
114
|
+
if @pending_key_deadline
|
|
115
|
+
timeouts << [@pending_key_deadline - now, 0.0].max
|
|
116
|
+
end
|
|
117
|
+
if (msg_to = @editor.transient_message_timeout_seconds(now:))
|
|
118
|
+
timeouts << msg_to
|
|
119
|
+
end
|
|
120
|
+
timeouts.min
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def pending_key_timeout_expired?
|
|
124
|
+
@pending_key_deadline && monotonic_now >= @pending_key_deadline
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def escape_sequence_timeout_seconds
|
|
128
|
+
ms = @editor.global_options["ttimeoutlen"].to_i
|
|
129
|
+
ms = 50 if ms <= 0
|
|
130
|
+
ms / 1000.0
|
|
131
|
+
rescue StandardError
|
|
132
|
+
0.005
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def arm_pending_key_timeout
|
|
136
|
+
ms = @editor.global_options["timeoutlen"].to_i
|
|
137
|
+
ms = 1000 if ms <= 0
|
|
138
|
+
@pending_key_deadline = monotonic_now + (ms / 1000.0)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clear_pending_key_timeout
|
|
142
|
+
@pending_key_deadline = nil
|
|
143
|
+
@pending_ambiguous_invocation = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def handle_pending_key_timeout
|
|
147
|
+
inv = @pending_ambiguous_invocation
|
|
148
|
+
clear_pending_key_timeout
|
|
149
|
+
if inv
|
|
150
|
+
@dispatcher.dispatch(@editor, dup_invocation(inv))
|
|
151
|
+
elsif @pending_keys && !@pending_keys.empty?
|
|
152
|
+
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
153
|
+
end
|
|
154
|
+
@editor.pending_count = nil
|
|
155
|
+
@pending_keys = []
|
|
156
|
+
end
|
|
157
|
+
|
|
92
158
|
def register_builtins!
|
|
93
159
|
cmd = CommandRegistry.instance
|
|
94
160
|
ex = ExCommandRegistry.instance
|
|
@@ -99,6 +165,14 @@ module RuVim
|
|
|
99
165
|
register_internal_unless(cmd, "cursor.down", call: :cursor_down, desc: "Move cursor down")
|
|
100
166
|
register_internal_unless(cmd, "cursor.page_up", call: :cursor_page_up, desc: "Move one page up")
|
|
101
167
|
register_internal_unless(cmd, "cursor.page_down", call: :cursor_page_down, desc: "Move one page down")
|
|
168
|
+
register_internal_unless(cmd, "window.scroll_up", call: :window_scroll_up, desc: "Scroll window up")
|
|
169
|
+
register_internal_unless(cmd, "window.scroll_down", call: :window_scroll_down, desc: "Scroll window down")
|
|
170
|
+
register_internal_unless(cmd, "cursor.page_up.default", call: :cursor_page_up_default, desc: "Move one page up (view-sized)")
|
|
171
|
+
register_internal_unless(cmd, "cursor.page_down.default", call: :cursor_page_down_default, desc: "Move one page down (view-sized)")
|
|
172
|
+
register_internal_unless(cmd, "cursor.page_up.half", call: :cursor_page_up_half, desc: "Move half page up")
|
|
173
|
+
register_internal_unless(cmd, "cursor.page_down.half", call: :cursor_page_down_half, desc: "Move half page down")
|
|
174
|
+
register_internal_unless(cmd, "window.scroll_up.line", call: :window_scroll_up_line, desc: "Scroll window up one line")
|
|
175
|
+
register_internal_unless(cmd, "window.scroll_down.line", call: :window_scroll_down_line, desc: "Scroll window down one line")
|
|
102
176
|
register_internal_unless(cmd, "cursor.line_start", call: :cursor_line_start, desc: "Move to column 1")
|
|
103
177
|
register_internal_unless(cmd, "cursor.line_end", call: :cursor_line_end, desc: "Move to end of line")
|
|
104
178
|
register_internal_unless(cmd, "cursor.first_nonblank", call: :cursor_first_nonblank, desc: "Move to first nonblank")
|
|
@@ -153,7 +227,9 @@ module RuVim
|
|
|
153
227
|
register_internal_unless(cmd, "jump.newer", call: :jump_newer, desc: "Jump newer")
|
|
154
228
|
register_internal_unless(cmd, "editor.buffer_next", call: :buffer_next, desc: "Next buffer")
|
|
155
229
|
register_internal_unless(cmd, "editor.buffer_prev", call: :buffer_prev, desc: "Previous buffer")
|
|
230
|
+
register_internal_unless(cmd, "editor.buffer_delete", call: :buffer_delete, desc: "Delete buffer")
|
|
156
231
|
register_internal_unless(cmd, "buffer.replace_char", call: :replace_char, desc: "Replace single char")
|
|
232
|
+
register_internal_unless(cmd, "file.goto_under_cursor", call: :file_goto_under_cursor, desc: "Open file under cursor")
|
|
157
233
|
register_internal_unless(cmd, "ui.clear_message", call: :clear_message, desc: "Clear message")
|
|
158
234
|
|
|
159
235
|
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :maybe_one, bang: true)
|
|
@@ -167,6 +243,7 @@ module RuVim
|
|
|
167
243
|
register_ex_unless(ex, "bnext", call: :buffer_next, aliases: %w[bn], desc: "Next buffer", nargs: 0, bang: true)
|
|
168
244
|
register_ex_unless(ex, "bprev", call: :buffer_prev, aliases: %w[bp], desc: "Previous buffer", nargs: 0, bang: true)
|
|
169
245
|
register_ex_unless(ex, "buffer", call: :buffer_switch, aliases: %w[b], desc: "Switch buffer", nargs: 1, bang: true)
|
|
246
|
+
register_ex_unless(ex, "bdelete", call: :buffer_delete, aliases: %w[bd], desc: "Delete buffer", nargs: :maybe_one, bang: true)
|
|
170
247
|
register_ex_unless(ex, "commands", call: :ex_commands, desc: "List Ex commands", nargs: 0)
|
|
171
248
|
register_ex_unless(ex, "set", call: :ex_set, desc: "Set options", nargs: :any)
|
|
172
249
|
register_ex_unless(ex, "setlocal", call: :ex_setlocal, desc: "Set window/buffer local option", nargs: :any)
|
|
@@ -193,6 +270,10 @@ module RuVim
|
|
|
193
270
|
@keymaps.bind(:normal, "j", "cursor.down")
|
|
194
271
|
@keymaps.bind(:normal, "k", "cursor.up")
|
|
195
272
|
@keymaps.bind(:normal, "l", "cursor.right")
|
|
273
|
+
@keymaps.bind(:normal, ["<Left>"], "cursor.left")
|
|
274
|
+
@keymaps.bind(:normal, ["<Down>"], "cursor.down")
|
|
275
|
+
@keymaps.bind(:normal, ["<Up>"], "cursor.up")
|
|
276
|
+
@keymaps.bind(:normal, ["<Right>"], "cursor.right")
|
|
196
277
|
@keymaps.bind(:normal, "0", "cursor.line_start")
|
|
197
278
|
@keymaps.bind(:normal, "$", "cursor.line_end")
|
|
198
279
|
@keymaps.bind(:normal, "^", "cursor.first_nonblank")
|
|
@@ -226,20 +307,32 @@ module RuVim
|
|
|
226
307
|
@keymaps.bind(:normal, ["<C-r>"], "buffer.redo")
|
|
227
308
|
@keymaps.bind(:normal, ["<C-o>"], "jump.older")
|
|
228
309
|
@keymaps.bind(:normal, ["<C-i>"], "jump.newer")
|
|
310
|
+
@keymaps.bind(:normal, ["<C-d>"], "cursor.page_down.half")
|
|
311
|
+
@keymaps.bind(:normal, ["<C-u>"], "cursor.page_up.half")
|
|
312
|
+
@keymaps.bind(:normal, ["<C-f>"], "cursor.page_down.default")
|
|
313
|
+
@keymaps.bind(:normal, ["<C-b>"], "cursor.page_up.default")
|
|
314
|
+
@keymaps.bind(:normal, ["<C-e>"], "window.scroll_down.line")
|
|
315
|
+
@keymaps.bind(:normal, ["<C-y>"], "window.scroll_up.line")
|
|
229
316
|
@keymaps.bind(:normal, "n", "search.next")
|
|
230
317
|
@keymaps.bind(:normal, "N", "search.prev")
|
|
231
318
|
@keymaps.bind(:normal, "*", "search.word_forward")
|
|
232
319
|
@keymaps.bind(:normal, "#", "search.word_backward")
|
|
233
320
|
@keymaps.bind(:normal, "g*", "search.word_forward_partial")
|
|
234
321
|
@keymaps.bind(:normal, "g#", "search.word_backward_partial")
|
|
322
|
+
@keymaps.bind(:normal, "gf", "file.goto_under_cursor")
|
|
323
|
+
@keymaps.bind(:normal, ["<PageUp>"], "cursor.page_up.default")
|
|
324
|
+
@keymaps.bind(:normal, ["<PageDown>"], "cursor.page_down.default")
|
|
235
325
|
@keymaps.bind(:normal, "\e", "ui.clear_message")
|
|
236
326
|
end
|
|
237
327
|
|
|
238
328
|
def handle_key(key)
|
|
329
|
+
mode_before = @editor.mode
|
|
330
|
+
clear_stale_message_before_key(key)
|
|
239
331
|
@skip_record_for_current_key = false
|
|
240
332
|
append_dot_change_capture_key(key)
|
|
241
333
|
if key == :ctrl_c
|
|
242
334
|
handle_ctrl_c
|
|
335
|
+
track_mode_transition(mode_before)
|
|
243
336
|
record_macro_key_if_needed(key)
|
|
244
337
|
return
|
|
245
338
|
end
|
|
@@ -254,153 +347,130 @@ module RuVim
|
|
|
254
347
|
else
|
|
255
348
|
handle_normal_key(key)
|
|
256
349
|
end
|
|
350
|
+
track_mode_transition(mode_before)
|
|
257
351
|
load_current_ftplugin!
|
|
258
352
|
record_macro_key_if_needed(key)
|
|
259
353
|
end
|
|
260
354
|
|
|
261
|
-
def
|
|
262
|
-
if
|
|
263
|
-
|
|
264
|
-
return
|
|
265
|
-
end
|
|
355
|
+
def clear_stale_message_before_key(key)
|
|
356
|
+
return if @editor.message.to_s.empty?
|
|
357
|
+
return if @editor.command_line_active?
|
|
266
358
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
359
|
+
# Keep the error visible while the user is still dismissing/cancelling;
|
|
360
|
+
# otherwise, the next operation replaces the command-line area naturally.
|
|
361
|
+
return if key == :ctrl_c
|
|
362
|
+
|
|
363
|
+
@editor.clear_message
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def handle_normal_key(key)
|
|
367
|
+
case
|
|
368
|
+
when handle_normal_key_pre_dispatch(key)
|
|
369
|
+
when (token = normalize_key_token(key)).nil?
|
|
370
|
+
when handle_normal_pending_state(token)
|
|
371
|
+
when handle_normal_direct_token(token)
|
|
372
|
+
else
|
|
373
|
+
@pending_keys ||= []
|
|
374
|
+
@pending_keys << token
|
|
375
|
+
resolve_normal_key_sequence
|
|
270
376
|
end
|
|
377
|
+
end
|
|
271
378
|
|
|
272
|
-
|
|
379
|
+
def handle_normal_key_pre_dispatch(key)
|
|
380
|
+
case
|
|
381
|
+
when key == :enter && handle_list_window_enter
|
|
382
|
+
when digit_key?(key) && count_digit_allowed?(key)
|
|
273
383
|
@editor.pending_count = (@editor.pending_count.to_s + key).to_i
|
|
274
384
|
@editor.echo(@editor.pending_count.to_s)
|
|
275
385
|
@pending_keys = []
|
|
276
|
-
|
|
386
|
+
else
|
|
387
|
+
return false
|
|
277
388
|
end
|
|
389
|
+
true
|
|
390
|
+
end
|
|
278
391
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
392
|
+
def handle_normal_pending_state(token)
|
|
393
|
+
case
|
|
394
|
+
when @pending_keys && !@pending_keys.empty?
|
|
395
|
+
@pending_keys << token
|
|
396
|
+
resolve_normal_key_sequence
|
|
397
|
+
when @operator_pending
|
|
283
398
|
handle_operator_pending_key(token)
|
|
284
|
-
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
if @register_pending
|
|
399
|
+
when @register_pending
|
|
288
400
|
finish_register_pending(token)
|
|
289
|
-
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
if @mark_pending
|
|
401
|
+
when @mark_pending
|
|
293
402
|
finish_mark_pending(token)
|
|
294
|
-
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
if @jump_pending
|
|
403
|
+
when @jump_pending
|
|
298
404
|
finish_jump_pending(token)
|
|
299
|
-
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
if @macro_record_pending
|
|
405
|
+
when @macro_record_pending
|
|
303
406
|
finish_macro_record_pending(token)
|
|
304
|
-
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
if @macro_play_pending
|
|
407
|
+
when @macro_play_pending
|
|
308
408
|
finish_macro_play_pending(token)
|
|
309
|
-
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
if @replace_pending
|
|
409
|
+
when @replace_pending
|
|
313
410
|
handle_replace_pending_key(token)
|
|
314
|
-
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
if @find_pending
|
|
411
|
+
when @find_pending
|
|
318
412
|
finish_find_pending(token)
|
|
319
|
-
|
|
413
|
+
else
|
|
414
|
+
return false
|
|
320
415
|
end
|
|
416
|
+
true
|
|
417
|
+
end
|
|
321
418
|
|
|
322
|
-
|
|
419
|
+
def handle_normal_direct_token(token)
|
|
420
|
+
case token
|
|
421
|
+
when "\""
|
|
323
422
|
start_register_pending
|
|
324
|
-
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
if token == "d"
|
|
423
|
+
when "d"
|
|
328
424
|
start_operator_pending(:delete)
|
|
329
|
-
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
if token == "y"
|
|
425
|
+
when "y"
|
|
333
426
|
start_operator_pending(:yank)
|
|
334
|
-
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
if token == "c"
|
|
427
|
+
when "c"
|
|
338
428
|
start_operator_pending(:change)
|
|
339
|
-
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
if token == "r"
|
|
429
|
+
when "r"
|
|
343
430
|
start_replace_pending
|
|
344
|
-
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
if %w[f F t T].include?(token)
|
|
431
|
+
when "f", "F", "t", "T"
|
|
348
432
|
start_find_pending(token)
|
|
349
|
-
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
if token == ";"
|
|
433
|
+
when ";"
|
|
353
434
|
repeat_last_find(reverse: false)
|
|
354
|
-
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
if token == ","
|
|
435
|
+
when ","
|
|
358
436
|
repeat_last_find(reverse: true)
|
|
359
|
-
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
if token == "."
|
|
437
|
+
when "."
|
|
363
438
|
repeat_last_change
|
|
364
|
-
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
if token == "q"
|
|
439
|
+
when "q"
|
|
368
440
|
if @editor.macro_recording?
|
|
369
441
|
stop_macro_recording
|
|
370
442
|
else
|
|
371
443
|
start_macro_record_pending
|
|
372
444
|
end
|
|
373
|
-
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
if token == "@"
|
|
445
|
+
when "@"
|
|
377
446
|
start_macro_play_pending
|
|
378
|
-
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
if token == "m"
|
|
447
|
+
when "m"
|
|
382
448
|
start_mark_pending
|
|
383
|
-
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
if token == "'"
|
|
449
|
+
when "'"
|
|
387
450
|
start_jump_pending(linewise: true, repeat_token: "'")
|
|
388
|
-
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
if token == "`"
|
|
451
|
+
when "`"
|
|
392
452
|
start_jump_pending(linewise: false, repeat_token: "`")
|
|
393
|
-
|
|
453
|
+
else
|
|
454
|
+
return false
|
|
394
455
|
end
|
|
456
|
+
true
|
|
457
|
+
end
|
|
395
458
|
|
|
396
|
-
|
|
397
|
-
@pending_keys << token
|
|
398
|
-
|
|
459
|
+
def resolve_normal_key_sequence
|
|
399
460
|
match = @keymaps.resolve_with_context(:normal, @pending_keys, editor: @editor)
|
|
400
461
|
case match.status
|
|
401
462
|
when :pending, :ambiguous
|
|
463
|
+
if match.status == :ambiguous && match.invocation
|
|
464
|
+
inv = dup_invocation(match.invocation)
|
|
465
|
+
inv.count = @editor.pending_count || 1
|
|
466
|
+
@pending_ambiguous_invocation = inv
|
|
467
|
+
else
|
|
468
|
+
@pending_ambiguous_invocation = nil
|
|
469
|
+
end
|
|
470
|
+
arm_pending_key_timeout
|
|
402
471
|
return
|
|
403
472
|
when :match
|
|
473
|
+
clear_pending_key_timeout
|
|
404
474
|
matched_keys = @pending_keys.dup
|
|
405
475
|
repeat_count = @editor.pending_count || 1
|
|
406
476
|
invocation = dup_invocation(match.invocation)
|
|
@@ -408,6 +478,7 @@ module RuVim
|
|
|
408
478
|
@dispatcher.dispatch(@editor, invocation)
|
|
409
479
|
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
410
480
|
else
|
|
481
|
+
clear_pending_key_timeout
|
|
411
482
|
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
412
483
|
end
|
|
413
484
|
@editor.pending_count = nil
|
|
@@ -424,28 +495,27 @@ module RuVim
|
|
|
424
495
|
@editor.echo("")
|
|
425
496
|
when :backspace
|
|
426
497
|
clear_insert_completion
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
@editor.current_window.cursor_x = x
|
|
498
|
+
return unless insert_backspace_allowed?
|
|
499
|
+
insert_backspace_in_insert_mode
|
|
430
500
|
when :ctrl_n
|
|
431
501
|
insert_complete(+1)
|
|
432
502
|
when :ctrl_p
|
|
433
503
|
insert_complete(-1)
|
|
434
504
|
when :ctrl_i
|
|
435
505
|
clear_insert_completion
|
|
436
|
-
|
|
437
|
-
@editor.current_window.cursor_x += 1
|
|
506
|
+
insert_tab_in_insert_mode
|
|
438
507
|
when :enter
|
|
439
508
|
clear_insert_completion
|
|
440
509
|
y, x = @editor.current_buffer.insert_newline(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
|
|
510
|
+
x = apply_insert_autoindent(y, x, previous_row: y - 1)
|
|
441
511
|
@editor.current_window.cursor_y = y
|
|
442
512
|
@editor.current_window.cursor_x = x
|
|
443
513
|
when :left
|
|
444
514
|
clear_insert_completion
|
|
445
|
-
|
|
515
|
+
dispatch_insert_cursor_motion("cursor.left")
|
|
446
516
|
when :right
|
|
447
517
|
clear_insert_completion
|
|
448
|
-
|
|
518
|
+
dispatch_insert_cursor_motion("cursor.right")
|
|
449
519
|
when :up
|
|
450
520
|
clear_insert_completion
|
|
451
521
|
@editor.current_window.move_up(@editor.current_buffer, 1)
|
|
@@ -461,6 +531,7 @@ module RuVim
|
|
|
461
531
|
clear_insert_completion
|
|
462
532
|
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
463
533
|
@editor.current_window.cursor_x += 1
|
|
534
|
+
maybe_showmatch_after_insert(key)
|
|
464
535
|
end
|
|
465
536
|
end
|
|
466
537
|
|
|
@@ -551,6 +622,7 @@ module RuVim
|
|
|
551
622
|
if token == "g"
|
|
552
623
|
@pending_keys ||= []
|
|
553
624
|
@pending_keys << token
|
|
625
|
+
arm_pending_key_timeout
|
|
554
626
|
return
|
|
555
627
|
end
|
|
556
628
|
|
|
@@ -559,9 +631,11 @@ module RuVim
|
|
|
559
631
|
end
|
|
560
632
|
|
|
561
633
|
if id
|
|
634
|
+
clear_pending_key_timeout
|
|
562
635
|
count = @editor.pending_count || 1
|
|
563
636
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
564
637
|
else
|
|
638
|
+
clear_pending_key_timeout
|
|
565
639
|
@editor.echo_error("Unknown visual key: #{token}")
|
|
566
640
|
end
|
|
567
641
|
ensure
|
|
@@ -572,29 +646,87 @@ module RuVim
|
|
|
572
646
|
cmd = @editor.command_line
|
|
573
647
|
case key
|
|
574
648
|
when :escape
|
|
649
|
+
clear_command_line_completion
|
|
650
|
+
cancel_incsearch_preview_if_any
|
|
575
651
|
@editor.cancel_command_line
|
|
576
652
|
when :enter
|
|
653
|
+
clear_command_line_completion
|
|
577
654
|
line = cmd.text.dup
|
|
578
655
|
push_command_line_history(cmd.prefix, line)
|
|
579
656
|
handle_command_line_submit(cmd.prefix, line)
|
|
580
657
|
when :backspace
|
|
658
|
+
clear_command_line_completion
|
|
659
|
+
if cmd.text.empty? && cmd.cursor.zero?
|
|
660
|
+
cancel_incsearch_preview_if_any
|
|
661
|
+
@editor.cancel_command_line
|
|
662
|
+
return
|
|
663
|
+
end
|
|
581
664
|
cmd.backspace
|
|
582
665
|
when :up
|
|
666
|
+
clear_command_line_completion
|
|
583
667
|
command_line_history_move(-1)
|
|
584
668
|
when :down
|
|
669
|
+
clear_command_line_completion
|
|
585
670
|
command_line_history_move(1)
|
|
586
671
|
when :left
|
|
672
|
+
clear_command_line_completion
|
|
587
673
|
cmd.move_left
|
|
588
674
|
when :right
|
|
675
|
+
clear_command_line_completion
|
|
589
676
|
cmd.move_right
|
|
590
677
|
else
|
|
591
678
|
if key == :ctrl_i
|
|
592
679
|
command_line_complete
|
|
593
680
|
elsif key.is_a?(String)
|
|
681
|
+
clear_command_line_completion
|
|
594
682
|
@cmdline_history_index = nil
|
|
595
683
|
cmd.insert(key)
|
|
596
684
|
end
|
|
597
685
|
end
|
|
686
|
+
update_incsearch_preview_if_needed
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def handle_list_window_enter
|
|
690
|
+
buffer = @editor.current_buffer
|
|
691
|
+
return false unless buffer.kind == :quickfix || buffer.kind == :location_list
|
|
692
|
+
|
|
693
|
+
item_index = @editor.current_window.cursor_y - 2
|
|
694
|
+
if item_index.negative?
|
|
695
|
+
@editor.echo_error("No list item on this line")
|
|
696
|
+
return true
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
source_window_id = buffer.options["ruvim_list_source_window_id"]
|
|
700
|
+
source_window_id = source_window_id.to_i if source_window_id
|
|
701
|
+
source_window_id = nil unless source_window_id && @editor.windows.key?(source_window_id)
|
|
702
|
+
|
|
703
|
+
item =
|
|
704
|
+
if buffer.kind == :quickfix
|
|
705
|
+
@editor.select_quickfix(item_index)
|
|
706
|
+
else
|
|
707
|
+
owner_window_id = source_window_id || @editor.current_window_id
|
|
708
|
+
@editor.select_location_list(item_index, window_id: owner_window_id)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
unless item
|
|
712
|
+
@editor.echo_error("#{buffer.kind == :quickfix ? 'quickfix' : 'location list'} item not found")
|
|
713
|
+
return true
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
if source_window_id
|
|
717
|
+
@editor.current_window_id = source_window_id
|
|
718
|
+
end
|
|
719
|
+
@editor.jump_to_location(item)
|
|
720
|
+
@editor.echo(
|
|
721
|
+
if buffer.kind == :quickfix
|
|
722
|
+
"qf #{@editor.quickfix_index.to_i + 1}/#{@editor.quickfix_items.length}"
|
|
723
|
+
else
|
|
724
|
+
owner_window_id = source_window_id || @editor.current_window_id
|
|
725
|
+
list = @editor.location_list(owner_window_id)
|
|
726
|
+
"ll #{list[:index].to_i + 1}/#{list[:items].length}"
|
|
727
|
+
end
|
|
728
|
+
)
|
|
729
|
+
true
|
|
598
730
|
end
|
|
599
731
|
|
|
600
732
|
def arrow_key?(key)
|
|
@@ -630,13 +762,6 @@ module RuVim
|
|
|
630
762
|
@pending_keys = []
|
|
631
763
|
end
|
|
632
764
|
|
|
633
|
-
def current_page_step_lines
|
|
634
|
-
height = @screen.current_window_view_height(@editor)
|
|
635
|
-
[height - 1, 1].max
|
|
636
|
-
rescue StandardError
|
|
637
|
-
1
|
|
638
|
-
end
|
|
639
|
-
|
|
640
765
|
def digit_key?(key)
|
|
641
766
|
key.is_a?(String) && key.match?(/\A\d\z/)
|
|
642
767
|
end
|
|
@@ -653,10 +778,21 @@ module RuVim
|
|
|
653
778
|
when String then key
|
|
654
779
|
when :escape then "\e"
|
|
655
780
|
when :ctrl_r then "<C-r>"
|
|
781
|
+
when :ctrl_d then "<C-d>"
|
|
782
|
+
when :ctrl_u then "<C-u>"
|
|
783
|
+
when :ctrl_f then "<C-f>"
|
|
784
|
+
when :ctrl_b then "<C-b>"
|
|
785
|
+
when :ctrl_e then "<C-e>"
|
|
786
|
+
when :ctrl_y then "<C-y>"
|
|
656
787
|
when :ctrl_v then "<C-v>"
|
|
657
788
|
when :ctrl_i then "<C-i>"
|
|
658
789
|
when :ctrl_o then "<C-o>"
|
|
659
790
|
when :ctrl_w then "<C-w>"
|
|
791
|
+
when :ctrl_l then "<C-l>"
|
|
792
|
+
when :left then "<Left>"
|
|
793
|
+
when :right then "<Right>"
|
|
794
|
+
when :up then "<Up>"
|
|
795
|
+
when :down then "<Down>"
|
|
660
796
|
when :home then "<Home>"
|
|
661
797
|
when :end then "<End>"
|
|
662
798
|
when :pageup then "<PageUp>"
|
|
@@ -682,17 +818,22 @@ module RuVim
|
|
|
682
818
|
finish_insert_change_group
|
|
683
819
|
finish_dot_change_capture
|
|
684
820
|
clear_insert_completion
|
|
821
|
+
clear_pending_key_timeout
|
|
685
822
|
@editor.enter_normal_mode
|
|
686
823
|
@editor.echo("")
|
|
687
824
|
when :command_line
|
|
825
|
+
clear_pending_key_timeout
|
|
826
|
+
cancel_incsearch_preview_if_any
|
|
688
827
|
@editor.cancel_command_line
|
|
689
828
|
when :visual_char, :visual_line, :visual_block
|
|
690
829
|
@visual_pending = nil
|
|
691
830
|
@register_pending = false
|
|
692
831
|
@mark_pending = false
|
|
693
832
|
@jump_pending = nil
|
|
833
|
+
clear_pending_key_timeout
|
|
694
834
|
@editor.enter_normal_mode
|
|
695
835
|
else
|
|
836
|
+
clear_pending_key_timeout
|
|
696
837
|
@editor.pending_count = nil
|
|
697
838
|
@pending_keys = []
|
|
698
839
|
@operator_pending = nil
|
|
@@ -711,6 +852,7 @@ module RuVim
|
|
|
711
852
|
end
|
|
712
853
|
|
|
713
854
|
def handle_command_line_submit(prefix, line)
|
|
855
|
+
clear_incsearch_preview_state(apply: false) if %w[/ ?].include?(prefix)
|
|
714
856
|
case prefix
|
|
715
857
|
when ":"
|
|
716
858
|
verbose_log(2, "ex: #{line}")
|
|
@@ -1181,6 +1323,7 @@ module RuVim
|
|
|
1181
1323
|
else
|
|
1182
1324
|
cmd.replace_text(hist[@cmdline_history_index])
|
|
1183
1325
|
end
|
|
1326
|
+
update_incsearch_preview_if_needed
|
|
1184
1327
|
end
|
|
1185
1328
|
|
|
1186
1329
|
def command_line_complete
|
|
@@ -1193,14 +1336,96 @@ module RuVim
|
|
|
1193
1336
|
matches = ex_completion_candidates(ctx)
|
|
1194
1337
|
case matches.length
|
|
1195
1338
|
when 0
|
|
1339
|
+
clear_command_line_completion
|
|
1196
1340
|
@editor.echo("No completion")
|
|
1197
1341
|
when 1
|
|
1342
|
+
clear_command_line_completion
|
|
1198
1343
|
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches.first)
|
|
1199
1344
|
else
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1345
|
+
apply_wildmode_completion(cmd, ctx, matches)
|
|
1346
|
+
end
|
|
1347
|
+
update_incsearch_preview_if_needed
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def clear_command_line_completion
|
|
1351
|
+
@cmdline_completion = nil
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
def apply_wildmode_completion(cmd, ctx, matches)
|
|
1355
|
+
mode_steps = wildmode_steps
|
|
1356
|
+
mode_steps = [:full] if mode_steps.empty?
|
|
1357
|
+
state = @cmdline_completion
|
|
1358
|
+
before_text = cmd.text[0...ctx[:token_start]].to_s
|
|
1359
|
+
after_text = cmd.text[ctx[:token_end]..].to_s
|
|
1360
|
+
same = state &&
|
|
1361
|
+
state[:prefix] == cmd.prefix &&
|
|
1362
|
+
state[:kind] == ctx[:kind] &&
|
|
1363
|
+
state[:command] == ctx[:command] &&
|
|
1364
|
+
state[:arg_index] == ctx[:arg_index] &&
|
|
1365
|
+
state[:token_start] == ctx[:token_start] &&
|
|
1366
|
+
state[:before_text] == before_text &&
|
|
1367
|
+
state[:after_text] == after_text &&
|
|
1368
|
+
state[:matches] == matches
|
|
1369
|
+
unless same
|
|
1370
|
+
state = {
|
|
1371
|
+
prefix: cmd.prefix,
|
|
1372
|
+
kind: ctx[:kind],
|
|
1373
|
+
command: ctx[:command],
|
|
1374
|
+
arg_index: ctx[:arg_index],
|
|
1375
|
+
token_start: ctx[:token_start],
|
|
1376
|
+
before_text: before_text,
|
|
1377
|
+
after_text: after_text,
|
|
1378
|
+
matches: matches.dup,
|
|
1379
|
+
step_index: -1,
|
|
1380
|
+
full_index: nil
|
|
1381
|
+
}
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
state[:step_index] += 1
|
|
1385
|
+
step = mode_steps[state[:step_index] % mode_steps.length]
|
|
1386
|
+
case step
|
|
1387
|
+
when :longest
|
|
1388
|
+
pref = common_prefix(matches)
|
|
1389
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
|
|
1390
|
+
when :list
|
|
1391
|
+
show_command_line_completion_menu(matches, selected: state[:full_index], force: true)
|
|
1392
|
+
when :full
|
|
1393
|
+
state[:full_index] = state[:full_index] ? (state[:full_index] + 1) % matches.length : 0
|
|
1394
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches[state[:full_index]])
|
|
1395
|
+
show_command_line_completion_menu(matches, selected: state[:full_index], force: false)
|
|
1396
|
+
else
|
|
1397
|
+
pref = common_prefix(matches)
|
|
1398
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
@cmdline_completion = state
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
def wildmode_steps
|
|
1405
|
+
raw = @editor.effective_option("wildmode").to_s
|
|
1406
|
+
return [:full] if raw.empty?
|
|
1407
|
+
|
|
1408
|
+
raw.split(",").flat_map do |tok|
|
|
1409
|
+
tok.to_s.split(":").map do |part|
|
|
1410
|
+
case part.strip.downcase
|
|
1411
|
+
when "longest" then :longest
|
|
1412
|
+
when "list" then :list
|
|
1413
|
+
when "full" then :full
|
|
1414
|
+
end
|
|
1415
|
+
end
|
|
1416
|
+
end.compact
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
def show_command_line_completion_menu(matches, selected:, force:)
|
|
1420
|
+
return unless force || @editor.effective_option("wildmenu")
|
|
1421
|
+
|
|
1422
|
+
limit = [@editor.effective_option("pumheight").to_i, 1].max
|
|
1423
|
+
items = matches.first(limit).each_with_index.map do |m, i|
|
|
1424
|
+
idx = i
|
|
1425
|
+
idx == selected ? "[#{m}]" : m
|
|
1203
1426
|
end
|
|
1427
|
+
items << "..." if matches.length > limit
|
|
1428
|
+
@editor.echo(items.join(" "))
|
|
1204
1429
|
end
|
|
1205
1430
|
|
|
1206
1431
|
def common_prefix(strings)
|
|
@@ -1219,6 +1444,66 @@ module RuVim
|
|
|
1219
1444
|
@insert_completion = nil
|
|
1220
1445
|
end
|
|
1221
1446
|
|
|
1447
|
+
def insert_tab_in_insert_mode
|
|
1448
|
+
buf = @editor.current_buffer
|
|
1449
|
+
win = @editor.current_window
|
|
1450
|
+
if @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
1451
|
+
width = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
1452
|
+
width = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if width <= 0
|
|
1453
|
+
width = 2 if width <= 0
|
|
1454
|
+
line = buf.line_at(win.cursor_y)
|
|
1455
|
+
current_col = RuVim::TextMetrics.screen_col_for_char_index(line, win.cursor_x, tabstop: effective_tabstop(win, buf))
|
|
1456
|
+
spaces = width - (current_col % width)
|
|
1457
|
+
spaces = width if spaces <= 0
|
|
1458
|
+
_y, x = buf.insert_text(win.cursor_y, win.cursor_x, " " * spaces)
|
|
1459
|
+
win.cursor_x = x
|
|
1460
|
+
else
|
|
1461
|
+
buf.insert_char(win.cursor_y, win.cursor_x, "\t")
|
|
1462
|
+
win.cursor_x += 1
|
|
1463
|
+
end
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
def apply_insert_autoindent(row, x, previous_row:)
|
|
1467
|
+
buf = @editor.current_buffer
|
|
1468
|
+
win = @editor.current_window
|
|
1469
|
+
return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
|
|
1470
|
+
return x if previous_row.negative?
|
|
1471
|
+
|
|
1472
|
+
prev = buf.line_at(previous_row)
|
|
1473
|
+
indent = prev[/\A[ \t]*/].to_s
|
|
1474
|
+
if @editor.effective_option("smartindent", window: win, buffer: buf)
|
|
1475
|
+
trimmed = prev.rstrip
|
|
1476
|
+
if trimmed.end_with?("{", "[", "(")
|
|
1477
|
+
sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
|
|
1478
|
+
sw = effective_tabstop(win, buf) if sw <= 0
|
|
1479
|
+
sw = 2 if sw <= 0
|
|
1480
|
+
indent += " " * sw
|
|
1481
|
+
end
|
|
1482
|
+
end
|
|
1483
|
+
return x if indent.empty?
|
|
1484
|
+
|
|
1485
|
+
_y, new_x = buf.insert_text(row, x, indent)
|
|
1486
|
+
new_x
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
def maybe_showmatch_after_insert(key)
|
|
1490
|
+
return unless [")", "]", "}"].include?(key)
|
|
1491
|
+
return unless @editor.effective_option("showmatch")
|
|
1492
|
+
|
|
1493
|
+
mt = @editor.effective_option("matchtime").to_i
|
|
1494
|
+
mt = 5 if mt <= 0
|
|
1495
|
+
@editor.echo_temporary("match", duration_seconds: mt * 0.1)
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
def clear_expired_transient_message_if_any
|
|
1499
|
+
@needs_redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
def effective_tabstop(window = @editor.current_window, buffer = @editor.current_buffer)
|
|
1503
|
+
v = @editor.effective_option("tabstop", window:, buffer:).to_i
|
|
1504
|
+
v.positive? ? v : 2
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1222
1507
|
def insert_complete(direction)
|
|
1223
1508
|
state = ensure_insert_completion_state
|
|
1224
1509
|
return unless state
|
|
@@ -1229,8 +1514,27 @@ module RuVim
|
|
|
1229
1514
|
return
|
|
1230
1515
|
end
|
|
1231
1516
|
|
|
1517
|
+
if state[:index].nil? && insert_completion_noselect? && matches.length > 1
|
|
1518
|
+
show_insert_completion_menu(matches, selected: nil)
|
|
1519
|
+
state[:index] = :pending_select
|
|
1520
|
+
return
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
if state[:index].nil? && insert_completion_noinsert?
|
|
1524
|
+
preview_idx = direction.positive? ? 0 : matches.length - 1
|
|
1525
|
+
state[:index] = :pending_insert
|
|
1526
|
+
state[:pending_index] = preview_idx
|
|
1527
|
+
show_insert_completion_menu(matches, selected: preview_idx, current: matches[preview_idx])
|
|
1528
|
+
return
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1232
1531
|
idx = state[:index]
|
|
1233
|
-
idx =
|
|
1532
|
+
idx = nil if idx == :pending_select
|
|
1533
|
+
if idx == :pending_insert
|
|
1534
|
+
idx = state.delete(:pending_index) || (direction.positive? ? 0 : matches.length - 1)
|
|
1535
|
+
else
|
|
1536
|
+
idx = idx.nil? ? (direction.positive? ? 0 : matches.length - 1) : (idx + direction) % matches.length
|
|
1537
|
+
end
|
|
1234
1538
|
replacement = matches[idx]
|
|
1235
1539
|
|
|
1236
1540
|
end_col = state[:current_end_col]
|
|
@@ -1241,17 +1545,51 @@ module RuVim
|
|
|
1241
1545
|
@editor.current_window.cursor_x = new_x
|
|
1242
1546
|
state[:index] = idx
|
|
1243
1547
|
state[:current_end_col] = start_col + replacement.length
|
|
1244
|
-
|
|
1548
|
+
if matches.length == 1
|
|
1549
|
+
@editor.echo(replacement)
|
|
1550
|
+
else
|
|
1551
|
+
show_insert_completion_menu(matches, selected: idx, current: replacement)
|
|
1552
|
+
end
|
|
1245
1553
|
rescue StandardError => e
|
|
1246
1554
|
@editor.echo_error("Completion error: #{e.message}")
|
|
1247
1555
|
clear_insert_completion
|
|
1248
1556
|
end
|
|
1249
1557
|
|
|
1558
|
+
def insert_completion_noselect?
|
|
1559
|
+
@editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noselect")
|
|
1560
|
+
end
|
|
1561
|
+
|
|
1562
|
+
def insert_completion_noinsert?
|
|
1563
|
+
@editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noinsert")
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
def insert_completion_menu_enabled?
|
|
1567
|
+
opts = @editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }
|
|
1568
|
+
opts.include?("menu") || opts.include?("menuone")
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def show_insert_completion_menu(matches, selected:, current: nil)
|
|
1572
|
+
if insert_completion_menu_enabled?
|
|
1573
|
+
limit = [@editor.effective_option("pumheight").to_i, 1].max
|
|
1574
|
+
items = matches.first(limit).each_with_index.map do |m, i|
|
|
1575
|
+
i == selected ? "[#{m}]" : m
|
|
1576
|
+
end
|
|
1577
|
+
items << "..." if matches.length > limit
|
|
1578
|
+
if current
|
|
1579
|
+
@editor.echo("#{current} (#{selected + 1}/#{matches.length}) | #{items.join(' ')}")
|
|
1580
|
+
else
|
|
1581
|
+
@editor.echo(items.join(" "))
|
|
1582
|
+
end
|
|
1583
|
+
elsif current
|
|
1584
|
+
@editor.echo("#{current} (#{selected + 1}/#{matches.length})")
|
|
1585
|
+
end
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1250
1588
|
def ensure_insert_completion_state
|
|
1251
1589
|
row = @editor.current_window.cursor_y
|
|
1252
1590
|
col = @editor.current_window.cursor_x
|
|
1253
1591
|
line = @editor.current_buffer.line_at(row)
|
|
1254
|
-
prefix = line[0...col].to_s
|
|
1592
|
+
prefix = trailing_keyword_fragment(line[0...col].to_s, @editor.current_window, @editor.current_buffer)
|
|
1255
1593
|
return nil if prefix.nil? || prefix.empty?
|
|
1256
1594
|
|
|
1257
1595
|
start_col = col - prefix.length
|
|
@@ -1280,9 +1618,10 @@ module RuVim
|
|
|
1280
1618
|
def collect_buffer_word_completions(prefix, current_word:)
|
|
1281
1619
|
words = []
|
|
1282
1620
|
seen = {}
|
|
1621
|
+
rx = keyword_scan_regex(@editor.current_window, @editor.current_buffer)
|
|
1283
1622
|
@editor.buffers.values.each do |buf|
|
|
1284
1623
|
buf.lines.each do |line|
|
|
1285
|
-
line.scan(
|
|
1624
|
+
line.scan(rx) do |w|
|
|
1286
1625
|
next unless w.start_with?(prefix)
|
|
1287
1626
|
next if w == current_word
|
|
1288
1627
|
next if seen[w]
|
|
@@ -1295,6 +1634,210 @@ module RuVim
|
|
|
1295
1634
|
words.sort
|
|
1296
1635
|
end
|
|
1297
1636
|
|
|
1637
|
+
def track_mode_transition(mode_before)
|
|
1638
|
+
mode_after = @editor.mode
|
|
1639
|
+
if mode_before != :insert && mode_after == :insert
|
|
1640
|
+
@insert_start_location = @editor.current_location
|
|
1641
|
+
elsif mode_before == :insert && mode_after != :insert
|
|
1642
|
+
@insert_start_location = nil
|
|
1643
|
+
end
|
|
1644
|
+
|
|
1645
|
+
if mode_before != :command_line && mode_after == :command_line
|
|
1646
|
+
@incsearch_preview = nil
|
|
1647
|
+
elsif mode_before == :command_line && mode_after != :command_line
|
|
1648
|
+
@incsearch_preview = nil
|
|
1649
|
+
end
|
|
1650
|
+
end
|
|
1651
|
+
|
|
1652
|
+
def insert_backspace_allowed?
|
|
1653
|
+
buf = @editor.current_buffer
|
|
1654
|
+
win = @editor.current_window
|
|
1655
|
+
row = win.cursor_y
|
|
1656
|
+
col = win.cursor_x
|
|
1657
|
+
return false if row.zero? && col.zero?
|
|
1658
|
+
|
|
1659
|
+
opt = @editor.effective_option("backspace", window: win, buffer: buf).to_s
|
|
1660
|
+
allow = opt.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
1661
|
+
allow_all = allow.include?("2")
|
|
1662
|
+
allow_indent = allow_all || allow.include?("indent")
|
|
1663
|
+
|
|
1664
|
+
if col.zero? && row.positive?
|
|
1665
|
+
return true if allow_all || allow.include?("eol")
|
|
1666
|
+
|
|
1667
|
+
@editor.echo_error("backspace=eol required")
|
|
1668
|
+
return false
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1671
|
+
if @insert_start_location
|
|
1672
|
+
same_buf = @insert_start_location[:buffer_id] == buf.id
|
|
1673
|
+
if same_buf && (row < @insert_start_location[:row] || (row == @insert_start_location[:row] && col <= @insert_start_location[:col]))
|
|
1674
|
+
if allow_all || allow.include?("start")
|
|
1675
|
+
return true
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
if allow_indent && same_row_autoindent_backspace?(buf, row, col)
|
|
1679
|
+
return true
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
@editor.echo_error("backspace=start required")
|
|
1683
|
+
return false
|
|
1684
|
+
end
|
|
1685
|
+
end
|
|
1686
|
+
|
|
1687
|
+
true
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1690
|
+
def insert_backspace_in_insert_mode
|
|
1691
|
+
buf = @editor.current_buffer
|
|
1692
|
+
win = @editor.current_window
|
|
1693
|
+
row = win.cursor_y
|
|
1694
|
+
col = win.cursor_x
|
|
1695
|
+
|
|
1696
|
+
if row >= 0 && col.positive? && try_softtabstop_backspace(buf, win)
|
|
1697
|
+
return
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
y, x = buf.backspace(row, col)
|
|
1701
|
+
win.cursor_y = y
|
|
1702
|
+
win.cursor_x = x
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
def dispatch_insert_cursor_motion(id)
|
|
1706
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: id, count: 1))
|
|
1707
|
+
rescue StandardError => e
|
|
1708
|
+
@editor.echo_error("Motion error: #{e.message}")
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
def try_softtabstop_backspace(buf, win)
|
|
1712
|
+
row = win.cursor_y
|
|
1713
|
+
col = win.cursor_x
|
|
1714
|
+
line = buf.line_at(row)
|
|
1715
|
+
return false unless line
|
|
1716
|
+
return false unless @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
1717
|
+
|
|
1718
|
+
sts = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
1719
|
+
sts = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if sts <= 0
|
|
1720
|
+
return false if sts <= 0
|
|
1721
|
+
|
|
1722
|
+
prefix = line[0...col].to_s
|
|
1723
|
+
m = prefix.match(/ +\z/)
|
|
1724
|
+
return false unless m
|
|
1725
|
+
|
|
1726
|
+
run = m[0].length
|
|
1727
|
+
return false if run <= 1
|
|
1728
|
+
|
|
1729
|
+
tabstop = effective_tabstop(win, buf)
|
|
1730
|
+
cur_screen = RuVim::TextMetrics.screen_col_for_char_index(line, col, tabstop:)
|
|
1731
|
+
target_screen = [cur_screen - sts, 0].max
|
|
1732
|
+
target_col = RuVim::TextMetrics.char_index_for_screen_col(line, target_screen, tabstop:, align: :floor)
|
|
1733
|
+
delete_cols = col - target_col
|
|
1734
|
+
delete_cols = [delete_cols, run, sts].min
|
|
1735
|
+
return false if delete_cols <= 1
|
|
1736
|
+
|
|
1737
|
+
# Only collapse whitespace run; if target lands before the run, clamp to run start.
|
|
1738
|
+
run_start = col - run
|
|
1739
|
+
target_col = [target_col, run_start].max
|
|
1740
|
+
delete_cols = col - target_col
|
|
1741
|
+
return false if delete_cols <= 1
|
|
1742
|
+
|
|
1743
|
+
buf.delete_span(row, target_col, row, col)
|
|
1744
|
+
win.cursor_x = target_col
|
|
1745
|
+
true
|
|
1746
|
+
rescue StandardError
|
|
1747
|
+
false
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
def same_row_autoindent_backspace?(buf, row, col)
|
|
1751
|
+
return false unless @insert_start_location
|
|
1752
|
+
return false unless row == @insert_start_location[:row]
|
|
1753
|
+
return false unless col <= @insert_start_location[:col]
|
|
1754
|
+
|
|
1755
|
+
line = buf.line_at(row)
|
|
1756
|
+
line[0...@insert_start_location[:col]].to_s.match?(/\A[ \t]*\z/)
|
|
1757
|
+
rescue StandardError
|
|
1758
|
+
false
|
|
1759
|
+
end
|
|
1760
|
+
|
|
1761
|
+
def incsearch_enabled?
|
|
1762
|
+
return false unless @editor.command_line_active?
|
|
1763
|
+
return false unless ["/", "?"].include?(@editor.command_line.prefix)
|
|
1764
|
+
|
|
1765
|
+
!!@editor.effective_option("incsearch")
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
def update_incsearch_preview_if_needed
|
|
1769
|
+
return unless incsearch_enabled?
|
|
1770
|
+
|
|
1771
|
+
cmd = @editor.command_line
|
|
1772
|
+
ensure_incsearch_preview_origin!(direction: (cmd.prefix == "/" ? :forward : :backward))
|
|
1773
|
+
pattern = cmd.text.to_s
|
|
1774
|
+
if pattern.empty?
|
|
1775
|
+
clear_incsearch_preview_state(apply: false)
|
|
1776
|
+
return
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
buf = @editor.current_buffer
|
|
1780
|
+
win = @editor.current_window
|
|
1781
|
+
origin = @incsearch_preview[:origin]
|
|
1782
|
+
tmp_window = RuVim::Window.new(id: -1, buffer_id: buf.id)
|
|
1783
|
+
tmp_window.cursor_y = origin[:row]
|
|
1784
|
+
tmp_window.cursor_x = origin[:col]
|
|
1785
|
+
regex = GlobalCommands.instance.send(:compile_search_regex, pattern, editor: @editor, window: win, buffer: buf)
|
|
1786
|
+
match = GlobalCommands.instance.send(:find_next_match, buf, tmp_window, regex, direction: @incsearch_preview[:direction])
|
|
1787
|
+
if match
|
|
1788
|
+
win.cursor_y = match[:row]
|
|
1789
|
+
win.cursor_x = match[:col]
|
|
1790
|
+
win.clamp_to_buffer(buf)
|
|
1791
|
+
end
|
|
1792
|
+
@incsearch_preview[:active] = true
|
|
1793
|
+
rescue RuVim::CommandError, RegexpError
|
|
1794
|
+
# Keep editing command-line without forcing an error flash on every keystroke.
|
|
1795
|
+
end
|
|
1796
|
+
|
|
1797
|
+
def ensure_incsearch_preview_origin!(direction:)
|
|
1798
|
+
return if @incsearch_preview
|
|
1799
|
+
|
|
1800
|
+
@incsearch_preview = {
|
|
1801
|
+
origin: @editor.current_location,
|
|
1802
|
+
direction: direction,
|
|
1803
|
+
active: false
|
|
1804
|
+
}
|
|
1805
|
+
end
|
|
1806
|
+
|
|
1807
|
+
def cancel_incsearch_preview_if_any
|
|
1808
|
+
clear_incsearch_preview_state(apply: false)
|
|
1809
|
+
end
|
|
1810
|
+
|
|
1811
|
+
def clear_incsearch_preview_state(apply:)
|
|
1812
|
+
return unless @incsearch_preview
|
|
1813
|
+
|
|
1814
|
+
if !apply && @incsearch_preview[:origin]
|
|
1815
|
+
@editor.jump_to_location(@incsearch_preview[:origin])
|
|
1816
|
+
end
|
|
1817
|
+
@incsearch_preview = nil
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
def trailing_keyword_fragment(prefix_text, window, buffer)
|
|
1821
|
+
cls = keyword_char_class(window, buffer)
|
|
1822
|
+
prefix_text.to_s[/[#{cls}]+\z/]
|
|
1823
|
+
rescue RegexpError
|
|
1824
|
+
prefix_text.to_s[/[[:alnum:]_]+\z/]
|
|
1825
|
+
end
|
|
1826
|
+
|
|
1827
|
+
def keyword_scan_regex(window, buffer)
|
|
1828
|
+
cls = keyword_char_class(window, buffer)
|
|
1829
|
+
/[#{cls}]+/
|
|
1830
|
+
rescue RegexpError
|
|
1831
|
+
/[[:alnum:]_]+/
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1834
|
+
def keyword_char_class(window, buffer)
|
|
1835
|
+
raw = @editor.effective_option("iskeyword", window:, buffer:).to_s
|
|
1836
|
+
RuVim::KeywordChars.char_class(raw)
|
|
1837
|
+
rescue StandardError
|
|
1838
|
+
"[:alnum:]_"
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1298
1841
|
def ex_completion_context(cmd)
|
|
1299
1842
|
text = cmd.text
|
|
1300
1843
|
cursor = cmd.cursor
|
|
@@ -1369,12 +1912,27 @@ module RuVim
|
|
|
1369
1912
|
Dir.glob(pattern, File::FNM_DOTMATCH).sort.filter_map do |p|
|
|
1370
1913
|
next if [".", ".."].include?(File.basename(p))
|
|
1371
1914
|
next unless p.start_with?(input) || input.empty?
|
|
1915
|
+
next if wildignore_path?(p)
|
|
1372
1916
|
File.directory?(p) ? "#{p}/" : p
|
|
1373
1917
|
end
|
|
1374
1918
|
rescue StandardError
|
|
1375
1919
|
[]
|
|
1376
1920
|
end
|
|
1377
1921
|
|
|
1922
|
+
def wildignore_path?(path)
|
|
1923
|
+
spec = @editor.global_options["wildignore"].to_s
|
|
1924
|
+
return false if spec.empty?
|
|
1925
|
+
|
|
1926
|
+
flags = @editor.global_options["wildignorecase"] ? File::FNM_CASEFOLD : 0
|
|
1927
|
+
name = path.to_s
|
|
1928
|
+
base = File.basename(name)
|
|
1929
|
+
spec.split(",").map(&:strip).reject(&:empty?).any? do |pat|
|
|
1930
|
+
File.fnmatch?(pat, name, flags) || File.fnmatch?(pat, base, flags)
|
|
1931
|
+
end
|
|
1932
|
+
rescue StandardError
|
|
1933
|
+
false
|
|
1934
|
+
end
|
|
1935
|
+
|
|
1378
1936
|
def buffer_completion_candidates(prefix)
|
|
1379
1937
|
pfx = prefix.to_s
|
|
1380
1938
|
items = @editor.buffers.values.flat_map do |b|
|