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.
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(wakeup_ios: [@signal_r])
75
- next if key.nil?
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 handle_normal_key(key)
262
- if arrow_key?(key)
263
- invoke_arrow(key)
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
- if paging_key?(key)
268
- invoke_page_key(key)
269
- return
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
- if digit_key?(key) && count_digit_allowed?(key)
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
- return
386
+ else
387
+ return false
277
388
  end
389
+ true
390
+ end
278
391
 
279
- token = normalize_key_token(key)
280
- return if token.nil?
281
-
282
- if @operator_pending
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
- return
285
- end
286
-
287
- if @register_pending
399
+ when @register_pending
288
400
  finish_register_pending(token)
289
- return
290
- end
291
-
292
- if @mark_pending
401
+ when @mark_pending
293
402
  finish_mark_pending(token)
294
- return
295
- end
296
-
297
- if @jump_pending
403
+ when @jump_pending
298
404
  finish_jump_pending(token)
299
- return
300
- end
301
-
302
- if @macro_record_pending
405
+ when @macro_record_pending
303
406
  finish_macro_record_pending(token)
304
- return
305
- end
306
-
307
- if @macro_play_pending
407
+ when @macro_play_pending
308
408
  finish_macro_play_pending(token)
309
- return
310
- end
311
-
312
- if @replace_pending
409
+ when @replace_pending
313
410
  handle_replace_pending_key(token)
314
- return
315
- end
316
-
317
- if @find_pending
411
+ when @find_pending
318
412
  finish_find_pending(token)
319
- return
413
+ else
414
+ return false
320
415
  end
416
+ true
417
+ end
321
418
 
322
- if token == "\""
419
+ def handle_normal_direct_token(token)
420
+ case token
421
+ when "\""
323
422
  start_register_pending
324
- return
325
- end
326
-
327
- if token == "d"
423
+ when "d"
328
424
  start_operator_pending(:delete)
329
- return
330
- end
331
-
332
- if token == "y"
425
+ when "y"
333
426
  start_operator_pending(:yank)
334
- return
335
- end
336
-
337
- if token == "c"
427
+ when "c"
338
428
  start_operator_pending(:change)
339
- return
340
- end
341
-
342
- if token == "r"
429
+ when "r"
343
430
  start_replace_pending
344
- return
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
- return
350
- end
351
-
352
- if token == ";"
433
+ when ";"
353
434
  repeat_last_find(reverse: false)
354
- return
355
- end
356
-
357
- if token == ","
435
+ when ","
358
436
  repeat_last_find(reverse: true)
359
- return
360
- end
361
-
362
- if token == "."
437
+ when "."
363
438
  repeat_last_change
364
- return
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
- return
374
- end
375
-
376
- if token == "@"
445
+ when "@"
377
446
  start_macro_play_pending
378
- return
379
- end
380
-
381
- if token == "m"
447
+ when "m"
382
448
  start_mark_pending
383
- return
384
- end
385
-
386
- if token == "'"
449
+ when "'"
387
450
  start_jump_pending(linewise: true, repeat_token: "'")
388
- return
389
- end
390
-
391
- if token == "`"
451
+ when "`"
392
452
  start_jump_pending(linewise: false, repeat_token: "`")
393
- return
453
+ else
454
+ return false
394
455
  end
456
+ true
457
+ end
395
458
 
396
- @pending_keys ||= []
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
- y, x = @editor.current_buffer.backspace(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
428
- @editor.current_window.cursor_y = y
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
- @editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, "\t")
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
- @editor.current_window.move_left(@editor.current_buffer, 1)
515
+ dispatch_insert_cursor_motion("cursor.left")
446
516
  when :right
447
517
  clear_insert_completion
448
- @editor.current_window.move_right(@editor.current_buffer, 1)
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
- prefix = common_prefix(matches)
1201
- cmd.replace_span(ctx[:token_start], ctx[:token_end], prefix) if prefix.length > ctx[:prefix].length
1202
- @editor.echo(matches.join(" "))
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 = idx.nil? ? (direction.positive? ? 0 : matches.length - 1) : (idx + direction) % matches.length
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
- @editor.echo(matches.length == 1 ? replacement : "#{replacement} (#{idx + 1}/#{matches.length})")
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[/[[:alnum:]_]+\z/]
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(/[[:alnum:]_]+/) do |w|
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|