ruvim 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +53 -4
  3. data/README.md +15 -6
  4. data/Rakefile +7 -0
  5. data/benchmark/cext_compare.rb +165 -0
  6. data/benchmark/chunked_load.rb +256 -0
  7. data/benchmark/file_load.rb +140 -0
  8. data/benchmark/hotspots.rb +178 -0
  9. data/docs/binding.md +3 -2
  10. data/docs/command.md +81 -9
  11. data/docs/done.md +23 -0
  12. data/docs/spec.md +105 -19
  13. data/docs/todo.md +9 -0
  14. data/docs/tutorial.md +9 -1
  15. data/docs/vim_diff.md +13 -0
  16. data/ext/ruvim/extconf.rb +5 -0
  17. data/ext/ruvim/ruvim_ext.c +519 -0
  18. data/lib/ruvim/app.rb +217 -2778
  19. data/lib/ruvim/browser.rb +104 -0
  20. data/lib/ruvim/buffer.rb +39 -28
  21. data/lib/ruvim/command_invocation.rb +2 -2
  22. data/lib/ruvim/completion_manager.rb +708 -0
  23. data/lib/ruvim/dispatcher.rb +14 -8
  24. data/lib/ruvim/display_width.rb +91 -45
  25. data/lib/ruvim/editor.rb +64 -81
  26. data/lib/ruvim/ex_command_registry.rb +3 -1
  27. data/lib/ruvim/gh/link.rb +207 -0
  28. data/lib/ruvim/git/blame.rb +16 -6
  29. data/lib/ruvim/git/branch.rb +20 -5
  30. data/lib/ruvim/git/grep.rb +107 -0
  31. data/lib/ruvim/git/handler.rb +42 -1
  32. data/lib/ruvim/global_commands.rb +175 -35
  33. data/lib/ruvim/highlighter.rb +4 -13
  34. data/lib/ruvim/key_handler.rb +1510 -0
  35. data/lib/ruvim/keymap_manager.rb +7 -7
  36. data/lib/ruvim/lang/base.rb +5 -0
  37. data/lib/ruvim/lang/c.rb +116 -0
  38. data/lib/ruvim/lang/cpp.rb +107 -0
  39. data/lib/ruvim/lang/csv.rb +4 -1
  40. data/lib/ruvim/lang/diff.rb +2 -0
  41. data/lib/ruvim/lang/dockerfile.rb +36 -0
  42. data/lib/ruvim/lang/elixir.rb +85 -0
  43. data/lib/ruvim/lang/erb.rb +30 -0
  44. data/lib/ruvim/lang/go.rb +83 -0
  45. data/lib/ruvim/lang/html.rb +34 -0
  46. data/lib/ruvim/lang/javascript.rb +83 -0
  47. data/lib/ruvim/lang/json.rb +6 -0
  48. data/lib/ruvim/lang/lua.rb +76 -0
  49. data/lib/ruvim/lang/makefile.rb +36 -0
  50. data/lib/ruvim/lang/markdown.rb +3 -4
  51. data/lib/ruvim/lang/ocaml.rb +77 -0
  52. data/lib/ruvim/lang/perl.rb +91 -0
  53. data/lib/ruvim/lang/python.rb +85 -0
  54. data/lib/ruvim/lang/registry.rb +102 -0
  55. data/lib/ruvim/lang/ruby.rb +7 -0
  56. data/lib/ruvim/lang/rust.rb +95 -0
  57. data/lib/ruvim/lang/scheme.rb +5 -0
  58. data/lib/ruvim/lang/sh.rb +76 -0
  59. data/lib/ruvim/lang/sql.rb +52 -0
  60. data/lib/ruvim/lang/toml.rb +36 -0
  61. data/lib/ruvim/lang/tsv.rb +4 -1
  62. data/lib/ruvim/lang/typescript.rb +53 -0
  63. data/lib/ruvim/lang/yaml.rb +62 -0
  64. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  65. data/lib/ruvim/rich_view.rb +14 -7
  66. data/lib/ruvim/screen.rb +126 -72
  67. data/lib/ruvim/stream/file_load.rb +85 -0
  68. data/lib/ruvim/stream/follow.rb +40 -0
  69. data/lib/ruvim/stream/git.rb +43 -0
  70. data/lib/ruvim/stream/run.rb +74 -0
  71. data/lib/ruvim/stream/stdin.rb +55 -0
  72. data/lib/ruvim/stream.rb +35 -0
  73. data/lib/ruvim/stream_mixer.rb +394 -0
  74. data/lib/ruvim/terminal.rb +18 -4
  75. data/lib/ruvim/text_metrics.rb +84 -65
  76. data/lib/ruvim/version.rb +1 -1
  77. data/lib/ruvim/window.rb +5 -5
  78. data/lib/ruvim.rb +23 -6
  79. data/test/app_command_test.rb +382 -0
  80. data/test/app_completion_test.rb +43 -19
  81. data/test/app_dot_repeat_test.rb +27 -3
  82. data/test/app_ex_command_test.rb +154 -0
  83. data/test/app_motion_test.rb +13 -12
  84. data/test/app_register_test.rb +2 -1
  85. data/test/app_scenario_test.rb +15 -10
  86. data/test/app_startup_test.rb +70 -27
  87. data/test/app_text_object_test.rb +2 -1
  88. data/test/app_unicode_behavior_test.rb +3 -2
  89. data/test/browser_test.rb +88 -0
  90. data/test/buffer_test.rb +24 -0
  91. data/test/cli_test.rb +63 -0
  92. data/test/command_invocation_test.rb +33 -0
  93. data/test/config_dsl_test.rb +47 -0
  94. data/test/dispatcher_test.rb +74 -4
  95. data/test/ex_command_registry_test.rb +106 -0
  96. data/test/follow_test.rb +20 -21
  97. data/test/gh_link_test.rb +141 -0
  98. data/test/git_blame_test.rb +96 -17
  99. data/test/git_grep_test.rb +64 -0
  100. data/test/highlighter_test.rb +125 -0
  101. data/test/indent_test.rb +137 -0
  102. data/test/input_screen_integration_test.rb +1 -1
  103. data/test/keyword_chars_test.rb +85 -0
  104. data/test/lang_test.rb +634 -0
  105. data/test/markdown_renderer_test.rb +5 -5
  106. data/test/on_save_hook_test.rb +12 -8
  107. data/test/render_snapshot_test.rb +78 -0
  108. data/test/rich_view_test.rb +42 -42
  109. data/test/run_command_test.rb +307 -0
  110. data/test/screen_test.rb +68 -5
  111. data/test/stream_test.rb +165 -0
  112. data/test/window_test.rb +59 -0
  113. metadata +52 -2
