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