ruvim 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +53 -4
  3. data/README.md +15 -6
  4. data/Rakefile +7 -0
  5. data/benchmark/cext_compare.rb +165 -0
  6. data/benchmark/chunked_load.rb +256 -0
  7. data/benchmark/file_load.rb +140 -0
  8. data/benchmark/hotspots.rb +178 -0
  9. data/docs/binding.md +3 -2
  10. data/docs/command.md +81 -9
  11. data/docs/done.md +23 -0
  12. data/docs/spec.md +105 -19
  13. data/docs/todo.md +9 -0
  14. data/docs/tutorial.md +9 -1
  15. data/docs/vim_diff.md +13 -0
  16. data/ext/ruvim/extconf.rb +5 -0
  17. data/ext/ruvim/ruvim_ext.c +519 -0
  18. data/lib/ruvim/app.rb +217 -2778
  19. data/lib/ruvim/browser.rb +104 -0
  20. data/lib/ruvim/buffer.rb +39 -28
  21. data/lib/ruvim/command_invocation.rb +2 -2
  22. data/lib/ruvim/completion_manager.rb +708 -0
  23. data/lib/ruvim/dispatcher.rb +14 -8
  24. data/lib/ruvim/display_width.rb +91 -45
  25. data/lib/ruvim/editor.rb +64 -81
  26. data/lib/ruvim/ex_command_registry.rb +3 -1
  27. data/lib/ruvim/gh/link.rb +207 -0
  28. data/lib/ruvim/git/blame.rb +16 -6
  29. data/lib/ruvim/git/branch.rb +20 -5
  30. data/lib/ruvim/git/grep.rb +107 -0
  31. data/lib/ruvim/git/handler.rb +42 -1
  32. data/lib/ruvim/global_commands.rb +175 -35
  33. data/lib/ruvim/highlighter.rb +4 -13
  34. data/lib/ruvim/key_handler.rb +1510 -0
  35. data/lib/ruvim/keymap_manager.rb +7 -7
  36. data/lib/ruvim/lang/base.rb +5 -0
  37. data/lib/ruvim/lang/c.rb +116 -0
  38. data/lib/ruvim/lang/cpp.rb +107 -0
  39. data/lib/ruvim/lang/csv.rb +4 -1
  40. data/lib/ruvim/lang/diff.rb +2 -0
  41. data/lib/ruvim/lang/dockerfile.rb +36 -0
  42. data/lib/ruvim/lang/elixir.rb +85 -0
  43. data/lib/ruvim/lang/erb.rb +30 -0
  44. data/lib/ruvim/lang/go.rb +83 -0
  45. data/lib/ruvim/lang/html.rb +34 -0
  46. data/lib/ruvim/lang/javascript.rb +83 -0
  47. data/lib/ruvim/lang/json.rb +6 -0
  48. data/lib/ruvim/lang/lua.rb +76 -0
  49. data/lib/ruvim/lang/makefile.rb +36 -0
  50. data/lib/ruvim/lang/markdown.rb +3 -4
  51. data/lib/ruvim/lang/ocaml.rb +77 -0
  52. data/lib/ruvim/lang/perl.rb +91 -0
  53. data/lib/ruvim/lang/python.rb +85 -0
  54. data/lib/ruvim/lang/registry.rb +102 -0
  55. data/lib/ruvim/lang/ruby.rb +7 -0
  56. data/lib/ruvim/lang/rust.rb +95 -0
  57. data/lib/ruvim/lang/scheme.rb +5 -0
  58. data/lib/ruvim/lang/sh.rb +76 -0
  59. data/lib/ruvim/lang/sql.rb +52 -0
  60. data/lib/ruvim/lang/toml.rb +36 -0
  61. data/lib/ruvim/lang/tsv.rb +4 -1
  62. data/lib/ruvim/lang/typescript.rb +53 -0
  63. data/lib/ruvim/lang/yaml.rb +62 -0
  64. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  65. data/lib/ruvim/rich_view.rb +14 -7
  66. data/lib/ruvim/screen.rb +126 -72
  67. data/lib/ruvim/stream/file_load.rb +85 -0
  68. data/lib/ruvim/stream/follow.rb +40 -0
  69. data/lib/ruvim/stream/git.rb +43 -0
  70. data/lib/ruvim/stream/run.rb +74 -0
  71. data/lib/ruvim/stream/stdin.rb +55 -0
  72. data/lib/ruvim/stream.rb +35 -0
  73. data/lib/ruvim/stream_mixer.rb +394 -0
  74. data/lib/ruvim/terminal.rb +18 -4
  75. data/lib/ruvim/text_metrics.rb +84 -65
  76. data/lib/ruvim/version.rb +1 -1
  77. data/lib/ruvim/window.rb +5 -5
  78. data/lib/ruvim.rb +23 -6
  79. data/test/app_command_test.rb +382 -0
  80. data/test/app_completion_test.rb +43 -19
  81. data/test/app_dot_repeat_test.rb +27 -3
  82. data/test/app_ex_command_test.rb +154 -0
  83. data/test/app_motion_test.rb +13 -12
  84. data/test/app_register_test.rb +2 -1
  85. data/test/app_scenario_test.rb +15 -10
  86. data/test/app_startup_test.rb +70 -27
  87. data/test/app_text_object_test.rb +2 -1
  88. data/test/app_unicode_behavior_test.rb +3 -2
  89. data/test/browser_test.rb +88 -0
  90. data/test/buffer_test.rb +24 -0
  91. data/test/cli_test.rb +63 -0
  92. data/test/command_invocation_test.rb +33 -0
  93. data/test/config_dsl_test.rb +47 -0
  94. data/test/dispatcher_test.rb +74 -4
  95. data/test/ex_command_registry_test.rb +106 -0
  96. data/test/follow_test.rb +20 -21
  97. data/test/gh_link_test.rb +141 -0
  98. data/test/git_blame_test.rb +96 -17
  99. data/test/git_grep_test.rb +64 -0
  100. data/test/highlighter_test.rb +125 -0
  101. data/test/indent_test.rb +137 -0
  102. data/test/input_screen_integration_test.rb +1 -1
  103. data/test/keyword_chars_test.rb +85 -0
  104. data/test/lang_test.rb +634 -0
  105. data/test/markdown_renderer_test.rb +5 -5
  106. data/test/on_save_hook_test.rb +12 -8
  107. data/test/render_snapshot_test.rb +78 -0
  108. data/test/rich_view_test.rb +42 -42
  109. data/test/run_command_test.rb +307 -0
  110. data/test/screen_test.rb +68 -5
  111. data/test/stream_test.rb +165 -0
  112. data/test/window_test.rb +59 -0
  113. metadata +52 -2
data/lib/ruvim/app.rb CHANGED
@@ -3,19 +3,22 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require_relative "file_watcher"
6
+ require_relative "stream_mixer"
7
+ require_relative "completion_manager"
8
+ require_relative "key_handler"
6
9
 
7
10
  module RuVim
8
11
  class App
9
- LARGE_FILE_ASYNC_THRESHOLD_BYTES = 64 * 1024 * 1024
10
- LARGE_FILE_STAGED_PREFIX_BYTES = 8 * 1024 * 1024
11
- ASYNC_FILE_READ_CHUNK_BYTES = 1 * 1024 * 1024
12
- ASYNC_FILE_EVENT_FLUSH_BYTES = 4 * 1024 * 1024
13
-
12
+ StartupState = Struct.new(
13
+ :readonly, :diff_mode, :quickfix_errorfile, :session_file,
14
+ :nomodifiable, :follow, :time_path, :time_origin, :timeline,
15
+ :open_layout, :open_count, :skip_user_config, :config_path,
16
+ keyword_init: true
17
+ )
14
18
  def initialize(path: nil, paths: nil, stdin: STDIN, ui_stdin: nil, stdin_stream_mode: false, stdout: STDOUT, pre_config_actions: [], startup_actions: [], clean: false, skip_user_config: false, config_path: nil, readonly: false, diff_mode: false, quickfix_errorfile: nil, session_file: nil, nomodifiable: false, follow: false, restricted: false, verbose_level: 0, verbose_io: STDERR, startup_time_path: nil, startup_open_layout: nil, startup_open_count: nil)
15
19
  startup_paths = Array(paths || path).compact
16
20
  @ui_stdin = ui_stdin || stdin
17
21
  @stdin_stream_mode = !!stdin_stream_mode
18
- @stdin_stream_source = @stdin_stream_mode ? stdin : nil
19
22
  @editor = Editor.new
20
23
  @terminal = Terminal.new(stdin: @ui_stdin, stdout:)
21
24
  @input = Input.new(@ui_stdin)
@@ -23,45 +26,57 @@ module RuVim
23
26
  @dispatcher = Dispatcher.new
24
27
  @keymaps = KeymapManager.new
25
28
  @signal_r, @signal_w = IO.pipe
26
- @stream_event_queue = nil
27
- @stream_reader_thread = nil
28
- @stream_buffer_id = nil
29
- @stream_stop_requested = false
30
- @async_file_loads = {}
31
- @follow_watchers = {}
32
- @cmdline_history = Hash.new { |h, k| h[k] = [] }
33
- @cmdline_history_index = nil
34
- @cmdline_completion = nil
35
- @pending_key_deadline = nil
36
- @pending_ambiguous_invocation = nil
37
- @insert_start_location = nil
38
- @incsearch_preview = nil
39
29
  @needs_redraw = true
40
30
  @clean_mode = clean
41
- @skip_user_config = skip_user_config
42
- @config_path = config_path
43
- @startup_readonly = readonly
44
- @startup_diff_mode = diff_mode
45
- @startup_quickfix_errorfile = quickfix_errorfile
46
- @startup_session_file = session_file
47
- @startup_nomodifiable = nomodifiable
48
- @startup_follow = follow
31
+ @startup = StartupState.new(
32
+ skip_user_config: skip_user_config,
33
+ config_path: config_path,
34
+ readonly: readonly,
35
+ diff_mode: diff_mode,
36
+ quickfix_errorfile: quickfix_errorfile,
37
+ session_file: session_file,
38
+ nomodifiable: nomodifiable,
39
+ follow: follow,
40
+ time_path: startup_time_path,
41
+ time_origin: monotonic_now,
42
+ timeline: [],
43
+ open_layout: startup_open_layout,
44
+ open_count: startup_open_count
45
+ )
49
46
  @restricted_mode = restricted
50
47
  @verbose_level = verbose_level.to_i
51
48
  @verbose_io = verbose_io
52
- @startup_time_path = startup_time_path
53
- @startup_time_origin = monotonic_now
54
- @startup_timeline = []
55
- @startup_open_layout = startup_open_layout
56
- @startup_open_count = startup_open_count
49
+
50
+ @stream_mixer = StreamMixer.new(editor: @editor, signal_w: @signal_w)
51
+ @completion = CompletionManager.new(
52
+ editor: @editor,
53
+ terminal: @terminal,
54
+ verbose_logger: method(:verbose_log)
55
+ )
56
+ @key_handler = KeyHandler.new(
57
+ editor: @editor,
58
+ dispatcher: @dispatcher,
59
+ keymaps: @keymaps,
60
+ terminal: @terminal,
61
+ screen: @screen,
62
+ completion: @completion,
63
+ stream_mixer: @stream_mixer
64
+ )
65
+
57
66
  @editor.restricted_mode = @restricted_mode
58
- @editor.stdin_stream_stop_handler = method(:stdin_stream_stop_command)
59
- @editor.open_path_handler = method(:open_path_with_large_file_support)
67
+ @editor.open_path_handler = @stream_mixer.method(:open_path_with_large_file_support)
60
68
  @editor.keymap_manager = @keymaps
61
- @editor.app_action_handler = method(:handle_editor_app_action)
62
- @editor.git_stream_handler = method(:start_git_stream_command)
63
- @editor.git_stream_stop_handler = method(:stop_git_stream!)
64
- load_command_line_history!
69
+ @editor.app_action_handler = @key_handler.method(:handle_editor_app_action)
70
+ @editor.git_stream_handler = @stream_mixer.method(:start_git_stream_command)
71
+ @editor.git_stream_stop_handler = @stream_mixer.method(:stop_git_stream!)
72
+ @editor.run_stream_handler = @stream_mixer.method(:start_command_stream!)
73
+ @editor.shell_executor = ->(command) {
74
+ result = @terminal.suspend_for_shell(command)
75
+ @screen.invalidate_cache!
76
+ result
77
+ }
78
+
79
+ @completion.load_history!
65
80
 
66
81
  startup_mark("init.start")
67
82
  register_builtins!
@@ -79,12 +94,12 @@ module RuVim
79
94
 
80
95
  if @stdin_stream_mode && startup_paths.empty?
81
96
  verbose_log(1, "startup: stdin stream buffer")
82
- prepare_stdin_stream_buffer!
97
+ @stdin_stream_buf = @stream_mixer.prepare_stdin_stream_buffer!(stdin)
83
98
  elsif startup_paths.empty?
84
99
  verbose_log(1, "startup: intro")
85
100
  @editor.show_intro_buffer_if_applicable!
86
101
  else
87
- verbose_log(1, "startup: open_paths #{startup_paths.inspect} layout=#{@startup_open_layout || :single}")
102
+ verbose_log(1, "startup: open_paths #{startup_paths.inspect} layout=#{@startup.open_layout || :single}")
88
103
  open_startup_paths!(startup_paths)
89
104
  end
90
105
  startup_mark("buffers.opened")
@@ -95,14 +110,15 @@ module RuVim
95
110
  verbose_log(1, "startup: run_startup_actions count=#{Array(startup_actions).length}")
96
111
  run_startup_actions!(startup_actions)
97
112
  startup_mark("startup_actions.done")
98
- start_stdin_stream_reader! if @stream_buffer_id
113
+ @stream_mixer.start_pending_stdin! if @stdin_stream_buf
99
114
  write_startuptime_log!
115
+ @startup = nil
100
116
  end
101
117
 
102
118
  def run
103
119
  @terminal.with_ui do
104
120
  loop do
105
- @needs_redraw = true if drain_stream_events!
121
+ @needs_redraw = true if @stream_mixer.drain_events!
106
122
  if @needs_redraw
107
123
  @screen.render(@editor)
108
124
  @needs_redraw = false
@@ -111,36 +127,39 @@ module RuVim
111
127
 
112
128
  key = @input.read_key(
113
129
  wakeup_ios: [@signal_r],
114
- timeout: loop_timeout_seconds,
115
- esc_timeout: escape_sequence_timeout_seconds
130
+ timeout: @key_handler.loop_timeout_seconds,
131
+ esc_timeout: @key_handler.escape_sequence_timeout_seconds
116
132
  )
117
133
  if key.nil?
118
- handle_pending_key_timeout if pending_key_timeout_expired?
119
- clear_expired_transient_message_if_any
134
+ @needs_redraw = true if @key_handler.handle_idle_timeout
120
135
  next
121
136
  end
122
137
 
123
- handle_key(key)
138
+ needs_redraw_from_key = @key_handler.handle(key)
124
139
  @needs_redraw = true
140
+ load_current_ftplugin!
141
+
142
+ # Force redraw after suspend_to_shell
143
+ @needs_redraw = true if needs_redraw_from_key
125
144
 
126
145
  # Batch insert-mode keystrokes to avoid per-char rendering during paste
127
146
  if @editor.mode == :insert && @input.has_pending_input?
128
- @paste_batch = true
147
+ @key_handler.paste_batch = true
129
148
  begin
130
149
  while @editor.mode == :insert && @input.has_pending_input?
131
150
  batch_key = @input.read_key(timeout: 0, esc_timeout: 0)
132
151
  break unless batch_key
133
- handle_key(batch_key)
152
+ @key_handler.handle(batch_key)
134
153
  end
135
154
  ensure
136
- @paste_batch = false
155
+ @key_handler.paste_batch = false
137
156
  end
138
157
  end
139
158
  end