@@ -21,9 +21,9 @@ module RuVim
21
21
  end
22
22
 
23
23
  def dispatch_ex(editor, line)
24
- raw = line.to_s.strip
24
+ raw = line.strip
25
25
  if raw.start_with?("!")
26
- command = raw[1..].to_s.strip
26
+ command = raw[1..].strip
27
27
  invocation = CommandInvocation.new(id: "__shell__", argv: [command])
28
28
  ctx = Context.new(editor:, invocation:)
29
29
  @command_host.ex_shell(ctx, command:)
@@ -52,15 +52,21 @@ module RuVim
52
52
  return if parsed.nil?
53
53
 
54
54
  spec = @ex_registry.fetch(parsed.name)
55
- validate_ex_args!(spec, parsed.argv, parsed.bang)
56
- invocation = CommandInvocation.new(id: spec.name, argv: parsed.argv, bang: parsed.bang)
55
+ argv = parsed.argv
56
+ if spec.raw_args
57
+ # Re-extract raw text after command name (preserving shell quoting)
58
+ raw_rest = rest.strip.sub(/\A\S+\s*/, "")
59
+ argv = raw_rest.empty? ? [] : [raw_rest]
60
+ end
61
+ validate_ex_args!(spec, argv, parsed.bang)
62
+ invocation = CommandInvocation.new(id: spec.name, argv: argv, bang: parsed.bang)
57
63
  ctx = Context.new(editor:, invocation:)
