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
@@ -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,29 +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, :follow_backend
11
+ attr_accessor :stream
10
12
 
11
13
  def stream_status
12
- return nil unless @stream_state
14
+ @stream&.status
15
+ end
13
16
 
14
- if @kind == :stream
15
- "stdin/#{@stream_state}"
16
- elsif @follow_backend == :inotify
17
- "follow/i"
18
- else
19
- "follow"
20
- end
17
+ def stream_command
18
+ @stream&.command
21
19
  end
20
+
22
21
  attr_accessor :lang_module
23
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
+
24
27
  def self.from_file(id:, path:)
28
+ ensure_regular_file!(path)
25
29
  lines =
26
- if File.exist?(path)
30
+ if File.file?(path)
27
31
  data = decode_text(File.binread(path))
28
32
  split_lines(data)
29
33
  else
@@ -44,34 +48,33 @@ module RuVim
44
48
  end
45
49
 
46
50
  def self.decode_text(bytes)
47
- s = bytes.to_s.dup
48
- 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?
49
53
 
50
54
  utf8 = s.dup.force_encoding(Encoding::UTF_8)
51
55
  return utf8 if utf8.valid_encoding?
52
56
 
53
57
  ext = Encoding.default_external
54
58
  if ext && ext != Encoding::UTF_8
55
- 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)
56
60
  end
57
61
 
58
62
  utf8.scrub
59
63
  rescue StandardError
60
- s.dup.force_encoding(Encoding::UTF_8).scrub
64
+ bytes.to_s.dup.force_encoding(Encoding::UTF_8).scrub
61
65
  end
62
66
 
63
67
  def initialize(id:, path: nil, lines: [""], kind: :file, name: nil, readonly: false, modifiable: true)
64
68
  @id = id
65
69
  @path = path
66
- @kind = kind.to_sym
70
+ @kind = kind
67
71
  @name = name
68
72
  @lines = lines.dup
69
73
  @lines = [""] if @lines.empty?
70
74
  @modified = false
71
75
  @readonly = !!readonly
72
76
  @modifiable = !!modifiable
73
- @stream_state = nil
74
- @loading_state = nil
77
+ @stream = nil
75
78
  @undo_stack = []
76
79
  @redo_stack = []
77
80
  @change_group_depth = 0
@@ -99,7 +102,7 @@ module RuVim
99
102
  end
100
103
 
101
104
  def modifiable?
102
- @modifiable && @loading_state != :live && @stream_state != :live
105
+ @modifiable && !@stream&.live?
103
106
  end
104
107
 
105
108
  def modifiable=(value)
@@ -123,12 +126,11 @@ module RuVim
123
126
  end
124
127
 
125
128
  def configure_special!(kind:, name: nil, readonly: true, modifiable: false)
126
- @kind = kind.to_sym
129
+ @kind = kind
127
130
  @name = name
128
131
  @readonly = !!readonly
129
132
  @modifiable = !!modifiable
130
- @stream_state = nil unless @kind == :stream
131
- @loading_state = nil
133
+ @stream = nil unless @kind == :stream
132
134
  self
133
135
  end
134
136
 
@@ -138,8 +140,7 @@ module RuVim
138
140
  @path = nil
139
141
  @readonly = false
140
142
  @modifiable = true
141
- @stream_state = nil
142
- @loading_state = nil
143
+ @stream = nil
143
144
  @lines = [""]
144
145
  @modified = false
145
146
  @undo_stack.clear
@@ -354,18 +355,26 @@ module RuVim
354
355
 
355
356
  # Append externally-streamed text without touching undo history or modifiable state.
356
357
  def append_stream_text!(text)
357
- chunk = text.to_s
358
- return [@lines.length - 1, @lines[-1].length] if chunk.empty?
358
+ return [@lines.length - 1, @lines[-1].length] if text.empty?
359
359
 
360
- parts = chunk.split("\n", -1)
360
+ parts = text.split("\n", -1)
361
361
  head = parts.shift || ""
362
- @lines[-1] = @lines[-1].to_s + head
362
+ @lines[-1] = @lines[-1] + head
363
363
  @lines.concat(parts)
364
364
  @lines = [""] if @lines.empty?
365
365
  @modified = false
366
366
  [@lines.length - 1, @lines[-1].length]
367
367
  end
368
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
+
369
378
  def finalize_async_file_load!(ended_with_newline:)
370
379
  if ended_with_newline && @lines.length > 1 && @lines[-1] == ""
371
380
  @lines.pop
@@ -391,7 +400,9 @@ module RuVim
391
400
  target = path || @path
392
401
  raise RuVim::CommandError, "No file name" if target.nil? || target.empty?
393
402
 
394
- 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)) : ""
395
406
  @lines = self.class.split_lines(data)
396
407
  @path = target
397
408
  @modified = false
@@ -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