140
159
  end
141
160
  ensure
142
- shutdown_background_readers!
143
- save_command_line_history!
161
+ @stream_mixer.shutdown!
162
+ @completion.save_history!
144
163
  end
145
164
 
146
165
  def run_startup_actions!(actions, log_prefix: "startup")
@@ -152,58 +171,10 @@ module RuVim
152
171
 
153
172
  private
154
173
 
155
- def pending_key_timeout_seconds
156
- return nil unless @pending_key_deadline
157
-
158
- [@pending_key_deadline - monotonic_now, 0.0].max
159
- end
160
-
161
- def loop_timeout_seconds
162
- now = monotonic_now
163
- timeouts = []
164
- if @pending_key_deadline
165
- timeouts << [@pending_key_deadline - now, 0.0].max
166
- end
167
- if (msg_to = @editor.transient_message_timeout_seconds(now:))
168
- timeouts << msg_to
169
- end
170
- timeouts.min
171
- end
172
-
173
- def pending_key_timeout_expired?
174
- @pending_key_deadline && monotonic_now >= @pending_key_deadline
175
- end
176
-
177
- def escape_sequence_timeout_seconds
178
- ms = @editor.global_options["ttimeoutlen"].to_i
179
- ms = 50 if ms <= 0
180
- ms / 1000.0
181
- rescue StandardError
182
- 0.005
183
- end
184
-
185
- def arm_pending_key_timeout
186
- ms = @editor.global_options["timeoutlen"].to_i
187
- ms = 1000 if ms <= 0
188
- @pending_key_deadline = monotonic_now + (ms / 1000.0)
189
- end
190
-
191
- def clear_pending_key_timeout
192
- @pending_key_deadline = nil
193
- @pending_ambiguous_invocation = nil
174
+ def clear_expired_transient_message_if_any
175
+ @needs_redraw = true if @key_handler.handle_idle_timeout
194
176
  end
195
177
 
196
- def handle_pending_key_timeout
197
- inv = @pending_ambiguous_invocation
198
- clear_pending_key_timeout
199
- if inv
200
- @dispatcher.dispatch(@editor, dup_invocation(inv))
201
- elsif @pending_keys && !@pending_keys.empty?
202
- @editor.echo_error("Unknown key: #{@pending_keys.join}")
203
- end
204
- @editor.pending_count = nil
205
- @pending_keys = []
206
- end
207
178
 
208
179
  def register_builtins!
209
180
  cmd = CommandRegistry.instance
@@ -315,16 +286,20 @@ module RuVim
315
286
  register_internal_unless(
316
287
  cmd,
317
288
  "stdin.stream_stop",
318
- call: ->(ctx, **) { ctx.editor.stdin_stream_stop_or_cancel! },
319
- desc: "Stop stdin follow stream (or cancel pending state)"
289
+ call: ->(ctx, **) {
290
+ return if ctx.editor.stream_stop_or_cancel!
291
+ ctx.editor.invoke_app_action(:normal_ctrl_c)
292
+ },
293
+ desc: "Stop stream (or cancel pending state)"
320
294
  )
321
295
 
322
- register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :maybe_one, bang: true)
296
+ register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :any, bang: true)
323
297
  register_ex_unless(ex, "q", call: :app_quit, aliases: %w[quit], desc: "Quit", nargs: 0, bang: true)
324
298
  register_ex_unless(ex, "qa", call: :app_quit_all, aliases: %w[qall], desc: "Quit all", nargs: 0, bang: true)
325
299
  register_ex_unless(ex, "wq", call: :file_write_quit, desc: "Write and quit", nargs: :maybe_one, bang: true)
326
300
  register_ex_unless(ex, "wqa", call: :file_write_quit_all, aliases: %w[wqall xa xall], desc: "Write all and quit", nargs: 0, bang: true)
327
301
  register_ex_unless(ex, "e", call: :file_edit, aliases: %w[edit], desc: "Edit file / reload", nargs: :maybe_one, bang: true)
302
+ register_ex_unless(ex, "r", call: :ex_read, aliases: %w[read], desc: "Read file or command output into buffer", nargs: :any)
328
303
  register_ex_unless(ex, "help", call: :ex_help, desc: "Show help / topics", nargs: :any)
329
304
  register_ex_unless(ex, "command", call: :ex_define_command, desc: "Define user command", nargs: :any, bang: true)
330
305
  register_ex_unless(ex, "ruby", call: :ex_ruby, aliases: %w[rb], desc: "Evaluate Ruby", nargs: :any, bang: false)
@@ -382,9 +357,12 @@ module RuVim
382
357
  register_internal_unless(cmd, "git.close_buffer", call: :git_close_buffer, desc: "Close git buffer")
383
358
  register_internal_unless(cmd, "git.status.open_file", call: :git_status_open_file, desc: "Open file from git status")
384
359
  register_internal_unless(cmd, "git.diff.open_file", call: :git_diff_open_file, desc: "Open file from git diff")
360
+ register_internal_unless(cmd, "git.grep.open_file", call: :git_grep_open_file, desc: "Open file from git grep")
385
361
  register_internal_unless(cmd, "git.branch.checkout", call: :git_branch_checkout, desc: "Checkout branch under cursor")
386
362
  register_internal_unless(cmd, "git.commit.execute", call: :git_commit_execute, desc: "Execute git commit")
363
+ register_ex_unless(ex, "run", call: :ex_run, desc: "Run command and show output in buffer", nargs: :any, raw_args: true)
387
364
  register_ex_unless(ex, "git", call: :ex_git, desc: "Git subcommand dispatcher", nargs: :any)
365
+ register_ex_unless(ex, "gh", call: :ex_gh, desc: "GitHub subcommand dispatcher", nargs: :any)
388
366
  end
389
367
 
390
368
  def bind_default_keys!
@@ -487,2739 +465,200 @@ module RuVim
487
465
  @keymaps.bind(:normal, "\e", "ui.clear_message")
488
466
  end
489
467
 
490
- def handle_key(key)
491
- mode_before = @editor.mode
492
- clear_stale_message_before_key(key)
493
- @skip_record_for_current_key = false
494
- append_dot_change_capture_key(key)
495
- if key == :ctrl_z
496
- suspend_to_shell
497
- track_mode_transition(mode_before)
498
- return
499
- end
500
- if key == :ctrl_c && @editor.mode != :normal
501
- handle_ctrl_c
502
- track_mode_transition(mode_before)
503
- record_macro_key_if_needed(key)
504
- return
505
- end
506
-
507
- case @editor.mode
508
- when :hit_enter
509
- handle_hit_enter_key(key)
510
- when :insert
511
- handle_insert_key(key)
512
- when :command_line
513
- handle_command_line_key(key)
514
- when :visual_char, :visual_line, :visual_block
515
- handle_visual_key(key)
516
- when :rich
517
- handle_rich_key(key)
518
- else
519
- handle_normal_key(key)
520
- end
521
- track_mode_transition(mode_before)
522
- load_current_ftplugin!
523
- record_macro_key_if_needed(key)
524
- rescue RuVim::CommandError => e
525
- @editor.echo_error(e.message)
526
- end
527
-
528
- def clear_stale_message_before_key(key)
529
- return if @editor.message.to_s.empty?
530
- return if @editor.command_line_active?
531
- return if @editor.hit_enter_active?
532
-
533
- # Keep the error visible while the user is still dismissing/cancelling;
534
- # otherwise, the next operation replaces the command-line area naturally.
535
- return if key == :ctrl_c
536
-
537
- @editor.clear_message
538
- end
539
-
540
- def handle_editor_app_action(name, **kwargs)
541
- if @editor.rich_mode?
542
- case name.to_sym
543
- when :normal_operator_start
544
- op = (kwargs[:name] || kwargs["name"]).to_sym
545
- return if op == :delete || op == :change
546
- when :normal_replace_pending_start, :normal_change_repeat
547
- return
548
- end
549
- end
550
-
551
- case name.to_sym
552
- when :normal_register_pending_start
553
- start_register_pending
554
- when :normal_operator_start
555
- start_operator_pending((kwargs[:name] || kwargs["name"]).to_sym)
556
- when :normal_replace_pending_start
557
- start_replace_pending
558
- when :normal_find_pending_start
559
- start_find_pending((kwargs[:token] || kwargs["token"]).to_s)
560
- when :normal_find_repeat
561
- repeat_last_find(reverse: !!(kwargs[:reverse] || kwargs["reverse"]))
562
- when :normal_change_repeat
563
- repeat_last_change
564
- when :normal_macro_record_toggle
565
- toggle_macro_recording_or_start_pending
566
- when :normal_macro_play_pending_start
567
- start_macro_play_pending
568
- when :normal_mark_pending_start
569
- start_mark_pending
570
- when :normal_jump_pending_start
571
- start_jump_pending(
572
- linewise: !!(kwargs[:linewise] || kwargs["linewise"]),
573
- repeat_token: (kwargs[:repeat_token] || kwargs["repeat_token"]).to_s
574
- )
575
- when :follow_toggle
576
- ex_follow_toggle
577
- else
578
- raise RuVim::CommandError, "Unknown app action: #{name}"
579
- end
580
- end
581
-
582
- def handle_normal_key(key)
583
- case
584
- when handle_normal_key_pre_dispatch(key)
585
- when (token = normalize_key_token(key)).nil?
586
- when handle_normal_pending_state(token)
587
- when handle_normal_direct_token(token)
588
- else
589
- @pending_keys ||= []
590
- @pending_keys << token
591
- resolve_normal_key_sequence
592
- end
593
- end
594
-
595
- def handle_normal_key_pre_dispatch(key)
596
- case
597
- when key == :enter && handle_list_window_enter
598
- when digit_key?(key) && count_digit_allowed?(key)
599
- @editor.pending_count = (@editor.pending_count.to_s + key).to_i
600
- @editor.echo(@editor.pending_count.to_s)
601
- @pending_keys = []
602
- else
603
- return false
468
+ def install_signal_handlers
469
+ Signal.trap("WINCH") do
470
+ @screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
471
+ @needs_redraw = true
472
+ notify_signal_wakeup
604
473
  end
605
- true
474
+ rescue ArgumentError
475
+ nil
606
476
  end
607
477
 
608
- def handle_normal_pending_state(token)
609
- case
610
- when @pending_keys && !@pending_keys.empty?
611
- @pending_keys << token
612
- resolve_normal_key_sequence
613
- when @operator_pending
614
- handle_operator_pending_key(token)
615
- when @register_pending
616
- finish_register_pending(token)
617
- when @mark_pending
618
- finish_mark_pending(token)
619
- when @jump_pending
620
- finish_jump_pending(token)
621
- when @macro_record_pending
622
- finish_macro_record_pending(token)
623
- when @macro_play_pending
624
- finish_macro_play_pending(token)
625
- when @replace_pending
626
- handle_replace_pending_key(token)
627
- when @find_pending
628
- finish_find_pending(token)
629
- else
630
- return false
631
- end
632
- true
478
+ def init_config_loader!
479
+ @config_loader = ConfigLoader.new(
480
+ command_registry: CommandRegistry.instance,
481
+ ex_registry: ExCommandRegistry.instance,
482
+ keymaps: @keymaps,
483
+ command_host: GlobalCommands.instance
484
+ )
633
485
  end
634
486
 
635
- def handle_normal_direct_token(token)
636
- false
637
- end
487
+ def load_user_config!
488
+ return if @clean_mode || @restricted_mode
489
+ return if @startup.skip_user_config
638
490
 
639
- def resolve_normal_key_sequence
640
- match = @keymaps.resolve_with_context(:normal, @pending_keys, editor: @editor)
641
- case match.status
642
- when :pending, :ambiguous
643
- if match.status == :ambiguous && match.invocation
644
- inv = dup_invocation(match.invocation)
645
- inv.count = @editor.pending_count
646
- @pending_ambiguous_invocation = inv
647
- else
648
- @pending_ambiguous_invocation = nil
649
- end
650
- arm_pending_key_timeout
651
- return
652
- when :match
653
- clear_pending_key_timeout
654
- matched_keys = @pending_keys.dup
655
- repeat_count = @editor.pending_count
656
- @pending_keys = []
657
- invocation = dup_invocation(match.invocation)
658
- invocation.count = repeat_count
659
- if @editor.rich_mode? && rich_mode_block_command?(invocation.id)
660
- @editor.pending_count = nil
661
- @pending_keys = []
662
- return
663
- end
664
- @dispatcher.dispatch(@editor, invocation)
665
- maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
491
+ if @startup.config_path
492
+ @config_loader.load_file(@startup.config_path)
666
493
  else
667
- clear_pending_key_timeout
668
- @editor.echo_error("Unknown key: #{@pending_keys.join}")
494
+ @config_loader.load_default!
669
495
  end
670
- @editor.pending_count = nil
671
- @pending_keys = []
496
+ rescue StandardError => e
497
+ @editor.echo_error("config error: #{e.message}")
672
498
  end
673
499
 