58
64
  range_kwargs = {}
59
65
  if range_result
60
66
  range_kwargs[:range_start] = range_result[:range_start]
61
67
  range_kwargs[:range_end] = range_result[:range_end]
62
68
  end
63
- @command_host.call(spec.call, ctx, argv: parsed.argv, bang: parsed.bang, count: 1, kwargs: range_kwargs)
69
+ @command_host.call(spec.call, ctx, argv: argv, bang: parsed.bang, count: 1, kwargs: range_kwargs)
64
70
  rescue StandardError => e
65
71
  editor.echo_error("Error: #{e.message}")
66
72
  ensure
@@ -68,7 +74,7 @@ module RuVim
68
74
  end
69
75
 
70
76
  def parse_ex(line)
71
- raw = line.to_s.strip
77
+ raw = line.strip
72
78
  return nil if raw.empty?
73
79
 
74
80
  tokens = Shellwords.shellsplit(raw)
@@ -85,7 +91,7 @@ module RuVim
85
91
  # Parse a substitute command: s/pat/repl/flags
86
92
  # Returns {pattern:, replacement:, flags_str:} or nil
87
93
  def parse_substitute(line)
88
- raw = line.to_s.strip
94
+ raw = line.strip
89
95
  return nil unless raw.match?(/\As[^a-zA-Z]/)
90
96
  return nil if raw.length < 2
91
97
 
@@ -189,7 +195,7 @@ module RuVim
189
195
  # Parse a range from the beginning of raw.
190
196
  # Returns {range_start:, range_end:, rest:} or nil.
191
197
  def parse_range(raw, editor)
192
- str = raw.to_s
198
+ str = raw
193
199
  return nil if str.empty?
194
200
 
195
201
  # % = whole file
@@ -1,24 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Try loading the C extension (built by rake compile)
4
+ begin
5
+ require_relative "ruvim_ext"
6
+ rescue LoadError
7
+ # C extension not available — pure Ruby fallback below
8
+ end
9
+
3
10
  module RuVim
4
11
  module DisplayWidth
5
12
  module_function
6
13
 
7
- def cell_width(ch, col: 0, tabstop: 2)
8
- return 1 if ch.nil? || ch.empty?
14
+ if defined?(RuVim::DisplayWidthExt)
15
+ # ---- C extension paths ----
16
+
17
+ def cell_width(ch, col: 0, tabstop: 2)
18
+ sync_ambiguous_width
19
+ DisplayWidthExt.cell_width(ch, col:, tabstop:)
20
+ end
21
+
22
+ def display_width(str, tabstop: 2, start_col: 0)
23
+ sync_ambiguous_width
24
+ DisplayWidthExt.display_width(str, tabstop:, start_col:)
25
+ end
26
+
27
+ def expand_tabs(str, tabstop: 2, start_col: 0)
28
+ sync_ambiguous_width
29
+ DisplayWidthExt.expand_tabs(str, tabstop:, start_col:)
30
+ end
31
+
32
+ def ambiguous_width
33
+ sync_ambiguous_width
34
+ end
35
+
36
+ def sync_ambiguous_width
37
+ env = ::ENV["RUVIM_AMBIGUOUS_WIDTH"]
38
+ if !defined?(@ambiguous_width_cached) || @ambiguous_width_env != env
39
+ @ambiguous_width_env = env
40
+ @ambiguous_width_cached = (env == "2" ? 2 : 1)
41
+ DisplayWidthExt.set_ambiguous_width(@ambiguous_width_cached)
42
+ end
43
+ @ambiguous_width_cached
44
+ end
45
+ else
46
+ # ---- Pure Ruby fallback ----
47
+
48
+ def cell_width(ch, col: 0, tabstop: 2)
49
+ return 1 if ch.nil? || ch.empty?
50
+
51
+ if ch == "\t"
52
+ width = tabstop - (col % tabstop)
53
+ return width.zero? ? tabstop : width
54
+ end
55
+
56
+ # Fast path: byte length 1 means ASCII — always width 1
57
+ return 1 if ch.bytesize == 1
58
+
59
+ code = ch.ord
60
+ return cached_codepoint_width(code) if codepoint_cacheable?(code)
9
61
 
