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
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module RuVim
6
+ module Browser
7
+ ALLOWED_SCHEMES = %w[https http].freeze
8
+
9
+ module_function
10
+
11
+ def open_url(url)
12
+ return false unless valid_url?(url)
13
+
14
+ backend = detect_backend
15
+ return false unless backend
16
+
17
+ if backend[:type] == :powershell
18
+ cmd = powershell_encoded_command(backend[:ps_path], url)
19
+ _, _, status = Open3.capture3(*cmd)
20
+ else
21
+ _, _, status = Open3.capture3(*backend[:command], url)
22
+ end
23
+ status.success?
24
+ rescue StandardError
25
+ false
26
+ end
27
+
28
+ def valid_url?(url)
29
+ scheme = url.to_s.split(":", 2).first.to_s.downcase
30
+ ALLOWED_SCHEMES.include?(scheme)
31
+ end
32
+
33
+ def powershell_encoded_command(ps_path, url)
34
+ # Encode as UTF-16LE Base64 to avoid any command injection via -Command.
35
+ # This matches the approach used by Node.js "open" package.
36
+ ps_script = "Start-Process '#{url.gsub("'", "''")}'"
37
+ encoded = [ps_script.encode("UTF-16LE")].pack("m0")
38
+ [ps_path, "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded]
39
+ end
40
+
41
+ def detect_backend
42
+ return { command: %w[open], type: :macos } if command_available?("open") && macos?
43
+ return { command: %w[xdg-open], type: :xdg } if command_available?("xdg-open") && !wsl?
44
+ return { command: %w[wslview], type: :wslview } if command_available?("wslview")
45
+
46
+ ps_path = powershell_path(wsl_mount_point)
47
+ if wsl? && File.exist?(ps_path)
48
+ return { ps_path: ps_path, type: :powershell }
49
+ end
50
+
51
+ nil
52
+ end
53
+
54
+ def powershell_path(mount_point)
55
+ "#{mount_point}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"
56
+ end
57
+
58
+ def wsl_mount_point(config: nil)
59
+ config ||= begin
60
+ File.read("/etc/wsl.conf")
61
+ rescue StandardError
62
+ nil
63
+ end
64
+
65
+ default = "/mnt/"
66
+ return default unless config
67
+
68
+ config.each_line do |line|
69
+ stripped = line.strip
70
+ next if stripped.start_with?("#")
71
+
72
+ if stripped.match?(/\Aroot\s*=/)
73
+ value = stripped.sub(/\Aroot\s*=\s*/, "").strip
74
+ value += "/" unless value.end_with?("/")
75
+ return value
76
+ end
77
+ end
78
+
79
+ default
80
+ end
81
+
82
+ def macos?
83
+ RUBY_PLATFORM.include?("darwin")
84
+ end
85
+
86
+ def wsl?
87
+ return @wsl if defined?(@wsl)
88
+
89
+ @wsl = begin
90
+ File.read("/proc/version").include?("microsoft")
91
+ rescue StandardError
92
+ false
93
+ end
94
+ end
95
+
96
+ def reset!
97
+ @wsl = nil
98
+ end
99
+
100
+ def command_available?(name)
101
+ system("which", name, out: File::NULL, err: File::NULL)
102
+ end
103
+ end
104
+ end
data/lib/ruvim/buffer.rb CHANGED
@@ -1,17 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "stream"
4
+
3
5
  module RuVim
4
6
  class Buffer
5
7
  attr_reader :id, :kind, :name
6
8
  attr_accessor :path
7
9
  attr_reader :options
8
10
  attr_writer :modified
9
- attr_accessor :stream_state, :loading_state
11
+ attr_accessor :stream
12
+
13
+ def stream_status
14
+ @stream&.status
15
+ end
16
+
17
+ def stream_command
18
+ @stream&.command
19
+ end
20
+
10
21
  attr_accessor :lang_module
11
22
 
23
+ def self.ensure_regular_file!(path)
24
+ raise RuVim::CommandError, "Not a regular file: #{path}" if File.exist?(path) && !File.file?(path)
25
+ end
26
+
12
27
  def self.from_file(id:, path:)
28
+ ensure_regular_file!(path)
13
29
  lines =
14
- if File.exist?(path)
30
+ if File.file?(path)
15
31
  data = decode_text(File.binread(path))
16
32
  split_lines(data)
17
33
  else
@@ -32,34 +48,33 @@ module RuVim
32
48
  end
33
49
 
34
50
  def self.decode_text(bytes)
35
- s = bytes.to_s.dup
36
- return s if s.encoding == Encoding::UTF_8 && s.valid_encoding?
51
+ s = bytes.to_s
52
+ return s.dup if s.encoding == Encoding::UTF_8 && s.valid_encoding?
37
53
 
38
54
  utf8 = s.dup.force_encoding(Encoding::UTF_8)
39
55
  return utf8 if utf8.valid_encoding?
40
56
 
41
57
  ext = Encoding.default_external
42
58
  if ext && ext != Encoding::UTF_8
43
- return s.dup.force_encoding(ext).encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
59
+ return utf8.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
44
60
  end
45
61
 
46
62
  utf8.scrub
47
63
  rescue StandardError
48
- s.dup.force_encoding(Encoding::UTF_8).scrub
64
+ bytes.to_s.dup.force_encoding(Encoding::UTF_8).scrub
49
65
  end
50
66
 
51
67
  def initialize(id:, path: nil, lines: [""], kind: :file, name: nil, readonly: false, modifiable: true)
52
68
  @id = id
53
69
  @path = path
54
- @kind = kind.to_sym
70
+ @kind = kind
55
71
  @name = name
56
72
  @lines = lines.dup
57
73
  @lines = [""] if @lines.empty?
58
74
  @modified = false
59
75
  @readonly = !!readonly
60
76
  @modifiable = !!modifiable
61
- @stream_state = nil
62
- @loading_state = nil
77
+ @stream = nil
63
78
  @undo_stack = []
64
79
  @redo_stack = []
65
80
  @change_group_depth = 0
@@ -87,7 +102,7 @@ module RuVim
87
102
  end
88
103
 
89
104
  def modifiable?
90
- @modifiable && @loading_state != :live
105
+ @modifiable && !@stream&.live?
91
106
  end
92
107
 
93
108
  def modifiable=(value)
@@ -111,12 +126,11 @@ module RuVim
111
126
  end
112
127
 
113
128
  def configure_special!(kind:, name: nil, readonly: true, modifiable: false)
114
- @kind = kind.to_sym
129
+ @kind = kind
115
130
  @name = name
116
131
  @readonly = !!readonly
117
132
  @modifiable = !!modifiable
118
- @stream_state = nil unless @kind == :stream
119
- @loading_state = nil
133
+ @stream = nil unless @kind == :stream
120
134
  self
121
135
  end
122
136
 
@@ -126,8 +140,7 @@ module RuVim
126
140
  @path = nil
127
141
  @readonly = false
128
142
  @modifiable = true
129
- @stream_state = nil
130
- @loading_state = nil
143
+ @stream = nil
131
144
  @lines = [""]
132
145
  @modified = false
133
146
  @undo_stack.clear
@@ -342,18 +355,26 @@ module RuVim
342
355
 
343
356
  # Append externally-streamed text without touching undo history or modifiable state.
344
357
  def append_stream_text!(text)
345
- chunk = text.to_s
346
- return [@lines.length - 1, @lines[-1].length] if chunk.empty?
358
+ return [@lines.length - 1, @lines[-1].length] if text.empty?
347
359
 
348
- parts = chunk.split("\n", -1)
360
+ parts = text.split("\n", -1)
349
361
  head = parts.shift || ""
350
- @lines[-1] = @lines[-1].to_s + head
362
+ @lines[-1] = @lines[-1] + head
351
363
  @lines.concat(parts)
352
364
  @lines = [""] if @lines.empty?
353
365
  @modified = false
354
366
  [@lines.length - 1, @lines[-1].length]
355
367
  end
356
368
 
369
+ # Append pre-split lines (from background thread).
370
+ # head_suffix is concatenated to the current last line.
371
+ def append_stream_lines!(head_suffix, lines)
372
+ @lines[-1] = @lines[-1] + head_suffix unless head_suffix.empty?
373
+ @lines.concat(lines) unless lines.empty?
374
+ @lines = [""] if @lines.empty?
375
+ @modified = false
376
+ end
377
+
357
378
  def finalize_async_file_load!(ended_with_newline:)
358
379
  if ended_with_newline && @lines.length > 1 && @lines[-1] == ""
359
380
  @lines.pop
@@ -379,7 +400,9 @@ module RuVim
379
400
  target = path || @path
380
401
  raise RuVim::CommandError, "No file name" if target.nil? || target.empty?
381
402
 
382
- data = File.exist?(target) ? self.class.decode_text(File.binread(target)) : ""
403
+ self.class.ensure_regular_file!(target)
404
+
405
+ data = File.file?(target) ? self.class.decode_text(File.binread(target)) : ""
383
406
  @lines = self.class.split_lines(data)
384
407
  @path = target
385
408
  @modified = false
data/lib/ruvim/cli.rb CHANGED
@@ -16,6 +16,7 @@ module RuVim
16
16
  :no_swap,
17
17
  :nomodifiable,
18
18
  :restricted_mode,
19
+ :follow,
19
20
  :verbose_level,
20
21
  :startup_time_path,
21
22
  :startup_open_layout,
@@ -70,6 +71,7 @@ module RuVim
70
71
  quickfix_errorfile: opts.quickfix_errorfile,
71
72
  session_file: opts.session_file,
72
73
  nomodifiable: opts.nomodifiable,
74
+ follow: opts.follow,
73
75
  restricted: opts.restricted_mode,
74
76
  verbose_level: opts.verbose_level,
75
77
  verbose_io: stderr,
@@ -101,6 +103,7 @@ module RuVim
101
103
  no_swap: false,
102
104
  nomodifiable: false,
103
105
  restricted_mode: false,
106
+ follow: false,
104
107
  verbose_level: 0,
105
108
  startup_time_path: nil,
106
109
  startup_open_layout: nil,
@@ -159,6 +162,8 @@ module RuVim
159
162
  end
160
163
  when "-n"
161
164
  opts.no_swap = true
165
+ when "-f"
166
+ opts.follow = true
162
167
  when "-M"
163
168
  opts.nomodifiable = true
164
169
  when "-Z"
@@ -240,6 +245,7 @@ module RuVim
240
245
  -h, --help Show this help
241
246
  -v, --version Show version
242
247
  --clean Start without user config and ftplugin
248
+ -f Open file in follow mode (tail -f style)
243
249
  -R Open file readonly (disallow :w on current buffer)
244
250
  -d Diff mode requested (compat placeholder; not implemented yet)
245
251
  -q {errorfile} Quickfix startup placeholder (not implemented yet)
@@ -4,12 +4,12 @@ module RuVim
4
4
  class CommandInvocation
5
5
  attr_accessor :id, :argv, :kwargs, :count, :bang, :raw_keys
6
6
 
7
- def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: nil, raw_keys: nil)
7
+ def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: false, raw_keys: nil)
8
8
  @id = id
9
9
  @argv = argv || []
10
10
  @kwargs = kwargs || {}
11
11
  @count = count
12
- @bang = !!bang
12
+ @bang = bang
13
13
  @raw_keys = raw_keys
14
14
  end
15
15
  end