674
- def handle_insert_key(key)
675
- case key
676
- when :escape
677
- finish_insert_change_group
678
- finish_dot_change_capture
679
- clear_insert_completion
680
- @editor.enter_normal_mode
681
- @editor.echo("")
682
- when :backspace
683
- clear_insert_completion
684
- return unless insert_backspace_allowed?
685
- insert_backspace_in_insert_mode
686
- when :ctrl_n
687
- insert_complete(+1)
688
- when :ctrl_p
689
- insert_complete(-1)
690
- when :ctrl_i
691
- clear_insert_completion
692
- insert_tab_in_insert_mode
693
- when :enter
694
- clear_insert_completion
695
- y, x = @editor.current_buffer.insert_newline(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
696
- x = apply_insert_autoindent(y, x, previous_row: y - 1)
697
- @editor.current_window.cursor_y = y
698
- @editor.current_window.cursor_x = x
699
- when :left
700
- clear_insert_completion
701
- dispatch_insert_cursor_motion("cursor.left")
702
- when :right
703
- clear_insert_completion
704
- dispatch_insert_cursor_motion("cursor.right")
705
- when :up
706
- clear_insert_completion
707
- @editor.current_window.move_up(@editor.current_buffer, 1)
708
- when :down
709
- clear_insert_completion
710
- @editor.current_window.move_down(@editor.current_buffer, 1)
711
- when :pageup, :pagedown
712
- clear_insert_completion
713
- invoke_page_key(key)
714
- else
715
- return unless key.is_a?(String)
500
+ def load_current_ftplugin!
501
+ return if @clean_mode || @restricted_mode
502
+ return unless @config_loader
716
503
 
717
- clear_insert_completion
718
- @editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
719
- @editor.current_window.cursor_x += 1
720
- maybe_showmatch_after_insert(key)
721
- maybe_dedent_after_insert(key)
722
- end
504
+ @config_loader.load_ftplugin!(@editor, @editor.current_buffer)
505
+ rescue StandardError => e
506
+ @editor.echo_error("ftplugin error: #{e.message}")
723
507
  end
724
508
 
725
- def handle_visual_key(key)
726
- if arrow_key?(key)
727
- invoke_arrow(key)
728
- return
729
- end
730
-
731
- if paging_key?(key)
732
- invoke_page_key(key)
733
- return
734
- end
735
-
736
- token = normalize_key_token(key)
737
- return if token.nil?
738
-
739
- case token
740
- when "\e"
741
- @register_pending = false
742
- @visual_pending = nil
743
- @editor.enter_normal_mode
744
- when "v"
745
- if @editor.mode == :visual_char
746
- @editor.enter_normal_mode
747
- else
748
- @editor.enter_visual(:visual_char)
749
- end
750
- when "V"
751
- if @editor.mode == :visual_line
752
- @editor.enter_normal_mode
753
- else
754
- @editor.enter_visual(:visual_line)
755
- end
756
- when "<C-v>"
757
- if @editor.mode == :visual_block
758
- @editor.enter_normal_mode
759
- else
760
- @editor.enter_visual(:visual_block)
761
- end
762
- when "y"
763
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_yank"))
764
- when "d"
765
- @visual_pending = nil
766
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
767
- when "="
768
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_indent"))
769
- when "\""
770
- start_register_pending
771
- when "i", "a"
772
- @visual_pending = token
773
- else
774
- if @register_pending
775
- finish_register_pending(token)
776
- return
777
- end
778
- if @visual_pending
779
- if @editor.mode == :visual_block
780
- @visual_pending = nil
781
- @editor.echo_error("text object in Visual block not supported yet")
782
- return
783
- end
784
- motion = "#{@visual_pending}#{token}"
785
- @visual_pending = nil
786
- inv = CommandInvocation.new(id: "buffer.visual_select_text_object", kwargs: { motion: motion })
787
- @dispatcher.dispatch(@editor, inv)
788
- else
789
- handle_visual_motion_token(token)
790
- end
509
+ def run_startup_action!(action, log_prefix: "startup")
510
+ case action[:type]
511
+ when :ex
512
+ verbose_log(2, "#{log_prefix} ex: #{action[:value]}")
513
+ @dispatcher.dispatch_ex(@editor, action[:value].to_s)
514
+ when :line
515
+ verbose_log(2, "#{log_prefix} line: #{action[:value]}")
516
+ move_cursor_to_line(action[:value].to_i)
517
+ when :line_end
518
+ verbose_log(2, "#{log_prefix} line_end")
519
+ move_cursor_to_line(@editor.current_buffer.line_count)
791
520
  end
792
- @editor.pending_count = nil
793
- @pending_keys = []
794
521
  end
795
522
 
796
- def handle_visual_motion_token(token)
797
- id = {
798
- "h" => "cursor.left",
799
- "j" => "cursor.down",
800
- "k" => "cursor.up",
801
- "l" => "cursor.right",
802
- "0" => "cursor.line_start",
803
- "$" => "cursor.line_end",
804
- "^" => "cursor.first_nonblank",
805
- "w" => "cursor.word_forward",
806
- "b" => "cursor.word_backward",
807
- "e" => "cursor.word_end",
808
- "G" => "cursor.buffer_end"
809
- }[token]
810
-
811
- if token == "g"
812
- @pending_keys ||= []
813
- @pending_keys << token
814
- arm_pending_key_timeout
815
- return
816
- end
817
-
818
- if @pending_keys == ["g"] && token == "g"
819
- id = "cursor.buffer_start"
820
- end
821
-
822
- if id
823
- clear_pending_key_timeout
824
- count = @editor.pending_count
825
- @dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
826
- else
827
- clear_pending_key_timeout
828
- @editor.echo_error("Unknown visual key: #{token}")
829
- end
830
- ensure
831
- @pending_keys = [] unless token == "g"
832
- end
523
+ def verbose_log(level, message)
524
+ return if @verbose_level < level
525
+ return unless @verbose_io
833
526
 
834
- def handle_command_line_key(key)
835
- cmd = @editor.command_line
836
- case key
837
- when :escape
838
- clear_command_line_completion
839
- cancel_incsearch_preview_if_any
840
- @editor.cancel_command_line
841
- when :enter
842
- clear_command_line_completion
843
- line = cmd.text.dup
844
- push_command_line_history(cmd.prefix, line)
845
- handle_command_line_submit(cmd.prefix, line)
846
- when :backspace
847
- clear_command_line_completion
848
- if cmd.text.empty? && cmd.cursor.zero?
849
- cancel_incsearch_preview_if_any
850
- @editor.cancel_command_line
851
- return
852
- end
853
- cmd.backspace
854
- when :up
855
- clear_command_line_completion
856
- command_line_history_move(-1)
857
- when :down
858
- clear_command_line_completion
859
- command_line_history_move(1)
860
- when :left
861
- clear_command_line_completion
862
- cmd.move_left
863
- when :right
864
- clear_command_line_completion
865
- cmd.move_right
866
- else
867
- if key == :ctrl_i
868
- command_line_complete
869
- elsif key.is_a?(String)
870
- clear_command_line_completion
871
- @cmdline_history_index = nil
872
- cmd.insert(key)
873
- end
874
- end
875
- update_incsearch_preview_if_needed
527
+ @verbose_io.puts("[ruvim:v#{@verbose_level}] #{message}")
528
+ @verbose_io.flush if @verbose_io.respond_to?(:flush)
529
+ rescue StandardError
530
+ nil
876
531
  end
877
532
 
878
- def handle_list_window_enter
879
- buffer = @editor.current_buffer
880
- return handle_filter_buffer_enter if buffer.kind == :filter
881
- return handle_git_status_enter if buffer.kind == :git_status
882
- return handle_git_diff_enter if buffer.kind == :git_diff || buffer.kind == :git_log
883
- return handle_git_branch_enter if buffer.kind == :git_branch
884
- return false unless buffer.kind == :quickfix || buffer.kind == :location_list
885
-
886
- item_index = @editor.current_window.cursor_y - 2
887
- if item_index.negative?
888
- @editor.echo_error("No list item on this line")
889
- return true
890
- end
891
-
892
- source_window_id = buffer.options["ruvim_list_source_window_id"]
893
- source_window_id = source_window_id.to_i if source_window_id
894
- source_window_id = nil unless source_window_id && @editor.windows.key?(source_window_id)
895
-
896
- item =
897
- if buffer.kind == :quickfix
898
- @editor.select_quickfix(item_index)
899
- else
900
- owner_window_id = source_window_id || @editor.current_window_id
901
- @editor.select_location_list(item_index, window_id: owner_window_id)
902
- end
903
-
904
- unless item
905
- @editor.echo_error("#{buffer.kind == :quickfix ? 'quickfix' : 'location list'} item not found")
906
- return true
907
- end
533
+ def startup_mark(label)
534
+ return unless @startup&.time_path
908
535
 
909
- if source_window_id
910
- @editor.current_window_id = source_window_id
911
- end
912
- @editor.jump_to_location(item)
913
- @editor.echo(
914
- if buffer.kind == :quickfix
915
- "qf #{@editor.quickfix_index.to_i + 1}/#{@editor.quickfix_items.length}"
916
- else
917
- owner_window_id = source_window_id || @editor.current_window_id
918
- list = @editor.location_list(owner_window_id)
919
- "ll #{list[:index].to_i + 1}/#{list[:items].length}"
920
- end
921
- )
922
- true
536
+ @startup.timeline << [label.to_s, monotonic_now]
923
537
  end
924
538
 
925
- def handle_filter_buffer_enter
926
- buffer = @editor.current_buffer
927
- origins = buffer.options["filter_origins"]
928
- return false unless origins
929
-
930
- row = @editor.current_window.cursor_y
931
- origin = origins[row]
932
- unless origin
933
- @editor.echo_error("No filter item on this line")
934
- return true
935
- end
936
-
937
- target_buffer_id = origin[:buffer_id]
938
- target_row = origin[:row]
939
- filter_buf_id = buffer.id
539
+ def write_startuptime_log!
540
+ return unless @startup&.time_path
940
541
 
941
- @editor.delete_buffer(filter_buf_id)
942
- target_buf = @editor.buffers[target_buffer_id]
943
- if target_buf
944
- @editor.switch_to_buffer(target_buffer_id) unless @editor.current_buffer.id == target_buffer_id
945
- @editor.current_window.cursor_y = [target_row, target_buf.lines.length - 1].min
946
- @editor.current_window.cursor_x = 0
542
+ prev = @startup.time_origin
543
+ lines = @startup.timeline.map do |label, t|
544
+ total_ms = ((t - @startup.time_origin) * 1000.0)
545
+ delta_ms = ((t - prev) * 1000.0)
546
+ prev = t
547
+ format("%9.3f %9.3f %s", total_ms, delta_ms, label)
947
548
  end
948
- true
949
- end
950
-
951
- def handle_git_status_enter
952
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.status.open_file"))
953
- true
954
- end
955
-
956
- def handle_git_diff_enter
957
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.diff.open_file"))
958
- true
959
- end
960
-
961
- def handle_git_branch_enter
962
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.branch.checkout"))
963
- true
964
- end
965
-
966
- def arrow_key?(key)
967
- %i[left right up down].include?(key)
968
- end
969
-
970
- def paging_key?(key)
971
- %i[pageup pagedown].include?(key)
972
- end
973
-
974
- def invoke_arrow(key)
975
- id = {
976
- left: "cursor.left",
977
- right: "cursor.right",
978
- up: "cursor.up",
979
- down: "cursor.down"
980
- }.fetch(key)
981
- inv = CommandInvocation.new(id:, count: @editor.pending_count)
982
- @dispatcher.dispatch(@editor, inv)
983
- @editor.pending_count = nil
984
- @pending_keys = []
549
+ File.write(@startup.time_path, lines.join("\n") + "\n")
550
+ rescue StandardError => e
551
+ verbose_log(1, "startuptime write error: #{e.message}")
985
552
  end
986
553
 
987
- def invoke_page_key(key)
988
- id = (key == :pageup ? "cursor.page_up" : "cursor.page_down")
989
- inv = CommandInvocation.new(
990
- id: id,
991
- count: @editor.pending_count,
992
- kwargs: { page_lines: current_page_step_lines }
993
- )
994
- @dispatcher.dispatch(@editor, inv)
995
- @editor.pending_count = nil
996
- @pending_keys = []
554
+ def monotonic_now
555
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
556
+ rescue StandardError
557
+ Time.now.to_f
997
558
  end
998
559
 
999
- def digit_key?(key)
1000
- key.is_a?(String) && key.match?(/\A\d\z/)
560
+ def apply_startup_buffer_flags!
561
+ apply_startup_readonly! if @startup.readonly
562
+ apply_startup_nomodifiable! if @startup.nomodifiable
563
+ apply_startup_follow! if @startup.follow
1001
564
  end
1002
565
 
1003
- def count_digit_allowed?(key)
1004
- return false unless @editor.mode == :normal
1005
- return true unless @editor.pending_count.nil?
566
+ def apply_startup_readonly!
567
+ buf = @editor.current_buffer
568
+ return unless buf&.file_buffer?
1006
569
 
1007
- key != "0"
570
+ buf.readonly = true
571
+ @editor.echo("readonly: #{buf.display_name}")
1008
572
  end
1009
573
 
1010
- def normalize_key_token(key)
1011
- case key
1012
- when String then key
1013
- when :escape then "\e"
1014
- when :ctrl_r then "<C-r>"
1015
- when :ctrl_d then "<C-d>"
1016
- when :ctrl_u then "<C-u>"
1017
- when :ctrl_f then "<C-f>"
1018
- when :ctrl_b then "<C-b>"
1019
- when :ctrl_e then "<C-e>"
1020
- when :ctrl_y then "<C-y>"
1021
- when :ctrl_v then "<C-v>"
1022
- when :ctrl_i then "<C-i>"
1023
- when :ctrl_o then "<C-o>"
1024
- when :ctrl_w then "<C-w>"
1025
- when :ctrl_l then "<C-l>"
1026
- when :ctrl_c then "<C-c>"
1027
- when :ctrl_g then "<C-g>"
1028
- when :left then "<Left>"
1029
- when :right then "<Right>"
1030
- when :up then "<Up>"
1031
- when :down then "<Down>"
1032
- when :home then "<Home>"
1033
- when :end then "<End>"
1034
- when :pageup then "<PageUp>"
1035
- when :pagedown then "<PageDown>"
1036
- when :shift_up then "<S-Up>"
1037
- when :shift_down then "<S-Down>"
1038
- when :shift_left then "<S-Left>"
1039
- when :shift_right then "<S-Right>"
1040
- else nil
1041
- end
1042
- end
574
+ def apply_startup_follow!
575
+ buf = @editor.current_buffer
576
+ return unless buf&.file_buffer?
577
+ return if @stream_mixer.follow_active?(buf)
1043
578
 
1044
- def dup_invocation(inv)
1045
- CommandInvocation.new(
1046
- id: inv.id,
1047
- argv: inv.argv.dup,
1048
- kwargs: inv.kwargs.dup,
1049
- count: inv.count,
1050
- bang: inv.bang,
1051
- raw_keys: inv.raw_keys&.dup
1052
- )
579
+ win = @editor.current_window
580
+ win.cursor_y = buf.line_count - 1
581
+ win.clamp_to_buffer(buf)
582
+ @stream_mixer.start_follow!(buf)
1053
583
  end
1054
584
 
1055
- # Rich mode: delegates to normal mode key handling but blocks mutating operations.
1056
- RICH_MODE_BLOCKED_COMMANDS = %w[
1057
- mode.insert mode.append mode.append_line_end mode.insert_nonblank
1058
- mode.open_below mode.open_above
1059
- buffer.delete_char buffer.delete_line buffer.delete_motion
1060
- buffer.change_motion buffer.change_line
1061
- buffer.paste_after buffer.paste_before
1062
- buffer.replace_char
1063
- buffer.visual_delete
1064
- ].freeze
585
+ def apply_startup_nomodifiable!
586
+ buf = @editor.current_buffer
587
+ return unless buf&.file_buffer?
1065
588
 
1066
- def handle_hit_enter_key(key)
1067
- token = normalize_key_token(key)
1068
- case token
1069
- when ":"
1070
- @editor.exit_hit_enter_mode
1071
- @editor.enter_command_line_mode(":")
1072
- when "/", "?"
1073
- @editor.exit_hit_enter_mode
1074
- @editor.enter_command_line_mode(token)
1075
- else
1076
- @editor.exit_hit_enter_mode
1077
- end
589
+ buf.modifiable = false
590
+ buf.readonly = true
591
+ @editor.echo("nomodifiable: #{buf.display_name}")
1078
592
  end
1079
593
 
1080
- def handle_rich_key(key)
1081
- token = normalize_key_token(key)
1082
- if token == "\e"
1083
- RuVim::RichView.close!(@editor)
1084
- return
594
+ def apply_startup_compat_mode_messages!
595
+ if @startup.diff_mode
596
+ verbose_log(1, "startup: -d requested (diff mode placeholder)")
597
+ @editor.echo("diff mode (-d) is not implemented yet")
1085
598
  end
1086
599
 
1087
- handle_normal_key(key)
1088
- end
1089
-
1090
- def rich_mode_block_command?(command_id)
1091
- RICH_MODE_BLOCKED_COMMANDS.include?(command_id.to_s)
1092
- end
1093
-
1094
- def handle_ctrl_c
1095
- case @editor.mode
1096
- when :hit_enter
1097
- @editor.exit_hit_enter_mode
1098
- when :insert
1099
- finish_insert_change_group
1100
- finish_dot_change_capture
1101
- clear_insert_completion
1102
- clear_pending_key_timeout
1103
- @editor.enter_normal_mode
1104
- @editor.echo("")
1105
- when :command_line
1106
- clear_pending_key_timeout
1107
- cancel_incsearch_preview_if_any
1108
- @editor.cancel_command_line
1109
- when :visual_char, :visual_line, :visual_block
1110
- @visual_pending = nil
1111
- @register_pending = false
1112
- @mark_pending = false
1113
- @jump_pending = nil
1114
- clear_pending_key_timeout
1115
- @editor.enter_normal_mode
1116
- when :rich
1117
- clear_pending_key_timeout
1118
- @editor.pending_count = nil
1119
- @pending_keys = []
1120
- @operator_pending = nil
1121
- @replace_pending = nil
1122
- @register_pending = false
1123
- @mark_pending = false
1124
- @jump_pending = nil
1125
- @macro_record_pending = false
1126
- @macro_play_pending = false
1127
- RuVim::RichView.close!(@editor)
1128
- else
1129
- clear_pending_key_timeout
1130
- @editor.pending_count = nil
1131
- @pending_keys = []
1132
- @operator_pending = nil
1133
- @replace_pending = nil
1134
- @register_pending = false
1135
- @mark_pending = false
1136
- @jump_pending = nil
1137
- @macro_record_pending = false
1138
- @macro_play_pending = false
1139
- @editor.clear_message
600
+ if @startup.quickfix_errorfile
601
+ verbose_log(1, "startup: -q #{@startup.quickfix_errorfile} requested (quickfix placeholder)")
602
+ @editor.echo("quickfix startup (-q #{@startup.quickfix_errorfile}) is not implemented yet")
1140
603
  end
1141
- end
1142
604
 
1143
- def handle_normal_ctrl_c
1144
- clear_pending_key_timeout
1145
- @editor.pending_count = nil
1146
- @pending_keys = []
1147
- @operator_pending = nil
1148
- @replace_pending = nil
1149
- @register_pending = false
1150
- @mark_pending = false
1151
- @jump_pending = nil
1152
- @macro_record_pending = false
1153
- @macro_play_pending = false
1154
- buf = @editor.current_buffer
1155
- if buf && @follow_watchers[buf.id]
1156
- stop_follow!(buf)
1157
- else
1158
- @editor.clear_message
605
+ if @startup.session_file
606
+ verbose_log(1, "startup: -S #{@startup.session_file} requested (session placeholder)")
607
+ @editor.echo("session startup (-S #{@startup.session_file}) is not implemented yet")
1159
608
  end
1160
609
  end
1161
610
 
1162
- def suspend_to_shell
1163
- @terminal.suspend_for_tstp
1164
- @screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
1165
- @needs_redraw = true
1166
- rescue StandardError => e
1167
- @editor.echo_error("suspend failed: #{e.message}")
1168
- end
1169
-
1170
- def finish_insert_change_group
1171
- @editor.current_buffer.end_change_group
1172
- end
1173
-
1174
- def handle_command_line_submit(prefix, line)
1175
- clear_incsearch_preview_state(apply: false) if %w[/ ?].include?(prefix)
1176
- case prefix
1177
- when ":"
1178
- verbose_log(2, "ex: #{line}")
1179
- @dispatcher.dispatch_ex(@editor, line)
1180
- when "/"
1181
- verbose_log(2, "search(/): #{line}")
1182
- submit_search(line, direction: :forward)
1183
- when "?"
1184
- verbose_log(2, "search(?): #{line}")
1185
- submit_search(line, direction: :backward)
1186
- else
1187
- @editor.echo_error("Unknown command-line prefix: #{prefix}")
1188
- @editor.enter_normal_mode
1189
- end
1190
- @cmdline_history_index = nil
1191
- end
611
+ def open_startup_paths!(paths)
612
+ list = Array(paths).compact
613
+ return if list.empty?
1192
614
 
1193
- def start_operator_pending(name)
1194
- @operator_pending = { name:, count: @editor.pending_count }
1195
- @editor.pending_count = nil
1196
- @pending_keys = []
1197
- @editor.echo(name == :delete ? "d" : name.to_s)
1198
- end
615
+ evict_bootstrap_buffer!
616
+ @editor.set_arglist(list)
1199
617
 
1200
- def start_register_pending
1201
- @register_pending = true
1202
- @editor.echo('"')
1203
- end
618
+ first, *rest = list
619
+ @editor.open_path(first)
620
+ apply_startup_buffer_flags!
1204
621
 
1205
- def finish_register_pending(token)
1206
- @register_pending = false
1207
- if token.is_a?(String) && token.length == 1
1208
- @editor.set_active_register(token)
1209
- @editor.echo(%("#{token}))
622
+ case @startup.open_layout
623
+ when :horizontal
624
+ first_win_id = @editor.current_window_id
625
+ rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
626
+ @editor.focus_window(first_win_id)
627
+ when :vertical
628
+ first_win_id = @editor.current_window_id
629
+ rest.each { |p| open_path_in_split!(p, layout: :vertical) }
630
+ @editor.focus_window(first_win_id)
631
+ when :tab
632
+ rest.each { |p| open_path_in_tab!(p) }
633
+ @editor.tabnext(-(@editor.tabpage_count - 1))
1210
634
  else
1211
- @editor.echo_error("Invalid register")
1212
- end
1213
- end
1214
-
1215
- def start_mark_pending
1216
- @mark_pending = true
1217
- @editor.echo("m")
1218
- end
1219
-
1220
- def finish_mark_pending(token)
1221
- @mark_pending = false
1222
- if token == "\e"
1223
- @editor.clear_message
1224
- return
1225
- end
1226
- unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
1227
- @editor.echo_error("Invalid mark")
1228
- return
635
+ rest.each do |p|
636
+ buf = @editor.add_buffer_from_file(p)
637
+ @stream_mixer.start_follow!(buf) if @startup.follow
638
+ end
1229
639
  end
1230
-
1231
- inv = CommandInvocation.new(id: "mark.set", kwargs: { mark: token })
1232
- @dispatcher.dispatch(@editor, inv)
1233
- end
1234
-
1235
- def start_jump_pending(linewise:, repeat_token:)
1236
- @jump_pending = { linewise: linewise, repeat_token: repeat_token }
1237
- @editor.echo(repeat_token)
1238
640
  end
1239
641
 
1240
- def finish_jump_pending(token)
1241
- pending = @jump_pending
1242
- @jump_pending = nil
1243
- return unless pending
1244
- if token == "\e"
1245
- @editor.clear_message
1246
- return
1247
- end
1248
-
1249
- if token == pending[:repeat_token]
1250
- inv = CommandInvocation.new(id: "jump.older", kwargs: { linewise: pending[:linewise] })
1251
- @dispatcher.dispatch(@editor, inv)
1252
- return
1253
- end
1254
-
1255
- unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
1256
- @editor.echo_error("Invalid mark")
1257
- return
642
+ def evict_bootstrap_buffer!
643
+ bid = @editor.buffer_ids.find do |id|
644
+ b = @editor.buffers[id]
645
+ b.path.nil? && !b.modified? && b.line_count <= 1 && b.kind == :file
1258
646
  end
647
+ return unless bid
1259
648
 
1260
- inv = CommandInvocation.new(id: "mark.jump", kwargs: { mark: token, linewise: pending[:linewise] })
1261
- @dispatcher.dispatch(@editor, inv)
1262
- end
1263
-
1264
- def start_macro_record_pending
1265
- @macro_record_pending = true
1266
- @editor.echo("q")
649
+ @editor.buffers.delete(bid)
650
+ @editor.instance_variable_set(:@next_buffer_id, 1)
1267
651
  end
1268
652
 
1269
- def toggle_macro_recording_or_start_pending
1270
- if @editor.macro_recording?
1271
- stop_macro_recording
1272
- else
1273
- start_macro_record_pending
1274
- end
1275
- end
1276
-
1277
- def finish_macro_record_pending(token)
1278
- @macro_record_pending = false
1279
- if token == "\e"
1280
- @editor.clear_message
1281
- return
1282
- end
1283
- unless token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
1284
- @editor.echo_error("Invalid macro register")
1285
- return
1286
- end
1287
-
1288
- unless @editor.start_macro_recording(token)
1289
- @editor.echo("Failed to start recording")
1290
- return
1291
- end
1292
- @skip_record_for_current_key = true
1293
- @editor.echo("recording @#{token}")
1294
- end
1295
-
1296
- def stop_macro_recording
1297
- reg = @editor.macro_recording_name
1298
- @editor.stop_macro_recording
1299
- @editor.echo("recording @#{reg} stopped")
1300
- end
1301
-
1302
- def start_macro_play_pending
1303
- @macro_play_pending = true
1304
- @editor.echo("@")
1305
- end
1306
-
1307
- def finish_macro_play_pending(token)
1308
- @macro_play_pending = false
1309
- if token == "\e"
1310
- @editor.clear_message
1311
- return
1312
- end
1313
- name =
1314
- if token == "@"
1315
- @last_macro_name
1316
- elsif token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
1317
- token
1318
- end
1319
- unless name
1320
- @editor.echo_error("Invalid macro register")
1321
- return
1322
- end
1323
-
1324
- count = @editor.pending_count
1325
- @editor.pending_count = nil
1326
- play_macro(name, count:)
1327
- end
1328
-
1329
- def play_macro(name, count:)
1330
- reg = name.to_s.downcase
1331
- keys = @editor.macro_keys(reg)
1332
- if keys.nil? || keys.empty?
1333
- @editor.echo("Macro empty: #{reg}")
1334
- return
1335
- end
1336
-
1337
- @macro_play_stack ||= []
1338
- if @macro_play_stack.include?(reg) || @macro_play_stack.length >= 20
1339
- @editor.echo("Macro recursion blocked: #{reg}")
1340
- return
1341
- end
1342
-
1343
- @last_macro_name = reg
1344
- @macro_play_stack << reg
1345
- @suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
1346
- [count.to_i, 1].max.times do
1347
- keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
1348
- end
1349
- @editor.echo("@#{reg}")
1350
- ensure
1351
- @suspend_macro_recording_depth = [(@suspend_macro_recording_depth || 1) - 1, 0].max
1352
- @macro_play_stack.pop if @macro_play_stack && !@macro_play_stack.empty?
1353
- end
1354
-
1355
- def record_macro_key_if_needed(key)
1356
- return if @skip_record_for_current_key
1357
- return unless @editor.macro_recording?
1358
- return if (@suspend_macro_recording_depth || 0).positive?
1359
- return if (@dot_replay_depth || 0).positive?
1360
-
1361
- @editor.record_macro_key(key)
1362
- end
1363
-
1364
- def dup_macro_runtime_key(key)
1365
- case key
1366
- when String
1367
- key.dup
1368
- when Array
1369
- key.map { |v| v.is_a?(String) ? v.dup : v }
1370
- else
1371
- key
1372
- end
1373
- end
1374
-
1375
- def handle_operator_pending_key(token)
1376
- op = @operator_pending
1377
- if %w[i a g].include?(token) && !op[:motion_prefix]
1378
- @operator_pending[:motion_prefix] = token
1379
- @editor.echo("#{op[:name].to_s[0]}#{token}")
1380
- return
1381
- end
1382
-
1383
- motion = [op[:motion_prefix], token].compact.join
1384
- @operator_pending = nil
1385
-
1386
- if token == "\e"
1387
- @editor.clear_message
1388
- return
1389
- end
1390
-
1391
- if op[:name] == :delete && motion == "d"
1392
- inv = CommandInvocation.new(id: "buffer.delete_line", count: op[:count])
1393
- @dispatcher.dispatch(@editor, inv)
1394
- record_last_change_keys(count_prefixed_keys(op[:count], ["d", "d"]))
1395
- return
1396
- end
1397
-
1398
- if op[:name] == :delete
1399
- inv = CommandInvocation.new(id: "buffer.delete_motion", count: op[:count], kwargs: { motion: motion })
1400
- @dispatcher.dispatch(@editor, inv)
1401
- record_last_change_keys(count_prefixed_keys(op[:count], ["d", *motion.each_char.to_a]))
1402
- return
1403
- end
1404
-
1405
- if op[:name] == :yank && motion == "y"
1406
- inv = CommandInvocation.new(id: "buffer.yank_line", count: op[:count])
1407
- @dispatcher.dispatch(@editor, inv)
1408
- return
1409
- end
1410
-
1411
- if op[:name] == :yank
1412
- inv = CommandInvocation.new(id: "buffer.yank_motion", count: op[:count], kwargs: { motion: motion })
1413
- @dispatcher.dispatch(@editor, inv)
1414
- return
1415
- end
1416
-
1417
- if op[:name] == :indent && motion == "="
1418
- inv = CommandInvocation.new(id: "buffer.indent_lines", count: op[:count])
1419
- @dispatcher.dispatch(@editor, inv)
1420
- return
1421
- end
1422
-
1423
- if op[:name] == :indent
1424
- inv = CommandInvocation.new(id: "buffer.indent_motion", count: op[:count], kwargs: { motion: motion })
1425
- @dispatcher.dispatch(@editor, inv)
1426
- return
1427
- end
1428
-
1429
- if op[:name] == :change && motion == "c"
1430
- inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
1431
- @dispatcher.dispatch(@editor, inv)
1432
- begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", "c"])) if @editor.mode == :insert
1433
- return
1434
- end
1435
-
1436
- if op[:name] == :change
1437
- inv = CommandInvocation.new(id: "buffer.change_motion", count: op[:count], kwargs: { motion: motion })
1438
- @dispatcher.dispatch(@editor, inv)
1439
- begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", *motion.each_char.to_a])) if @editor.mode == :insert
1440
- return
1441
- end
1442
-
1443
- @editor.echo_error("Unknown operator")
1444
- end
1445
-
1446
- def start_replace_pending
1447
- @replace_pending = { count: @editor.pending_count }
1448
- @editor.pending_count = nil
1449
- @pending_keys = []
1450
- @editor.echo("r")
1451
- end
1452
-
1453
- def handle_replace_pending_key(token)
1454
- pending = @replace_pending
1455
- @replace_pending = nil
1456
- if token == "\e"
1457
- @editor.clear_message
1458
- return
1459
- end
1460
-
1461
- if token.is_a?(String) && !token.empty?
1462
- inv = CommandInvocation.new(id: "buffer.replace_char", argv: [token], count: pending[:count])
1463
- @dispatcher.dispatch(@editor, inv)
1464
- record_last_change_keys(count_prefixed_keys(pending[:count], ["r", token]))
1465
- else
1466
- @editor.echo("r expects one character")
1467
- end
1468
- end
1469
-
1470
- def repeat_last_change
1471
- keys = @last_change_keys
1472
- if keys.nil? || keys.empty?
1473
- @editor.echo("No previous change")
1474
- return
1475
- end
1476
-
1477
- @dot_replay_depth = (@dot_replay_depth || 0) + 1
1478
- keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
1479
- @editor.echo(".")
1480
- ensure
1481
- @dot_replay_depth = [(@dot_replay_depth || 1) - 1, 0].max
1482
- end
1483
-
1484
- def maybe_record_simple_dot_change(invocation, matched_keys, count)
1485
- return if (@dot_replay_depth || 0).positive?
1486
-
1487
- case invocation.id
1488
- when "buffer.delete_char", "buffer.delete_motion", "buffer.join_lines", "buffer.swapcase_char", "buffer.paste_after", "buffer.paste_before"
1489
- record_last_change_keys(count_prefixed_keys(count, matched_keys))
1490
- 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"
1491
- begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
1492
- end
1493
- end
1494
-
1495
- def begin_dot_change_capture(prefix_keys)
1496
- return if (@dot_replay_depth || 0).positive?
1497
-
1498
- @dot_change_capture_keys = Array(prefix_keys).map { |k| dup_macro_runtime_key(k) }
1499
- @dot_change_capture_active = true
1500
- end
1501
-
1502
- def append_dot_change_capture_key(key)
1503
- return unless @dot_change_capture_active
1504
- return if (@dot_replay_depth || 0).positive?
1505
-
1506
- @dot_change_capture_keys ||= []
1507
- @dot_change_capture_keys << dup_macro_runtime_key(key)
1508
- end
1509
-
1510
- def finish_dot_change_capture
1511
- return unless @dot_change_capture_active
1512
-
1513
- keys = Array(@dot_change_capture_keys)
1514
- @dot_change_capture_active = false
1515
- @dot_change_capture_keys = nil
1516
- record_last_change_keys(keys)
1517
- end
1518
-
1519
- def record_last_change_keys(keys)
1520
- return if (@dot_replay_depth || 0).positive?
1521
-
1522
- @last_change_keys = Array(keys).map { |k| dup_macro_runtime_key(k) }
1523
- end
1524
-
1525
- def count_prefixed_keys(count, keys)
1526
- c = count.to_i
1527
- prefix = c > 1 ? c.to_s.each_char.to_a : []
1528
- prefix + Array(keys)
1529
- end
1530
-
1531
- def start_find_pending(token)
1532
- @find_pending = {
1533
- direction: (token == "f" || token == "t") ? :forward : :backward,
1534
- till: (token == "t" || token == "T"),
1535
- count: @editor.pending_count
1536
- }
1537
- @editor.pending_count = nil
1538
- @pending_keys = []
1539
- @editor.echo(token)
1540
- end
1541
-
1542
- def finish_find_pending(token)
1543
- pending = @find_pending
1544
- @find_pending = nil
1545
- if token == "\e"
1546
- @editor.clear_message
1547
- return
1548
- end
1549
- unless token.is_a?(String) && !token.empty?
1550
- @editor.echo("find expects one character")
1551
- return
1552
- end
1553
-
1554
- moved = perform_find_on_line(
1555
- char: token,
1556
- direction: pending[:direction],
1557
- till: pending[:till],
1558
- count: pending[:count]
1559
- )
1560
- if moved
1561
- @editor.set_last_find(char: token, direction: pending[:direction], till: pending[:till])
1562
- else
1563
- @editor.echo("Char not found: #{token}")
1564
- end
1565
- end
1566
-
1567
- def repeat_last_find(reverse:)
1568
- last = @editor.last_find
1569
- unless last
1570
- @editor.echo("No previous f/t")
1571
- return
1572
- end
1573
-
1574
- direction =
1575
- if reverse
1576
- last[:direction] == :forward ? :backward : :forward
1577
- else
1578
- last[:direction]
1579
- end
1580
- count = @editor.pending_count
1581
- @editor.pending_count = nil
1582
- @pending_keys = []
1583
- moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
1584
- @editor.echo("Char not found: #{last[:char]}") unless moved
1585
- end
1586
-
1587
- def perform_find_on_line(char:, direction:, till:, count:)
1588
- win = @editor.current_window
1589
- buf = @editor.current_buffer
1590
- line = buf.line_at(win.cursor_y)
1591
- pos = win.cursor_x
1592
- target = nil
1593
-
1594
- [count.to_i, 1].max.times do
1595
- idx =
1596
- if direction == :forward
1597
- line.index(char, pos + 1)
1598
- else
1599
- rindex_from(line, char, pos - 1)
1600
- end
1601
- return false if idx.nil?
1602
-
1603
- target = idx
1604
- pos = idx
1605
- end
1606
-
1607
- if till
1608
- target =
1609
- if direction == :forward
1610
- RuVim::TextMetrics.previous_grapheme_char_index(line, target)
1611
- else
1612
- RuVim::TextMetrics.next_grapheme_char_index(line, target)
1613
- end
1614
- end
1615
-
1616
- win.cursor_x = target
1617
- win.clamp_to_buffer(buf)
1618
- true
1619
- end
1620
-
1621
- def rindex_from(line, char, pos)
1622
- return nil if pos.negative?
1623
-
1624
- line.rindex(char, pos)
1625
- end
1626
-
1627
- def submit_search(line, direction:)
1628
- inv = CommandInvocation.new(id: "__search_submit__", argv: [line], kwargs: { pattern: line, direction: direction })
1629
- ctx = Context.new(editor: @editor, invocation: inv)
1630
- GlobalCommands.instance.submit_search(ctx, pattern: line, direction: direction)
1631
- @editor.enter_normal_mode
1632
- rescue StandardError => e
1633
- @editor.echo_error("Error: #{e.message}")
1634
- @editor.enter_normal_mode
1635
- end
1636
-
1637
- def push_command_line_history(prefix, line)
1638
- text = line.to_s
1639
- return if text.empty?
1640
-
1641
- hist = @cmdline_history[prefix]
1642
- hist.delete(text)
1643
- hist << text
1644
- hist.shift while hist.length > 100
1645
- @cmdline_history_index = nil
1646
- end
1647
-
1648
- def load_command_line_history!
1649
- path = command_line_history_file_path
1650
- return unless path
1651
- return unless File.file?(path)
1652
-
1653
- raw = File.read(path)
1654
- data = JSON.parse(raw)
1655
- return unless data.is_a?(Hash)
1656
-
1657
- loaded = Hash.new { |h, k| h[k] = [] }
1658
- data.each do |prefix, items|
1659
- key = prefix.to_s
1660
- next unless [":", "/", "?"].include?(key)
1661
- next unless items.is_a?(Array)
1662
-
1663
- hist = loaded[key]
1664
- items.each do |item|
1665
- text = item.to_s
1666
- next if text.empty?
1667
-
1668
- hist.delete(text)
1669
- hist << text
1670
- end
1671
- hist.shift while hist.length > 100
1672
- end
1673
- @cmdline_history = loaded
1674
- rescue StandardError => e
1675
- verbose_log(1, "history load error: #{e.message}")
1676
- end
1677
-
1678
- def save_command_line_history!
1679
- path = command_line_history_file_path
1680
- return unless path
1681
-
1682
- payload = {
1683
- ":" => Array(@cmdline_history[":"]).map(&:to_s).last(100),
1684
- "/" => Array(@cmdline_history["/"]).map(&:to_s).last(100),
1685
- "?" => Array(@cmdline_history["?"]).map(&:to_s).last(100)
1686
- }
1687
-
1688
- FileUtils.mkdir_p(File.dirname(path))
1689
- tmp = "#{path}.tmp"
1690
- File.write(tmp, JSON.pretty_generate(payload) + "\n")
1691
- File.rename(tmp, path)
1692
- rescue StandardError => e
1693
- verbose_log(1, "history save error: #{e.message}")
1694
- end
1695
-
1696
- def command_line_history_file_path
1697
- xdg_state_home = ENV["XDG_STATE_HOME"].to_s
1698
- if !xdg_state_home.empty?
1699
- return File.join(xdg_state_home, "ruvim", "history.json")
1700
- end
1701
-
1702
- home = ENV["HOME"].to_s
1703
- return nil if home.empty?
1704
-
1705
- File.join(home, ".ruvim", "history.json")
1706
- end
1707
-
1708
- def command_line_history_move(delta)
1709
- cmd = @editor.command_line
1710
- hist = @cmdline_history[cmd.prefix]
1711
- return if hist.empty?
1712
-
1713
- @cmdline_history_index =
1714
- if @cmdline_history_index.nil?
1715
- delta.negative? ? hist.length - 1 : hist.length
1716
- else
1717
- @cmdline_history_index + delta
1718
- end
1719
-
1720
- @cmdline_history_index = [[@cmdline_history_index, 0].max, hist.length].min
1721
- if @cmdline_history_index == hist.length
1722
- cmd.replace_text("")
1723
- else
1724
- cmd.replace_text(hist[@cmdline_history_index])
1725
- end
1726
- update_incsearch_preview_if_needed
1727
- end
1728
-
1729
- def command_line_complete
1730
- cmd = @editor.command_line
1731
- return unless cmd.prefix == ":"
1732
-
1733
- ctx = ex_completion_context(cmd)
1734
- return unless ctx
1735
-
1736
- matches = reusable_command_line_completion_matches(cmd, ctx) || ex_completion_candidates(ctx)
1737
- case matches.length
1738
- when 0
1739
- clear_command_line_completion
1740
- @editor.echo("No completion")
1741
- when 1
1742
- clear_command_line_completion
1743
- cmd.replace_span(ctx[:token_start], ctx[:token_end], matches.first)
1744
- else
1745
- apply_wildmode_completion(cmd, ctx, matches)
1746
- end
1747
- update_incsearch_preview_if_needed
1748
- end
1749
-
1750
- def reusable_command_line_completion_matches(cmd, ctx)
1751
- state = @cmdline_completion
1752
- return nil unless state
1753
- return nil unless state[:prefix] == cmd.prefix
1754
- return nil unless state[:kind] == ctx[:kind]
1755
- return nil unless state[:command] == ctx[:command]
1756
- return nil unless state[:arg_index] == ctx[:arg_index]
1757
- return nil unless state[:token_start] == ctx[:token_start]
1758
-
1759
- before_text = cmd.text[0...ctx[:token_start]].to_s
1760
- after_text = cmd.text[ctx[:token_end]..].to_s
1761
- return nil unless state[:before_text] == before_text
1762
- return nil unless state[:after_text] == after_text
1763
-
1764
- matches = Array(state[:matches]).map(&:to_s)
1765
- return nil if matches.empty?
1766
-
1767
- current_token = cmd.text[ctx[:token_start]...ctx[:token_end]].to_s
1768
- return nil unless current_token.empty? || matches.include?(current_token) || common_prefix(matches).start_with?(current_token) || current_token.start_with?(common_prefix(matches))
1769
-
1770
- matches
1771
- end
1772
-
1773
- def clear_command_line_completion
1774
- @cmdline_completion = nil
1775
- end
1776
-
1777
- def apply_wildmode_completion(cmd, ctx, matches)
1778
- mode_steps = wildmode_steps
1779
- mode_steps = [:full] if mode_steps.empty?
1780
- state = @cmdline_completion
1781
- before_text = cmd.text[0...ctx[:token_start]].to_s
1782
- after_text = cmd.text[ctx[:token_end]..].to_s
1783
- same = state &&
1784
- state[:prefix] == cmd.prefix &&
1785
- state[:kind] == ctx[:kind] &&
1786
- state[:command] == ctx[:command] &&
1787
- state[:arg_index] == ctx[:arg_index] &&
1788
- state[:token_start] == ctx[:token_start] &&
1789
- state[:before_text] == before_text &&
1790
- state[:after_text] == after_text &&
1791
- state[:matches] == matches
1792
- unless same
1793
- state = {
1794
- prefix: cmd.prefix,
1795
- kind: ctx[:kind],
1796
- command: ctx[:command],
1797
- arg_index: ctx[:arg_index],
1798
- token_start: ctx[:token_start],
1799
- before_text: before_text,
1800
- after_text: after_text,
1801
- matches: matches.dup,
1802
- step_index: -1,
1803
- full_index: nil
1804
- }
1805
- end
1806
-
1807
- state[:step_index] += 1
1808
- step = mode_steps[state[:step_index] % mode_steps.length]
1809
- case step
1810
- when :longest
1811
- pref = common_prefix(matches)
1812
- cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
1813
- when :list
1814
- show_command_line_completion_menu(matches, selected: state[:full_index], force: true)
1815
- when :full
1816
- state[:full_index] = state[:full_index] ? (state[:full_index] + 1) % matches.length : 0
1817
- cmd.replace_span(ctx[:token_start], ctx[:token_end], matches[state[:full_index]])
1818
- show_command_line_completion_menu(matches, selected: state[:full_index], force: false)
1819
- else
1820
- pref = common_prefix(matches)
1821
- cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
1822
- end
1823
-
1824
- @cmdline_completion = state
1825
- end
1826
-
1827
- def wildmode_steps
1828
- raw = @editor.effective_option("wildmode").to_s
1829
- return [:full] if raw.empty?
1830
-
1831
- raw.split(",").flat_map do |tok|
1832
- tok.to_s.split(":").map do |part|
1833
- case part.strip.downcase
1834
- when "longest" then :longest
1835
- when "list" then :list
1836
- when "full" then :full
1837
- end
1838
- end
1839
- end.compact
1840
- end
1841
-
1842
- def show_command_line_completion_menu(matches, selected:, force:)
1843
- return unless force || @editor.effective_option("wildmenu")
1844
-
1845
- items = matches.each_with_index.map do |m, i|
1846
- idx = i
1847
- idx == selected ? "[#{m}]" : m
1848
- end
1849
- @editor.echo(compose_command_line_completion_menu(items))
1850
- end
1851
-
1852
- def compose_command_line_completion_menu(items)
1853
- parts = Array(items).map(&:to_s)
1854
- return "" if parts.empty?
1855
-
1856
- width = command_line_completion_menu_width
1857
- width = [width.to_i, 1].max
1858
- out = +""
1859
- shown = 0
1860
-
1861
- parts.each_with_index do |item, idx|
1862
- token = shown.zero? ? item : " #{item}"
1863
- if out.empty? && token.length > width
1864
- out = token[0, width]
1865
- shown = 1
1866
- break
1867
- end
1868
- break if out.length + token.length > width
1869
-
1870
- out << token
1871
- shown = idx + 1
1872
- end
1873
-
1874
- if shown < parts.length
1875
- ellipsis = (out.empty? ? "..." : " ...")
1876
- if out.length + ellipsis.length <= width
1877
- out << ellipsis
1878
- elsif width >= 3
1879
- out = out[0, width - 3] + "..."
1880
- else
1881
- out = "." * width
1882
- end
1883
- end
1884
-
1885
- out
1886
- end
1887
-
1888
- def command_line_completion_menu_width
1889
- return 80 unless defined?(@terminal) && @terminal && @terminal.respond_to?(:winsize)
1890
-
1891
- _rows, cols = @terminal.winsize
1892
- [cols.to_i, 1].max
1893
- rescue StandardError
1894
- 80
1895
- end
1896
-
1897
- def common_prefix(strings)
1898
- return "" if strings.empty?
1899
-
1900
- prefix = strings.first.dup
1901
- strings[1..]&.each do |s|
1902
- while !prefix.empty? && !s.start_with?(prefix)
1903
- prefix = prefix[0...-1]
1904
- end
1905
- end
1906
- prefix
1907
- end
1908
-
1909
- def clear_insert_completion
1910
- @insert_completion = nil
1911
- end
1912
-
1913
- def insert_tab_in_insert_mode
1914
- buf = @editor.current_buffer
1915
- win = @editor.current_window
1916
- if @editor.effective_option("expandtab", window: win, buffer: buf)
1917
- width = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
1918
- width = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if width <= 0
1919
- width = 2 if width <= 0
1920
- line = buf.line_at(win.cursor_y)
1921
- current_col = RuVim::TextMetrics.screen_col_for_char_index(line, win.cursor_x, tabstop: effective_tabstop(win, buf))
1922
- spaces = width - (current_col % width)
1923
- spaces = width if spaces <= 0
1924
- _y, x = buf.insert_text(win.cursor_y, win.cursor_x, " " * spaces)
1925
- win.cursor_x = x
1926
- else
1927
- buf.insert_char(win.cursor_y, win.cursor_x, "\t")
1928
- win.cursor_x += 1
1929
- end
1930
- end
1931
-
1932
- def apply_insert_autoindent(row, x, previous_row:)
1933
- return x if @paste_batch
1934
- buf = @editor.current_buffer
1935
- win = @editor.current_window
1936
- return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
1937
- return x if previous_row.negative?
1938
-
1939
- prev = buf.line_at(previous_row)
1940
- indent = prev[/\A[ \t]*/].to_s
1941
- if @editor.effective_option("smartindent", window: win, buffer: buf)
1942
- trimmed = prev.rstrip
1943
- needs_indent = trimmed.end_with?("{", "[", "(")
1944
- if !needs_indent
1945
- needs_indent = buf.lang_module.indent_trigger?(trimmed)
1946
- end
1947
- if needs_indent
1948
- sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
1949
- sw = effective_tabstop(win, buf) if sw <= 0
1950
- sw = 2 if sw <= 0
1951
- indent += " " * sw
1952
- end
1953
- end
1954
- return x if indent.empty?
1955
-
1956
- _y, new_x = buf.insert_text(row, x, indent)
1957
- new_x
1958
- end
1959
-
1960
- def maybe_showmatch_after_insert(key)
1961
- return unless [")", "]", "}"].include?(key)
1962
- return unless @editor.effective_option("showmatch")
1963
-
1964
- mt = @editor.effective_option("matchtime").to_i
1965
- mt = 5 if mt <= 0
1966
- @editor.echo_temporary("match", duration_seconds: mt * 0.1)
1967
- end
1968
-
1969
- def maybe_dedent_after_insert(key)
1970
- return unless @editor.effective_option("smartindent", window: @editor.current_window, buffer: @editor.current_buffer)
1971
-
1972
- buf = @editor.current_buffer
1973
- lang_mod = buf.lang_module
1974
-
1975
- pattern = lang_mod.dedent_trigger(key)
1976
- return unless pattern
1977
-
1978
- row = @editor.current_window.cursor_y
1979
- line = buf.line_at(row)
1980
- m = line.match(pattern)
1981
- return unless m
1982
-
1983
- sw = @editor.effective_option("shiftwidth", buffer: buf).to_i
1984
- sw = 2 if sw <= 0
1985
- target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
1986
- return unless target_indent
1987
-
1988
- current_indent = m[1].length
1989
- return if current_indent == target_indent
1990
-
1991
- stripped = line.to_s.strip
1992
- buf.delete_span(row, 0, row, current_indent) if current_indent > 0
1993
- buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
1994
- @editor.current_window.cursor_x = target_indent + stripped.length
1995
- end
1996
-
1997
- def clear_expired_transient_message_if_any
1998
- @needs_redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
1999
- end
2000
-
2001
- def effective_tabstop(window = @editor.current_window, buffer = @editor.current_buffer)
2002
- v = @editor.effective_option("tabstop", window:, buffer:).to_i
2003
- v.positive? ? v : 2
2004
- end
2005
-
2006
- def insert_complete(direction)
2007
- state = ensure_insert_completion_state
2008
- return unless state
2009
-
2010
- matches = state[:matches]
2011
- if matches.empty?
2012
- @editor.echo("No completion")
2013
- return
2014
- end
2015
-
2016
- if state[:index].nil? && insert_completion_noselect? && matches.length > 1
2017
- show_insert_completion_menu(matches, selected: nil)
2018
- state[:index] = :pending_select
2019
- return
2020
- end
2021
-
2022
- if state[:index].nil? && insert_completion_noinsert?
2023
- preview_idx = direction.positive? ? 0 : matches.length - 1
2024
- state[:index] = :pending_insert
2025
- state[:pending_index] = preview_idx
2026
- show_insert_completion_menu(matches, selected: preview_idx, current: matches[preview_idx])
2027
- return
2028
- end
2029
-
2030
- idx = state[:index]
2031
- idx = nil if idx == :pending_select
2032
- if idx == :pending_insert
2033
- idx = state.delete(:pending_index) || (direction.positive? ? 0 : matches.length - 1)
2034
- else
2035
- idx = idx.nil? ? (direction.positive? ? 0 : matches.length - 1) : (idx + direction) % matches.length
2036
- end
2037
- replacement = matches[idx]
2038
-
2039
- end_col = state[:current_end_col]
2040
- start_col = state[:start_col]
2041
- @editor.current_buffer.delete_span(state[:row], start_col, state[:row], end_col)
2042
- _y, new_x = @editor.current_buffer.insert_text(state[:row], start_col, replacement)
2043
- @editor.current_window.cursor_y = state[:row]
2044
- @editor.current_window.cursor_x = new_x
2045
- state[:index] = idx
2046
- state[:current_end_col] = start_col + replacement.length
2047
- if matches.length == 1
2048
- @editor.echo(replacement)
2049
- else
2050
- show_insert_completion_menu(matches, selected: idx, current: replacement)
2051
- end
2052
- rescue StandardError => e
2053
- @editor.echo_error("Completion error: #{e.message}")
2054
- clear_insert_completion
2055
- end
2056
-
2057
- def insert_completion_noselect?
2058
- @editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noselect")
2059
- end
2060
-
2061
- def insert_completion_noinsert?
2062
- @editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noinsert")
2063
- end
2064
-
2065
- def insert_completion_menu_enabled?
2066
- opts = @editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }
2067
- opts.include?("menu") || opts.include?("menuone")
2068
- end
2069
-
2070
- def show_insert_completion_menu(matches, selected:, current: nil)
2071
- if insert_completion_menu_enabled?
2072
- limit = [@editor.effective_option("pumheight").to_i, 1].max
2073
- items = matches.first(limit).each_with_index.map do |m, i|
2074
- i == selected ? "[#{m}]" : m
2075
- end
2076
- items << "..." if matches.length > limit
2077
- if current
2078
- @editor.echo("#{current} (#{selected + 1}/#{matches.length}) | #{items.join(' ')}")
2079
- else
2080
- @editor.echo(items.join(" "))
2081
- end
2082
- elsif current
2083
- @editor.echo("#{current} (#{selected + 1}/#{matches.length})")
2084
- end
2085
- end
2086
-
2087
- def ensure_insert_completion_state
2088
- row = @editor.current_window.cursor_y
2089
- col = @editor.current_window.cursor_x
2090
- line = @editor.current_buffer.line_at(row)
2091
- prefix = trailing_keyword_fragment(line[0...col].to_s, @editor.current_window, @editor.current_buffer)
2092
- return nil if prefix.nil? || prefix.empty?
2093
-
2094
- start_col = col - prefix.length
2095
- current_token = line[start_col...col].to_s
2096
- state = @insert_completion
2097
-
2098
- if state &&
2099
- state[:row] == row &&
2100
- state[:start_col] == start_col &&
2101
- state[:prefix] == prefix &&
2102
- col == state[:current_end_col]
2103
- return state
2104
- end
2105
-
2106
- matches = collect_buffer_word_completions(prefix, current_word: current_token)
2107
- @insert_completion = {
2108
- row: row,
2109
- start_col: start_col,
2110
- prefix: prefix,
2111
- matches: matches,
2112
- index: nil,
2113
- current_end_col: col
2114
- }
2115
- end
2116
-
2117
- def collect_buffer_word_completions(prefix, current_word:)
2118
- words = []
2119
- seen = {}
2120
- rx = keyword_scan_regex(@editor.current_window, @editor.current_buffer)
2121
- @editor.buffers.values.each do |buf|
2122
- buf.lines.each do |line|
2123
- line.scan(rx) do |w|
2124
- next unless w.start_with?(prefix)
2125
- next if w == current_word
2126
- next if seen[w]
2127
-
2128
- seen[w] = true
2129
- words << w
2130
- end
2131
- end
2132
- end
2133
- words.sort
2134
- end
2135
-
2136
- def track_mode_transition(mode_before)
2137
- mode_after = @editor.mode
2138
- if mode_before != :insert && mode_after == :insert
2139
- @insert_start_location = @editor.current_location
2140
- elsif mode_before == :insert && mode_after != :insert
2141
- @insert_start_location = nil
2142
- end
2143
-
2144
- if mode_before != :command_line && mode_after == :command_line
2145
- @incsearch_preview = nil
2146
- elsif mode_before == :command_line && mode_after != :command_line
2147
- @incsearch_preview = nil
2148
- end
2149
- end
2150
-
2151
- def insert_backspace_allowed?
2152
- buf = @editor.current_buffer
2153
- win = @editor.current_window
2154
- row = win.cursor_y
2155
- col = win.cursor_x
2156
- return false if row.zero? && col.zero?
2157
-
2158
- opt = @editor.effective_option("backspace", window: win, buffer: buf).to_s
2159
- allow = opt.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
2160
- allow_all = allow.include?("2")
2161
- allow_indent = allow_all || allow.include?("indent")
2162
-
2163
- if col.zero? && row.positive?
2164
- return true if allow_all || allow.include?("eol")
2165
-
2166
- @editor.echo_error("backspace=eol required")
2167
- return false
2168
- end
2169
-
2170
- if @insert_start_location
2171
- same_buf = @insert_start_location[:buffer_id] == buf.id
2172
- if same_buf && (row < @insert_start_location[:row] || (row == @insert_start_location[:row] && col <= @insert_start_location[:col]))
2173
- if allow_all || allow.include?("start")
2174
- return true
2175
- end
2176
-
2177
- if allow_indent && same_row_autoindent_backspace?(buf, row, col)
2178
- return true
2179
- end
2180
-
2181
- @editor.echo_error("backspace=start required")
2182
- return false
2183
- end
2184
- end
2185
-
2186
- true
2187
- end
2188
-
2189
- def insert_backspace_in_insert_mode
2190
- buf = @editor.current_buffer
2191
- win = @editor.current_window
2192
- row = win.cursor_y
2193
- col = win.cursor_x
2194
-
2195
- if row >= 0 && col.positive? && try_softtabstop_backspace(buf, win)
2196
- return
2197
- end
2198
-
2199
- y, x = buf.backspace(row, col)
2200
- win.cursor_y = y
2201
- win.cursor_x = x
2202
- end
2203
-
2204
- def dispatch_insert_cursor_motion(id)
2205
- @dispatcher.dispatch(@editor, CommandInvocation.new(id: id, count: 1))
2206
- rescue StandardError => e
2207
- @editor.echo_error("Motion error: #{e.message}")
2208
- end
2209
-
2210
- def try_softtabstop_backspace(buf, win)
2211
- row = win.cursor_y
2212
- col = win.cursor_x
2213
- line = buf.line_at(row)
2214
- return false unless line
2215
- return false unless @editor.effective_option("expandtab", window: win, buffer: buf)
2216
-
2217
- sts = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
2218
- sts = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if sts <= 0
2219
- return false if sts <= 0
2220
-
2221
- prefix = line[0...col].to_s
2222
- m = prefix.match(/ +\z/)
2223
- return false unless m
2224
-
2225
- run = m[0].length
2226
- return false if run <= 1
2227
-
2228
- tabstop = effective_tabstop(win, buf)
2229
- cur_screen = RuVim::TextMetrics.screen_col_for_char_index(line, col, tabstop:)
2230
- target_screen = [cur_screen - sts, 0].max
2231
- target_col = RuVim::TextMetrics.char_index_for_screen_col(line, target_screen, tabstop:, align: :floor)
2232
- delete_cols = col - target_col
2233
- delete_cols = [delete_cols, run, sts].min
2234
- return false if delete_cols <= 1
2235
-
2236
- # Only collapse whitespace run; if target lands before the run, clamp to run start.
2237
- run_start = col - run
2238
- target_col = [target_col, run_start].max
2239
- delete_cols = col - target_col
2240
- return false if delete_cols <= 1
2241
-
2242
- buf.delete_span(row, target_col, row, col)
2243
- win.cursor_x = target_col
2244
- true
2245
- rescue StandardError
2246
- false
2247
- end
2248
-
2249
- def same_row_autoindent_backspace?(buf, row, col)
2250
- return false unless @insert_start_location
2251
- return false unless row == @insert_start_location[:row]
2252
- return false unless col <= @insert_start_location[:col]
2253
-
2254
- line = buf.line_at(row)
2255
- line[0...@insert_start_location[:col]].to_s.match?(/\A[ \t]*\z/)
2256
- rescue StandardError
2257
- false
2258
- end
2259
-
2260
- def incsearch_enabled?
2261
- return false unless @editor.command_line_active?
2262
- return false unless ["/", "?"].include?(@editor.command_line.prefix)
2263
-
2264
- !!@editor.effective_option("incsearch")
2265
- end
2266
-
2267
- def update_incsearch_preview_if_needed
2268
- return unless incsearch_enabled?
2269
-
2270
- cmd = @editor.command_line
2271
- ensure_incsearch_preview_origin!(direction: (cmd.prefix == "/" ? :forward : :backward))
2272
- pattern = cmd.text.to_s
2273
- if pattern.empty?
2274
- clear_incsearch_preview_state(apply: false)
2275
- return
2276
- end
2277
-
2278
- buf = @editor.current_buffer
2279
- win = @editor.current_window
2280
- origin = @incsearch_preview[:origin]
2281
- tmp_window = RuVim::Window.new(id: -1, buffer_id: buf.id)
2282
- tmp_window.cursor_y = origin[:row]
2283
- tmp_window.cursor_x = origin[:col]
2284
- regex = GlobalCommands.instance.send(:compile_search_regex, pattern, editor: @editor, window: win, buffer: buf)
2285
- match = GlobalCommands.instance.send(:find_next_match, buf, tmp_window, regex, direction: @incsearch_preview[:direction])
2286
- if match
2287
- win.cursor_y = match[:row]
2288
- win.cursor_x = match[:col]
2289
- win.clamp_to_buffer(buf)
2290
- end
2291
- @incsearch_preview[:active] = true
2292
- rescue RuVim::CommandError, RegexpError
2293
- # Keep editing command-line without forcing an error flash on every keystroke.
2294
- end
2295
-
2296
- def ensure_incsearch_preview_origin!(direction:)
2297
- return if @incsearch_preview
2298
-
2299
- @incsearch_preview = {
2300
- origin: @editor.current_location,
2301
- direction: direction,
2302
- active: false
2303
- }
2304
- end
2305
-
2306
- def cancel_incsearch_preview_if_any
2307
- clear_incsearch_preview_state(apply: false)
2308
- end
2309
-
2310
- def clear_incsearch_preview_state(apply:)
2311
- return unless @incsearch_preview
2312
-
2313
- if !apply && @incsearch_preview[:origin]
2314
- @editor.jump_to_location(@incsearch_preview[:origin])
2315
- end
2316
- @incsearch_preview = nil
2317
- end
2318
-
2319
- def trailing_keyword_fragment(prefix_text, window, buffer)
2320
- cls = keyword_char_class(window, buffer)
2321
- prefix_text.to_s[/[#{cls}]+\z/]
2322
- rescue RegexpError
2323
- prefix_text.to_s[/[[:alnum:]_]+\z/]
2324
- end
2325
-
2326
- def keyword_scan_regex(window, buffer)
2327
- cls = keyword_char_class(window, buffer)
2328
- /[#{cls}]+/
2329
- rescue RegexpError
2330
- /[[:alnum:]_]+/
2331
- end
2332
-
2333
- def keyword_char_class(window, buffer)
2334
- raw = @editor.effective_option("iskeyword", window:, buffer:).to_s
2335
- RuVim::KeywordChars.char_class(raw)
2336
- rescue StandardError
2337
- "[:alnum:]_"
2338
- end
2339
-
2340
- def ex_completion_context(cmd)
2341
- text = cmd.text
2342
- cursor = cmd.cursor
2343
- token_start = token_start_index(text, cursor)
2344
- token_end = token_end_index(text, cursor)
2345
- prefix = text[token_start...cursor].to_s
2346
- before = text[0...token_start].to_s
2347
- argv_before = before.split(/\s+/).reject(&:empty?)
2348
-
2349
- if argv_before.empty?
2350
- {
2351
- kind: :command,
2352
- token_start: token_start,
2353
- token_end: token_end,
2354
- prefix: prefix
2355
- }
2356
- else
2357
- {
2358
- kind: :arg,
2359
- command: argv_before.first,
2360
- arg_index: argv_before.length - 1,
2361
- token_start: token_start,
2362
- token_end: token_end,
2363
- prefix: prefix
2364
- }
2365
- end
2366
- end
2367
-
2368
- def ex_completion_candidates(ctx)
2369
- case ctx[:kind]
2370
- when :command
2371
- ExCommandRegistry.instance.all.flat_map { |spec| [spec.name, *spec.aliases] }.uniq.sort.select { |n| n.start_with?(ctx[:prefix]) }
2372
- when :arg
2373
- ex_arg_completion_candidates(ctx[:command], ctx[:arg_index], ctx[:prefix])
2374
- else
2375
- []
2376
- end
2377
- end
2378
-
2379
- def ex_arg_completion_candidates(command_name, arg_index, prefix)
2380
- cmd = command_name.to_s
2381
- return [] unless arg_index.zero?
2382
-
2383
- if %w[e edit w write tabnew].include?(cmd)
2384
- return path_completion_candidates(prefix)
2385
- end
2386
-
2387
- if %w[buffer b].include?(cmd)
2388
- return buffer_completion_candidates(prefix)
2389
- end
2390
-
2391
- if %w[set setlocal setglobal].include?(cmd)
2392
- return option_completion_candidates(prefix)
2393
- end
2394
-
2395
- if cmd == "git"
2396
- return Git::Handler::GIT_SUBCOMMANDS.keys.sort.select { |s| s.start_with?(prefix) }
2397
- end
2398
-
2399
- []
2400
- end
2401
-
2402
- def path_completion_candidates(prefix)
2403
- input = prefix.to_s
2404
- base_dir =
2405
- if input.empty?
2406
- "."
2407
- elsif input.end_with?("/")
2408
- input
2409
- else
2410
- File.dirname(input)
2411
- end
2412
- partial = input.end_with?("/") ? "" : File.basename(input)
2413
- pattern =
2414
- if input.empty?
2415
- "*"
2416
- elsif base_dir == "."
2417
- "#{partial}*"
2418
- else
2419
- File.join(base_dir, "#{partial}*")
2420
- end
2421
- partial_starts_with_dot = partial.start_with?(".")
2422
- entries = Dir.glob(pattern, File::FNM_DOTMATCH).filter_map do |p|
2423
- next if [".", ".."].include?(File.basename(p))
2424
- next unless p.start_with?(input) || input.empty?
2425
- next if wildignore_path?(p)
2426
- File.directory?(p) ? "#{p}/" : p
2427
- end
2428
- entries.sort_by do |p|
2429
- base = File.basename(p.to_s.sub(%r{/\z}, ""))
2430
- hidden_rank = (!partial_starts_with_dot && base.start_with?(".")) ? 1 : 0
2431
- [hidden_rank, p]
2432
- end
2433
- rescue StandardError
2434
- []
2435
- end
2436
-
2437
- def wildignore_path?(path)
2438
- spec = @editor.global_options["wildignore"].to_s
2439
- return false if spec.empty?
2440
-
2441
- flags = @editor.global_options["wildignorecase"] ? File::FNM_CASEFOLD : 0
2442
- name = path.to_s
2443
- base = File.basename(name)
2444
- spec.split(",").map(&:strip).reject(&:empty?).any? do |pat|
2445
- File.fnmatch?(pat, name, flags) || File.fnmatch?(pat, base, flags)
2446
- end
2447
- rescue StandardError
2448
- false
2449
- end
2450
-
2451
- def buffer_completion_candidates(prefix)
2452
- pfx = prefix.to_s
2453
- items = @editor.buffers.values.flat_map do |b|
2454
- path = b.path.to_s
2455
- base = path.empty? ? nil : File.basename(path)
2456
- [b.id.to_s, path, base].compact
2457
- end.uniq.sort
2458
- items.select { |s| s.start_with?(pfx) }
2459
- end
2460
-
2461
- def option_completion_candidates(prefix)
2462
- pfx = prefix.to_s
2463
- names = RuVim::Editor::OPTION_DEFS.keys
2464
- tokens = names + names.map { |n| "no#{n}" } + names.map { |n| "inv#{n}" } + names.map { |n| "#{n}?" }
2465
- tokens.uniq.sort.select { |s| s.start_with?(pfx) }
2466
- end
2467
-
2468
- def token_start_index(text, cursor)
2469
- i = [[cursor, 0].max, text.length].min
2470
- i -= 1 while i.positive? && !whitespace_char?(text[i - 1])
2471
- i
2472
- end
2473
-
2474
- def token_end_index(text, cursor)
2475
- i = [[cursor, 0].max, text.length].min
2476
- i += 1 while i < text.length && !whitespace_char?(text[i])
2477
- i
2478
- end
2479
-
2480
- def whitespace_char?(ch)
2481
- ch && ch.match?(/\s/)
2482
- end
2483
-
2484
- def install_signal_handlers
2485
- Signal.trap("WINCH") do
2486
- @screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
2487
- @needs_redraw = true
2488
- notify_signal_wakeup
2489
- end
2490
- rescue ArgumentError
2491
- nil
2492
- end
2493
-
2494
- def init_config_loader!
2495
- @config_loader = ConfigLoader.new(
2496
- command_registry: CommandRegistry.instance,
2497
- ex_registry: ExCommandRegistry.instance,
2498
- keymaps: @keymaps,
2499
- command_host: GlobalCommands.instance
2500
- )
2501
- end
2502
-
2503
- def load_user_config!
2504
- return if @clean_mode || @restricted_mode
2505
- return if @skip_user_config
2506
-
2507
- if @config_path
2508
- @config_loader.load_file(@config_path)
2509
- else
2510
- @config_loader.load_default!
2511
- end
2512
- rescue StandardError => e
2513
- @editor.echo_error("config error: #{e.message}")
2514
- end
2515
-
2516
- def load_current_ftplugin!
2517
- return if @clean_mode || @restricted_mode
2518
- return unless @config_loader
2519
-
2520
- @config_loader.load_ftplugin!(@editor, @editor.current_buffer)
2521
- rescue StandardError => e
2522
- @editor.echo_error("ftplugin error: #{e.message}")
2523
- end
2524
-
2525
- def run_startup_action!(action, log_prefix: "startup")
2526
- case action[:type]
2527
- when :ex
2528
- verbose_log(2, "#{log_prefix} ex: #{action[:value]}")
2529
- @dispatcher.dispatch_ex(@editor, action[:value].to_s)
2530
- when :line
2531
- verbose_log(2, "#{log_prefix} line: #{action[:value]}")
2532
- move_cursor_to_line(action[:value].to_i)
2533
- when :line_end
2534
- verbose_log(2, "#{log_prefix} line_end")
2535
- move_cursor_to_line(@editor.current_buffer.line_count)
2536
- end
2537
- end
2538
-
2539
- def verbose_log(level, message)
2540
- return if @verbose_level.to_i < level.to_i
2541
- return unless @verbose_io
2542
-
2543
- @verbose_io.puts("[ruvim:v#{@verbose_level}] #{message}")
2544
- @verbose_io.flush if @verbose_io.respond_to?(:flush)
2545
- rescue StandardError
2546
- nil
2547
- end
2548
-
2549
- def startup_mark(label)
2550
- return unless @startup_time_path
2551
-
2552
- @startup_timeline << [label.to_s, monotonic_now]
2553
- end
2554
-
2555
- def write_startuptime_log!
2556
- return unless @startup_time_path
2557
-
2558
- prev = @startup_time_origin
2559
- lines = @startup_timeline.map do |label, t|
2560
- total_ms = ((t - @startup_time_origin) * 1000.0)
2561
- delta_ms = ((t - prev) * 1000.0)
2562
- prev = t
2563
- format("%9.3f %9.3f %s", total_ms, delta_ms, label)
2564
- end
2565
- File.write(@startup_time_path, lines.join("\n") + "\n")
2566
- rescue StandardError => e
2567
- verbose_log(1, "startuptime write error: #{e.message}")
2568
- end
2569
-
2570
- def monotonic_now
2571
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
2572
- rescue StandardError
2573
- Time.now.to_f
2574
- end
2575
-
2576
- def apply_startup_readonly!
2577
- buf = @editor.current_buffer
2578
- return unless buf&.file_buffer?
2579
-
2580
- buf.readonly = true
2581
- @editor.echo("readonly: #{buf.display_name}")
2582
- end
2583
-
2584
- def apply_startup_follow!
2585
- buf = @editor.current_buffer
2586
- return unless buf&.file_buffer?
2587
- return if @follow_watchers[buf.id]
2588
-
2589
- win = @editor.current_window
2590
- win.cursor_y = buf.line_count - 1
2591
- win.clamp_to_buffer(buf)
2592
- start_follow!(buf)
2593
- end
2594
-
2595
- def apply_startup_nomodifiable!
2596
- buf = @editor.current_buffer
2597
- return unless buf&.file_buffer?
2598
-
2599
- buf.modifiable = false
2600
- buf.readonly = true
2601
- @editor.echo("nomodifiable: #{buf.display_name}")
2602
- end
2603
-
2604
- def apply_startup_compat_mode_messages!
2605
- if @startup_diff_mode
2606
- verbose_log(1, "startup: -d requested (diff mode placeholder)")
2607
- @editor.echo("diff mode (-d) is not implemented yet")
2608
- end
2609
-
2610
- if @startup_quickfix_errorfile
2611
- verbose_log(1, "startup: -q #{@startup_quickfix_errorfile} requested (quickfix placeholder)")
2612
- @editor.echo("quickfix startup (-q #{@startup_quickfix_errorfile}) is not implemented yet")
2613
- end
2614
-
2615
- if @startup_session_file
2616
- verbose_log(1, "startup: -S #{@startup_session_file} requested (session placeholder)")
2617
- @editor.echo("session startup (-S #{@startup_session_file}) is not implemented yet")
2618
- end
2619
- end
2620
-
2621
- def open_startup_paths!(paths)
2622
- list = Array(paths).compact
2623
- return if list.empty?
2624
-
2625
- # Remove the bootstrap empty buffer and reset the ID counter
2626
- # so the first file gets buffer id 1 (Vim-like behavior).
2627
- evict_bootstrap_buffer!
2628
-
2629
- # Initialize arglist with all paths
2630
- @editor.set_arglist(list)
2631
-
2632
- first, *rest = list
2633
- @editor.open_path(first)
2634
- apply_startup_readonly! if @startup_readonly
2635
- apply_startup_nomodifiable! if @startup_nomodifiable
2636
- apply_startup_follow! if @startup_follow
2637
-
2638
- case @startup_open_layout
2639
- when :horizontal
2640
- first_win_id = @editor.current_window_id
2641
- rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
2642
- @editor.focus_window(first_win_id)
2643
- when :vertical
2644
- first_win_id = @editor.current_window_id
2645
- rest.each { |p| open_path_in_split!(p, layout: :vertical) }
2646
- @editor.focus_window(first_win_id)
2647
- when :tab
2648
- rest.each { |p| open_path_in_tab!(p) }
2649
- @editor.tabnext(-(@editor.tabpage_count - 1))
2650
- else
2651
- # Load remaining files as buffers (Vim-like behavior).
2652
- rest.each do |p|
2653
- buf = @editor.add_buffer_from_file(p)
2654
- start_follow!(buf) if @startup_follow
2655
- end
2656
- end
2657
- end
2658
-
2659
- # Remove the bootstrap empty buffer before opening real files,
2660
- # resetting the buffer ID counter so the first file gets id 1.
2661
- def evict_bootstrap_buffer!
2662
- bid = @editor.buffer_ids.find do |id|
2663
- b = @editor.buffers[id]
2664
- b.path.nil? && !b.modified? && b.line_count <= 1 && b.kind == :file
2665
- end
2666
- return unless bid
2667
-
2668
- @editor.buffers.delete(bid)
2669
- @editor.instance_variable_set(:@next_buffer_id, 1)
2670
- end
2671
-
2672
- def open_path_in_split!(path, layout:)
2673
- @editor.split_current_window(layout:)
2674
- @editor.open_path(path)
2675
- apply_startup_readonly! if @startup_readonly
2676
- apply_startup_nomodifiable! if @startup_nomodifiable
2677
- apply_startup_follow! if @startup_follow
653
+ def open_path_in_split!(path, layout:)
654
+ @editor.split_current_window(layout:)
655
+ @editor.open_path(path)
656
+ apply_startup_buffer_flags!
2678
657
  end
2679
658
 
2680
659
  def open_path_in_tab!(path)
2681
660
  @editor.tabnew(path:)
2682
- apply_startup_readonly! if @startup_readonly
2683
- apply_startup_nomodifiable! if @startup_nomodifiable
2684
- apply_startup_follow! if @startup_follow
2685
- end
2686
-
2687
- def open_path_with_large_file_support(path)
2688
- return @editor.open_path_sync(path) unless should_open_path_async?(path)
2689
- return @editor.open_path_sync(path) unless can_start_async_file_load?
2690
-
2691
- open_path_asynchronously!(path)
2692
- end
2693
-
2694
- def should_open_path_async?(path)
2695
- p = path.to_s
2696
- return false if p.empty?
2697
- return false unless File.file?(p)
2698
-
2699
- File.size(p) >= large_file_async_threshold_bytes
2700
- rescue StandardError
2701
- false
2702
- end
2703
-
2704
- def can_start_async_file_load?
2705
- @async_file_loads.empty?
2706
- end
2707
-
2708
- def large_file_async_threshold_bytes
2709
- raw = ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"]
2710
- n = raw.to_i if raw
2711
- return n if n && n.positive?
2712
-
2713
- LARGE_FILE_ASYNC_THRESHOLD_BYTES
2714
- end
2715
-
2716
- def open_path_asynchronously!(path)
2717
- file_size = File.size(path)
2718
- buf = @editor.add_empty_buffer(path: path)
2719
- @editor.switch_to_buffer(buf.id)
2720
- buf.loading_state = :live
2721
- buf.modified = false
2722
-
2723
- ensure_stream_event_queue!
2724
- io = File.open(path, "rb")
2725
- state = { path: path, io: io, thread: nil, ended_with_newline: false }
2726
- staged_prefix_bytes = async_file_staged_prefix_bytes
2727
- staged_mode = file_size > staged_prefix_bytes
2728
- if staged_mode
2729
- prefix = io.read(staged_prefix_bytes) || "".b
2730
- unless prefix.empty?
2731
- buf.append_stream_text!(Buffer.decode_text(prefix))
2732
- state[:ended_with_newline] = prefix.end_with?("\n")
2733
- end
2734
- end
2735
-
2736
- if io.eof?
2737
- buf.finalize_async_file_load!(ended_with_newline: state[:ended_with_newline])
2738
- buf.loading_state = :closed
2739
- io.close unless io.closed?
2740
- return buf
2741
- end
2742
-
2743
- @async_file_loads[buf.id] = state
2744
- state[:thread] = start_async_file_loader_thread(buf.id, io, bulk_once: staged_mode)
2745
-
2746
- size_mb = file_size.fdiv(1024 * 1024)
2747
- if staged_mode
2748
- @editor.echo(format("\"%s\" loading... (showing first %.0fMB of %.1fMB)", path, staged_prefix_bytes.fdiv(1024 * 1024), size_mb))
2749
- else
2750
- @editor.echo(format("\"%s\" loading... (%.1fMB)", path, size_mb))
2751
- end
2752
- buf
2753
- rescue StandardError
2754
- @async_file_loads.delete(buf.id) if buf
2755
- raise
2756
- end
2757
-
2758
- def async_file_staged_prefix_bytes
2759
- raw = ENV["RUVIM_ASYNC_FILE_PREFIX_BYTES"]
2760
- n = raw.to_i if raw
2761
- return n if n && n.positive?
2762
-
2763
- LARGE_FILE_STAGED_PREFIX_BYTES
2764
- end
2765
-
2766
- def start_async_file_loader_thread(buffer_id, io, bulk_once: false)
2767
- Thread.new do
2768
- if bulk_once
2769
- rest = io.read || "".b
2770
- unless rest.empty?
2771
- @stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: Buffer.decode_text(rest) }
2772
- notify_signal_wakeup
2773
- end
2774
- @stream_event_queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: rest.end_with?("\n") }
2775
- notify_signal_wakeup
2776
- next
2777
- end
2778
-
2779
- ended_with_newline = false
2780
- pending_text = +""
2781
- loop do
2782
- chunk = io.readpartial(ASYNC_FILE_READ_CHUNK_BYTES)
2783
- next if chunk.nil? || chunk.empty?
2784
-
2785
- ended_with_newline = chunk.end_with?("\n")
2786
- pending_text << Buffer.decode_text(chunk)
2787
- next if pending_text.bytesize < ASYNC_FILE_EVENT_FLUSH_BYTES
2788
-
2789
- @stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: pending_text }
2790
- pending_text = +""
2791
- notify_signal_wakeup
2792
- end
2793
- rescue EOFError
2794
- unless pending_text.empty?
2795
- @stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: pending_text }
2796
- notify_signal_wakeup
2797
- end
2798
- @stream_event_queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: ended_with_newline }
2799
- notify_signal_wakeup
2800
- rescue StandardError => e
2801
- @stream_event_queue << { type: :file_error, buffer_id: buffer_id, error: e.message.to_s }
2802
- notify_signal_wakeup
2803
- ensure
2804
- begin
2805
- io.close unless io.closed?
2806
- rescue StandardError
2807
- nil
2808
- end
2809
- end
2810
- end
2811
-
2812
- def prepare_stdin_stream_buffer!
2813
- buf = @editor.current_buffer
2814
- if buf.intro_buffer?
2815
- @editor.materialize_intro_buffer!
2816
- buf = @editor.current_buffer
2817
- end
2818
-
2819
- buf.replace_all_lines!([""])
2820
- buf.configure_special!(kind: :stream, name: "[stdin]", readonly: true, modifiable: false)
2821
- buf.modified = false
2822
- buf.stream_state = :live
2823
- buf.options["filetype"] = "text"
2824
- @stream_stop_requested = false
2825
- ensure_stream_event_queue!
2826
- @stream_buffer_id = buf.id
2827
- move_window_to_stream_end!(@editor.current_window, buf)
2828
- @editor.echo("[stdin] follow")
2829
- end
2830
-
2831
- def stdin_stream_stop_command
2832
- return if stop_stdin_stream!
2833
-
2834
- handle_normal_ctrl_c
2835
- end
2836
-
2837
- def stop_stdin_stream!
2838
- buf = @editor.buffers[@stream_buffer_id]
2839
- return false unless buf&.kind == :stream
2840
- return false unless (buf.stream_state || :live) == :live
2841
-
2842
- @stream_stop_requested = true
2843
- io = @stdin_stream_source
2844
- @stdin_stream_source = nil
2845
- begin
2846
- io.close if io && io.respond_to?(:close) && !(io.respond_to?(:closed?) && io.closed?)
2847
- rescue StandardError
2848
- nil
2849
- end
2850
- if @stream_reader_thread&.alive?
2851
- @stream_reader_thread.kill
2852
- @stream_reader_thread.join(0.05)
2853
- end
2854
- @stream_reader_thread = nil
2855
-
2856
- buf.stream_state = :closed
2857
- @editor.echo("[stdin] closed")
2858
- notify_signal_wakeup
2859
- true
2860
- end
2861
-
2862
- def start_stdin_stream_reader!
2863
- return unless @stdin_stream_source
2864
- ensure_stream_event_queue!
2865
- return if @stream_reader_thread&.alive?
2866
-
2867
- @stream_stop_requested = false
2868
- io = @stdin_stream_source
2869
- @stream_reader_thread = Thread.new do
2870
- loop do
2871
- chunk = io.readpartial(4096)
2872
- next if chunk.nil? || chunk.empty?
2873
-
2874
- @stream_event_queue << { type: :data, data: Buffer.decode_text(chunk) }
2875
- notify_signal_wakeup
2876
- end
2877
- rescue EOFError
2878
- unless @stream_stop_requested
2879
- @stream_event_queue << { type: :eof }
2880
- notify_signal_wakeup
2881
- end
2882
- rescue IOError => e
2883
- unless @stream_stop_requested
2884
- @stream_event_queue << { type: :error, error: e.message.to_s }
2885
- notify_signal_wakeup
2886
- end
2887
- rescue StandardError => e
2888
- unless @stream_stop_requested
2889
- @stream_event_queue << { type: :error, error: e.message.to_s }
2890
- notify_signal_wakeup
2891
- end
2892
- end
2893
- end
2894
-
2895
- def drain_stream_events!
2896
- return false unless @stream_event_queue
2897
-
2898
- changed = false
2899
- loop do
2900
- event = @stream_event_queue.pop(true)
2901
- case event[:type]
2902
- when :data
2903
- changed = apply_stream_chunk!(event[:data]) || changed
2904
- when :eof
2905
- if (buf = @editor.buffers[@stream_buffer_id])
2906
- buf.stream_state = :closed
2907
- end
2908
- @editor.echo("[stdin] EOF")
2909
- changed = true
2910
- when :error
2911
- next if ignore_stream_shutdown_error?(event[:error])
2912
- if (buf = @editor.buffers[@stream_buffer_id])
2913
- buf.stream_state = :error
2914
- end
2915
- @editor.echo_error("[stdin] stream error: #{event[:error]}")
2916
- changed = true
2917
- when :follow_data
2918
- changed = apply_follow_chunk!(event[:buffer_id], event[:data]) || changed
2919
- when :follow_truncated
2920
- if (buf = @editor.buffers[event[:buffer_id]])
2921
- @editor.echo("[follow] file truncated: #{buf.display_name}")
2922
- changed = true
2923
- end
2924
- when :follow_deleted
2925
- if (buf = @editor.buffers[event[:buffer_id]])
2926
- @editor.echo("[follow] file deleted, waiting for re-creation: #{buf.display_name}")
2927
- changed = true
2928
- end
2929
- when :file_data
2930
- changed = apply_async_file_chunk!(event[:buffer_id], event[:data]) || changed
2931
- when :file_eof
2932
- changed = finish_async_file_load!(event[:buffer_id], ended_with_newline: event[:ended_with_newline]) || changed
2933
- when :file_error
2934
- changed = fail_async_file_load!(event[:buffer_id], event[:error]) || changed
2935
- when :git_cmd_data
2936
- changed = apply_git_stream_chunk!(event[:buffer_id], event[:data]) || changed
2937
- when :git_cmd_eof
2938
- changed = finish_git_stream!(event[:buffer_id]) || changed
2939
- when :git_cmd_error
2940
- changed = fail_git_stream!(event[:buffer_id], event[:error]) || changed
2941
- end
2942
- end
2943
- rescue ThreadError
2944
- changed
2945
- end
2946
-
2947
- def apply_stream_chunk!(text)
2948
- return false if text.to_s.empty?
2949
-
2950
- buf = @editor.buffers[@stream_buffer_id]
2951
- return false unless buf
2952
-
2953
- follow_window_ids = @editor.windows.values.filter_map do |win|
2954
- next unless win.buffer_id == buf.id
2955
- next unless stream_window_following_end?(win, buf)
2956
-
2957
- win.id
2958
- end
2959
-
2960
- buf.append_stream_text!(text)
2961
-
2962
- follow_window_ids.each do |win_id|
2963
- win = @editor.windows[win_id]
2964
- move_window_to_stream_end!(win, buf) if win
2965
- end
2966
-
2967
- true
2968
- end
2969
-
2970
- def apply_async_file_chunk!(buffer_id, text)
2971
- return false if text.to_s.empty?
2972
-
2973
- buf = @editor.buffers[buffer_id]
2974
- return false unless buf
2975
-
2976
- buf.append_stream_text!(text)
2977
- true
2978
- end
2979
-
2980
- def finish_async_file_load!(buffer_id, ended_with_newline:)
2981
- @async_file_loads.delete(buffer_id)
2982
- buf = @editor.buffers[buffer_id]
2983
- return false unless buf
2984
-
2985
- buf.finalize_async_file_load!(ended_with_newline: !!ended_with_newline)
2986
- buf.loading_state = :closed
2987
- true
2988
- end
2989
-
2990
- def fail_async_file_load!(buffer_id, error)
2991
- state = @async_file_loads.delete(buffer_id)
2992
- buf = @editor.buffers[buffer_id]
2993
- if buf
2994
- buf.loading_state = :error
2995
- end
2996
- @editor.echo_error("\"#{(state && state[:path]) || (buf && buf.display_name) || buffer_id}\" load error: #{error}")
2997
- true
2998
- end
2999
-
3000
- def stream_window_following_end?(win, buf)
3001
- return false unless win
3002
-
3003
- last_row = buf.line_count - 1
3004
- win.cursor_y >= last_row
3005
- end
3006
-
3007
- def move_window_to_stream_end!(win, buf)
3008
- return unless win && buf
3009
-
3010
- last_row = buf.line_count - 1
3011
- win.cursor_y = last_row
3012
- win.cursor_x = buf.line_length(last_row)
3013
- win.clamp_to_buffer(buf)
3014
- end
3015
-
3016
- def shutdown_stream_reader!
3017
- thread = @stream_reader_thread
3018
- @stream_reader_thread = nil
3019
- @stream_stop_requested = true
3020
- return unless thread
3021
- return unless thread.alive?
3022
-
3023
- thread.kill
3024
- thread.join(0.05)
3025
- rescue StandardError
3026
- nil
3027
- end
3028
-
3029
- def ex_follow_toggle
3030
- buf = @editor.current_buffer
3031
- raise RuVim::CommandError, "No file associated with buffer" unless buf.path
3032
-
3033
- if @follow_watchers[buf.id]
3034
- stop_follow!(buf)
3035
- else
3036
- raise RuVim::CommandError, "Buffer has unsaved changes" if buf.modified?
3037
- start_follow!(buf)
3038
- end
3039
- end
3040
-
3041
- def start_follow!(buf)
3042
- ensure_stream_event_queue!
3043
- if buf.path && File.exist?(buf.path)
3044
- data = File.binread(buf.path)
3045
- if data.end_with?("\n") && buf.lines.last.to_s != ""
3046
- following_wins = @editor.windows.values.select do |w|
3047
- w.buffer_id == buf.id && stream_window_following_end?(w, buf)
3048
- end
3049
- buf.append_stream_text!("\n")
3050
- following_wins.each { |w| move_window_to_stream_end!(w, buf) }
3051
- end
3052
- end
3053
- buffer_id = buf.id
3054
- watcher = FileWatcher.create(buf.path) do |type, data|
3055
- case type
3056
- when :data
3057
- @stream_event_queue << { type: :follow_data, buffer_id: buffer_id, data: data }
3058
- when :truncated
3059
- @stream_event_queue << { type: :follow_truncated, buffer_id: buffer_id }
3060
- when :deleted
3061
- @stream_event_queue << { type: :follow_deleted, buffer_id: buffer_id }
3062
- end
3063
- notify_signal_wakeup
3064
- end
3065
- watcher.start
3066
- @follow_watchers[buf.id] = watcher
3067
- buf.stream_state = :live
3068
- buf.follow_backend = watcher.backend
3069
- @editor.echo("[follow] #{buf.display_name}")
3070
- end
3071
-
3072
- def stop_follow!(buf)
3073
- watcher = @follow_watchers.delete(buf.id)
3074
- watcher&.stop
3075
- # Remove trailing empty line added as sentinel by start_follow!
3076
- if buf.line_count > 1 && buf.lines.last.to_s == ""
3077
- buf.lines.pop
3078
- last = buf.line_count - 1
3079
- @editor.windows.each_value do |win|
3080
- next unless win.buffer_id == buf.id
3081
- win.cursor_y = last if win.cursor_y > last
3082
- end
3083
- end
3084
- buf.stream_state = nil
3085
- buf.follow_backend = nil
3086
- @editor.echo("[follow] stopped")
3087
- end
3088
-
3089
- def apply_follow_chunk!(buffer_id, text)
3090
- return false if text.to_s.empty?
3091
-
3092
- buf = @editor.buffers[buffer_id]
3093
- return false unless buf
3094
-
3095
- follow_window_ids = @editor.windows.values.filter_map do |win|
3096
- next unless win.buffer_id == buf.id
3097
- next unless stream_window_following_end?(win, buf)
3098
-
3099
- win.id
3100
- end
3101
-
3102
- buf.append_stream_text!(text)
3103
-
3104
- follow_window_ids.each do |win_id|
3105
- win = @editor.windows[win_id]
3106
- move_window_to_stream_end!(win, buf) if win
3107
- end
3108
-
3109
- true
3110
- end
3111
-
3112
- def shutdown_follow_watchers!
3113
- watchers = @follow_watchers
3114
- @follow_watchers = {}
3115
- watchers.each_value do |watcher|
3116
- watcher.stop
3117
- rescue StandardError
3118
- nil
3119
- end
3120
- end
3121
-
3122
- def shutdown_async_file_loaders!
3123
- loaders = @async_file_loads
3124
- @async_file_loads = {}
3125
- loaders.each_value do |state|
3126
- io = state[:io]
3127
- thread = state[:thread]
3128
- begin
3129
- io.close if io && !io.closed?
3130
- rescue StandardError
3131
- nil
3132
- end
3133
- next unless thread&.alive?
3134
-
3135
- thread.kill
3136
- thread.join(0.05)
3137
- rescue StandardError
3138
- nil
3139
- end
3140
- end
3141
-
3142
- def shutdown_background_readers!
3143
- shutdown_stream_reader!
3144
- shutdown_follow_watchers!
3145
- shutdown_async_file_loaders!
3146
- end
3147
-
3148
- def ignore_stream_shutdown_error?(message)
3149
- buf = @editor.buffers[@stream_buffer_id]
3150
- return false unless buf&.kind == :stream
3151
- return false unless (buf.stream_state || :live) == :closed
3152
-
3153
- msg = message.to_s.downcase
3154
- msg.include?("stream closed") || msg.include?("closed in another thread")
3155
- end
3156
-
3157
- def ensure_stream_event_queue!
3158
- @stream_event_queue ||= Queue.new
3159
- end
3160
-
3161
- def apply_git_stream_chunk!(buffer_id, text)
3162
- return false if text.to_s.empty?
3163
-
3164
- buf = @editor.buffers[buffer_id]
3165
- return false unless buf
3166
-
3167
- buf.append_stream_text!(text)
3168
- true
3169
- end
3170
-
3171
- def finish_git_stream!(buffer_id)
3172
- @git_stream_ios&.delete(buffer_id)
3173
- @git_stream_threads&.delete(buffer_id)
3174
- buf = @editor.buffers[buffer_id]
3175
- return false unless buf
3176
-
3177
- # Remove trailing empty line if present
3178
- if buf.lines.length > 1 && buf.lines[-1] == ""
3179
- buf.lines.pop
3180
- end
3181
- line_count = buf.line_count
3182
- @editor.echo("#{buf.name} #{line_count} lines")
3183
- true
3184
- end
3185
-
3186
- def fail_git_stream!(buffer_id, error)
3187
- @git_stream_ios&.delete(buffer_id)
3188
- @git_stream_threads&.delete(buffer_id)
3189
- buf = @editor.buffers[buffer_id]
3190
- @editor.echo_error("git stream error: #{error}") if buf
3191
- true
3192
- end
3193
-
3194
- def start_git_stream_command(buffer_id, cmd, root)
3195
- ensure_stream_event_queue!
3196
- @git_stream_ios ||= {}
3197
- @git_stream_threads ||= {}
3198
- queue = @stream_event_queue
3199
- ios = @git_stream_ios
3200
- @git_stream_threads[buffer_id] = Thread.new do
3201
- IO.popen(cmd, chdir: root, err: [:child, :out]) do |io|
3202
- ios[buffer_id] = io
3203
- while (chunk = io.read(4096))
3204
- queue << { type: :git_cmd_data, buffer_id: buffer_id, data: Buffer.decode_text(chunk) }
3205
- notify_signal_wakeup
3206
- end
3207
- end
3208
- ios.delete(buffer_id)
3209
- queue << { type: :git_cmd_eof, buffer_id: buffer_id }
3210
- notify_signal_wakeup
3211
- rescue StandardError => e
3212
- ios.delete(buffer_id)
3213
- queue << { type: :git_cmd_error, buffer_id: buffer_id, error: e.message.to_s }
3214
- notify_signal_wakeup
3215
- end
3216
- end
3217
-
3218
- def stop_git_stream!(buffer_id)
3219
- io = @git_stream_ios&.delete(buffer_id)
3220
- io&.close
3221
- rescue IOError
3222
- # already closed
661
+ apply_startup_buffer_flags!
3223
662
  end
3224
663
 
3225
664
  def move_cursor_to_line(line_number)
@@ -3227,7 +666,7 @@ module RuVim
3227
666
  buf = @editor.current_buffer
3228
667
  return unless win && buf
3229
668
 
3230
- target = [[line_number.to_i - 1, 0].max, buf.line_count - 1].min
669
+ target = [[line_number - 1, 0].max, buf.line_count - 1].min
3231
670
  win.cursor_y = target
3232
671
  win.clamp_to_buffer(buf)
3233
672
  end