10
- if ch == "\t"
11
- width = tabstop - (col % tabstop)
12
- return width.zero? ? tabstop : width
62
+ uncached_codepoint_width(code)
13
63
  end
14
64
 
15
- code = ch.ord
16
- return 1 if code <= 0xA0 && !code.zero?
17
- return cached_codepoint_width(code) if codepoint_cacheable?(code)
65
+ def display_width(str, tabstop: 2, start_col: 0)
66
+ col = start_col
67
+ str.to_s.each_char { |ch| col += cell_width(ch, col:, tabstop:) }
68
+ col - start_col
69
+ end
70
+
71
+ def expand_tabs(str, tabstop: 2, start_col: 0)
72
+ col = start_col
73
+ out = +""
74
+ str.to_s.each_char do |ch|
75
+ if ch == "\t"
76
+ n = cell_width(ch, col:, tabstop:)
77
+ out << (" " * n)
78
+ col += n
79
+ else
80
+ out << ch
81
+ col += cell_width(ch, col:, tabstop:)
82
+ end
83
+ end
84
+ out
85
+ end
86
+
87
+ def ambiguous_width
88
+ env = ::ENV["RUVIM_AMBIGUOUS_WIDTH"]
89
+ if !defined?(@ambiguous_width_cached) || @ambiguous_width_env != env
90
+ @ambiguous_width_env = env
91
+ @ambiguous_width_cached = (env == "2" ? 2 : 1)
92
+ @codepoint_width_cache = {}
93
+ end
18
94
 
19
- uncached_codepoint_width(code)
95
+ @ambiguous_width_cached
96
+ end
20
97
  end
21
98
 
99
+ # Shared helpers (used by pure Ruby path; kept available for tests)
100
+
22
101
  def codepoint_cacheable?(code)
23
102
  !code.nil? && !code.zero?
24
103
  end
@@ -43,28 +122,6 @@ module RuVim
43
122
  1
44
123
  end
45
124
 
46
- def display_width(str, tabstop: 2, start_col: 0)
47
- col = start_col
48
- str.to_s.each_char { |ch| col += cell_width(ch, col:, tabstop:) }
49
- col - start_col
50
- end
51
-
52
- def expand_tabs(str, tabstop: 2, start_col: 0)
53
- col = start_col
54
- out = +""
55
- str.to_s.each_char do |ch|
56
- if ch == "\t"
57
- n = cell_width(ch, col:, tabstop:)
58
- out << (" " * n)
59
- col += n
60
- else
61
- out << ch
62
- col += cell_width(ch, col:, tabstop:)
63
- end
64
- end
65
- out
66
- end
67
-
68
125
  def combining_mark?(code)
69
126
  (0x0300..0x036F).cover?(code) ||
70
127
  (0x1AB0..0x1AFF).cover?(code) ||
@@ -74,9 +131,9 @@ module RuVim
74
131
  end
75
132
 
76
133
  def zero_width_codepoint?(code)
77
- (0x200D..0x200D).cover?(code) || # ZWJ
78
- (0xFE00..0xFE0F).cover?(code) || # variation selectors
79
- (0xE0100..0xE01EF).cover?(code) # variation selectors supplement
134
+ (0x200D..0x200D).cover?(code) ||
135
+ (0xFE00..0xFE0F).cover?(code) ||
136
+ (0xE0100..0xE01EF).cover?(code)
80
137
  end
81
138
 
82
139
  def wide_codepoint?(code)
@@ -121,16 +178,5 @@ module RuVim
121
178
  (0x2460..0x24E9).cover?(code) ||
122
179
  (0x2500..0x257F).cover?(code)
123
180
  end
124
-
125
- def ambiguous_width
126
- env = ::ENV["RUVIM_AMBIGUOUS_WIDTH"]
127
- if !defined?(@ambiguous_width_cached) || @ambiguous_width_env != env
128
- @ambiguous_width_env = env
129
- @ambiguous_width_cached = (env == "2" ? 2 : 1)
130
- @codepoint_width_cache = {}
131
- end
132
-
133
- @ambiguous_width_cached
134
- end
135
181
  end
136
182
  end
data/lib/ruvim/editor.rb CHANGED
@@ -54,24 +54,12 @@ module RuVim
54
54
  "filetype" => { default_scope: :buffer, type: :string, default: nil },
55
55
  "onsavehook" => { default_scope: :buffer, type: :bool, default: true },
56
56
  "grepprg" => { default_scope: :global, type: :string, default: "grep -nH" },
57
- "grepformat" => { default_scope: :global, type: :string, default: "%f:%l:%m" }
57
+ "grepformat" => { default_scope: :global, type: :string, default: "%f:%l:%m" },
58
+ "runprg" => { default_scope: :buffer, type: :string, default: nil }
58
59
  }.freeze
59
- SHEBANG_FILETYPE_RULES = [
60
- [/\Aruby(?:\d+(?:\.\d+)*)?\z/, "ruby"],
61
- [/\Apython(?:\d+(?:\.\d+)*)?\z/, "python"],
62
- ["node", "javascript"],
63
- ["nodejs", "javascript"],
64
- ["deno", "javascript"],
65
- ["bash", "sh"],
66
- ["sh", "sh"],
67
- ["zsh", "sh"],
68
- ["ksh", "sh"],
69
- ["dash", "sh"],
70
- [/\Aperl(?:\d+(?:\.\d+)*)?\z/, "perl"]
71
- ].freeze
72
60
 
73
61
  attr_reader :buffers, :windows, :layout_tree
74
- attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :restricted_mode, :current_window_view_height_hint, :stdin_stream_stop_handler, :open_path_handler, :keymap_manager, :app_action_handler, :git_stream_handler, :git_stream_stop_handler
62
+ attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :restricted_mode, :current_window_view_height_hint, :open_path_handler, :keymap_manager, :app_action_handler, :git_stream_handler, :git_stream_stop_handler, :shell_executor, :run_stream_handler
75
63
 
76
64
  def initialize
77
65
  @buffers = {}
@@ -93,7 +81,6 @@ module RuVim
93
81
  @restricted_mode = false
94
82
  @current_window_view_height_hint = 1
95
83
  @running = true
96
- @stdin_stream_stop_handler = nil
97
84
  @open_path_handler = nil
98
85
  @keymap_manager = nil
99
86
  @app_action_handler = nil
@@ -117,6 +104,8 @@ module RuVim
117
104
  @arglist = []
118
105
  @arglist_index = 0
119
106
  @hit_enter_lines = nil
107
+ @run_history = {} # buffer_id => last run command (unexpanded)
108
+ @run_output_buffer_id = nil
120
109
  end
121
110
 
122
111
  def running?
@@ -131,6 +120,18 @@ module RuVim
131
120
  @running = false
132
121
  end
133
122
 
123
+ def run_history
124
+ @run_history
125
+ end
126
+
127
+ def run_output_buffer_id
128
+ @run_output_buffer_id
129
+ end
130
+
131
+ def run_output_buffer_id=(id)
132
+ @run_output_buffer_id = id
133
+ end
134
+
134
135
  def command_line
135
136
  @command_line
136
137
  end
@@ -152,7 +153,7 @@ module RuVim
152
153
  end
153
154
 
154
155
  def set_last_search(pattern:, direction:)
155
- @last_search = { pattern: pattern.to_s, direction: direction.to_sym }
156
+ @last_search = { pattern: pattern, direction: direction }
156
157
  @hlsearch_suppressed = false
157
158
  end
158
159
 
@@ -165,7 +166,7 @@ module RuVim
165
166
  end
166
167
 
167
168
  def set_last_find(char:, direction:, till:)
168
- @last_find = { char: char.to_s, direction: direction.to_sym, till: !!till }
169
+ @last_find = { char: char, direction: direction, till: !!till }
169
170
  end
170
171
 
171
172
  def current_window
@@ -176,19 +177,18 @@ module RuVim
176
177
  @buffers.fetch(current_window.buffer_id)
177
178
  end
178
179
 
179
- def stdin_stream_stop_or_cancel!
180
- handler = @stdin_stream_stop_handler
180
+ def stream_stop_or_cancel!
181
+ handler = current_buffer&.stream&.stop_handler
181
182
  return false unless handler
182
183
 
183
184
  handler.call
184
- true
185
185
  end
186
186
 
187
187
  def invoke_app_action(name, **kwargs)
188
188
  handler = @app_action_handler
189
189
  return false unless handler
190
190
 
191
- handler.call(name.to_sym, **kwargs)
191
+ handler.call(name, **kwargs)
192
192
  true
193
193
  end
194
194
 
@@ -213,7 +213,7 @@ module RuVim
213
213
 
214
214
  def get_option(name, scope: :effective, window: current_window, buffer: current_buffer)
215
215
  key = name.to_s
216
- case scope.to_sym
216
+ case scope
217
217
  when :global
218
218
  @global_options[key]
219
219
  when :buffer
@@ -228,7 +228,7 @@ module RuVim
228
228
  def set_option(name, value, scope: :auto, window: current_window, buffer: current_buffer)
229
229
  key = name.to_s
230
230
  value = coerce_option_value(key, value)
231
- actual_scope = (scope.to_sym == :auto ? option_default_scope(key) : scope.to_sym)
231
+ actual_scope = (scope == :auto ? option_default_scope(key) : scope)
232
232
  case actual_scope
233
233
  when :global
234
234
  @global_options[key] = value
@@ -262,34 +262,13 @@ module RuVim
262
262
  return nil if p.empty?
263
263
 
264
264
  base = File.basename(p)
265
- return "ruby" if %w[Gemfile Rakefile Guardfile].include?(base)
266
-
267
- ext_ft = {
268
- ".rb" => "ruby",
269
- ".rake" => "ruby",
270
- ".ru" => "ruby",
271
- ".py" => "python",
272
- ".js" => "javascript",
273
- ".mjs" => "javascript",
274
- ".cjs" => "javascript",
275
- ".ts" => "typescript",
276
- ".tsx" => "typescriptreact",
277
- ".jsx" => "javascriptreact",
278
- ".json" => "json",
279
- ".jsonl" => "jsonl",
280
- ".yml" => "yaml",
281
- ".yaml" => "yaml",
282
- ".md" => "markdown",
283
- ".txt" => "text",
284
- ".html" => "html",
285
- ".css" => "css",
286
- ".sh" => "sh",
287
- ".tsv" => "tsv",
288
- ".csv" => "csv",
289
- ".scm" => "scheme",
290
- ".ss" => "scheme",
291
- ".sld" => "scheme"
292
- }[File.extname(base).downcase]
265
+
266
+ # Basename-based detection (exact match, then prefix)
267
+ base_ft = Lang::Registry.detect_by_basename(base)
268
+ return base_ft if base_ft
269
+
270
+ # Extension-based detection
271
+ ext_ft = Lang::Registry.detect_by_extension(File.extname(base))
293
272
  return ext_ft if ext_ft
294
273
 
295
274
  detect_filetype_from_shebang(p)
@@ -301,9 +280,9 @@ module RuVim
301
280
 
302
281
  def set_register(name = "\"", text:, type: :charwise)
303
282
  key = name.to_s
304
- return { text: text.to_s, type: type.to_sym } if key == "_"
283
+ return { text: text, type: type } if key == "_"
305
284
 
306
- payload = write_register_payload(key, text: text.to_s, type: type.to_sym)
285
+ payload = write_register_payload(key, text: text, type: type)
307
286
  write_clipboard_register(key, payload)
308
287
  if key == "\""
309
288
  if (default_clip = clipboard_default_register_key)
@@ -316,14 +295,14 @@ module RuVim
316
295
  end
317
296
 
318
297
  def store_operator_register(name = "\"", text:, type:, kind:)
319
- key = (name || "\"").to_s
320
- payload = { text: text.to_s, type: type.to_sym }
298
+ key = (name || "\"")
299
+ payload = { text: text, type: type }
321
300
  return payload if key == "_"
322
301
 
323
302
  written = set_register(key, text: payload[:text], type: payload[:type])
324
303
  op_payload = dup_register_payload(written)
325
304
 
326
- case kind.to_sym
305
+ case kind
327
306
  when :yank
328
307
  @registers["0"] = dup_register_payload(op_payload)
329
308
  when :delete, :change
@@ -485,9 +464,9 @@ module RuVim
485
464
  end
486
465
 
487
466
  def enter_visual(mode)
488
- @mode = mode.to_sym
467
+ @mode = mode
489
468
  @visual_state = {
490
- mode: mode.to_sym,
469
+ mode: mode,
491
470
  anchor_y: current_window.cursor_y,
492
471
  anchor_x: current_window.cursor_x
493
472
  }
@@ -645,10 +624,10 @@ module RuVim
645
624
  win.row_offset = src.row_offset
646
625
  win.col_offset = src.col_offset
647
626
 
648
- split_type = (layout.to_sym == :vertical ? :vsplit : :hsplit)
627
+ split_type = (layout == :vertical ? :vsplit : :hsplit)
649
628
  new_leaf = { type: :window, id: win.id }
650
629
 
651
- @layout_tree = tree_split_leaf(@layout_tree, src.id, split_type, new_leaf, place.to_sym)
630
+ @layout_tree = tree_split_leaf(@layout_tree, src.id, split_type, new_leaf, place)
652
631
 
653
632
  @current_window_id = win.id
654
633
  save_current_tabpage_state! unless @suspend_tab_autosave
@@ -849,8 +828,7 @@ module RuVim
849
828
  end
850
829
 
851
830
  def delete_buffer(buffer_id)
852
- id = buffer_id.to_i
853
- buffer = @buffers[id]
831
+ buffer = @buffers[buffer_id]
854
832
  return nil unless buffer
855
833
 
856
834
  if @buffers.length <= 1
@@ -863,13 +841,13 @@ module RuVim
863
841
  if replacement
864
842
  replacement.id
865
843
  else
866
- candidates = @buffers.keys.reject { |bid| bid == id }
844
+ candidates = @buffers.keys.reject { |bid| bid == buffer_id }
867
845
  alt = @alternate_buffer_id
868
- (alt && alt != id && @buffers.key?(alt)) ? alt : candidates.first
846
+ (alt && alt != buffer_id && @buffers.key?(alt)) ? alt : candidates.first
869
847
  end
870
848
 
871
849
  @windows.each_value do |win|
872
- next unless win.buffer_id == id
850
+ next unless win.buffer_id == buffer_id
873
851
  next unless fallback_id
874
852
 
875
853
  win.buffer_id = fallback_id
@@ -879,9 +857,9 @@ module RuVim
879
857
  win.col_offset = 0
880
858
  end
881
859
 
882
- @buffers.delete(id)
883
- @local_marks.delete(id)
884
- @alternate_buffer_id = nil if @alternate_buffer_id == id
860
+ @buffers.delete(buffer_id)
861
+ @local_marks.delete(buffer_id)
862
+ @alternate_buffer_id = nil if @alternate_buffer_id == buffer_id
885
863
  save_current_tabpage_state! unless @suspend_tab_autosave
886
864
  ensure_bootstrap_buffer! if @buffers.empty?
887
865
  true
@@ -1023,11 +1001,10 @@ module RuVim
1023
1001
  end
1024
1002
 
1025
1003
  def find_window_ids_by_buffer_kind(kind)
1026
- sym = kind.to_sym
1027
1004
  window_order.select do |wid|
1028
1005
  win = @windows[wid]
1029
1006
  buf = win && @buffers[win.buffer_id]
1030
- buf && buf.kind == sym
1007
+ buf && buf.kind == kind
1031
1008
  end
1032
1009
  end
1033
1010
 
@@ -1165,15 +1142,24 @@ module RuVim
1165
1142
  def assign_filetype(buffer, ft)
1166
1143
  buffer.options["filetype"] = ft
1167
1144
  buffer.lang_module = resolve_lang_module(ft)
1145
+ apply_filetype_defaults(buffer, ft)
1146
+ end
1147
+
1148
+ def apply_filetype_defaults(buffer, ft)
1149
+ unless buffer.options.key?("runprg")
1150
+ runprg = filetype_default_runprg(ft)
1151
+ buffer.options["runprg"] = runprg if runprg
1152
+ end
1153
+ end
1154
+
1155
+ def filetype_default_runprg(ft)
1156
+ Lang::Registry.runprg_for(ft)
1168
1157
  end
1169
1158
 
1170
1159
  private
1171
1160
 
1172
1161
  def resolve_lang_module(ft)
1173
- case ft
1174
- when "ruby" then Lang::Ruby
1175
- else Lang::Base
1176
- end
1162
+ Lang::Registry.resolve_module(ft)
1177
1163
  end
1178
1164
 
1179
1165
  def detect_filetype_from_shebang(path)
@@ -1183,10 +1169,7 @@ module RuVim
1183
1169
  cmd = shebang_command_name(line)
1184
1170
  return nil if cmd.nil? || cmd.empty?
1185
1171
 
1186
- rule = SHEBANG_FILETYPE_RULES.find do |matcher, _filetype|
1187
- matcher.is_a?(Regexp) ? matcher.match?(cmd) : matcher.to_s == cmd
1188
- end
1189
- rule && rule[1]
1172
+ Lang::Registry.detect_by_shebang(cmd)
1190
1173
  rescue StandardError
1191
1174
  nil
1192
1175
  end
@@ -1222,7 +1205,7 @@ module RuVim
1222
1205
  prog = tokens[i].to_s
1223
1206
  end
1224
1207
 
1225
- File.basename(prog.to_s)
1208
+ File.basename(prog)
1226
1209
  end
1227
1210
 
1228
1211
  def default_global_options
@@ -1277,7 +1260,7 @@ module RuVim
1277
1260
  def dup_register_payload(payload)
1278
1261
  return nil unless payload
1279
1262
 
1280
- { text: payload[:text].to_s.dup, type: payload[:type].to_sym }
1263
+ { text: payload[:text].dup, type: payload[:type] }
1281
1264
  end
1282
1265
 
1283
1266
  def assign_detected_filetype(buffer)
@@ -9,6 +9,7 @@ module RuVim
9
9
  :desc,
10
10
  :nargs,
11
11
  :bang,
12
+ :raw_args,
12
13
  :source,
13
14
  keyword_init: true
14
15
  )
@@ -20,7 +21,7 @@ module RuVim
20
21
  @lookup = {}
21
22
  end
22
23
 
23
- def register(name, call:, aliases: [], desc: "", nargs: :any, bang: false, source: :builtin)
24
+ def register(name, call:, aliases: [], desc: "", nargs: :any, bang: false, raw_args: false, source: :builtin)
24
25
  canonical = name.to_s
25
26
  if @specs.key?(canonical)
26
27
  raise RuVim::CommandError, "Ex command already exists: #{canonical}"
@@ -32,6 +33,7 @@ module RuVim
32
33
  desc: desc,
33
34
  nargs: nargs,
34
35
  bang: bang,
36
+ raw_args: raw_args,
35
37
  source: source
36
38
  )
37
39