ruvim 0.2.0 → 0.3.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/AGENTS.md +84 -0
  4. data/CLAUDE.md +1 -0
  5. data/docs/binding.md +23 -0
  6. data/docs/command.md +85 -0
  7. data/docs/config.md +2 -2
  8. data/docs/done.md +21 -0
  9. data/docs/spec.md +157 -12
  10. data/docs/todo.md +1 -5
  11. data/docs/vim_diff.md +94 -172
  12. data/lib/ruvim/app.rb +882 -69
  13. data/lib/ruvim/buffer.rb +35 -1
  14. data/lib/ruvim/cli.rb +12 -3
  15. data/lib/ruvim/clipboard.rb +2 -0
  16. data/lib/ruvim/command_invocation.rb +3 -1
  17. data/lib/ruvim/command_line.rb +2 -0
  18. data/lib/ruvim/command_registry.rb +2 -0
  19. data/lib/ruvim/config_dsl.rb +2 -0
  20. data/lib/ruvim/config_loader.rb +2 -0
  21. data/lib/ruvim/context.rb +2 -0
  22. data/lib/ruvim/dispatcher.rb +143 -13
  23. data/lib/ruvim/display_width.rb +3 -0
  24. data/lib/ruvim/editor.rb +455 -71
  25. data/lib/ruvim/ex_command_registry.rb +2 -0
  26. data/lib/ruvim/global_commands.rb +890 -63
  27. data/lib/ruvim/highlighter.rb +16 -21
  28. data/lib/ruvim/input.rb +39 -28
  29. data/lib/ruvim/keymap_manager.rb +83 -0
  30. data/lib/ruvim/keyword_chars.rb +2 -0
  31. data/lib/ruvim/lang/base.rb +25 -0
  32. data/lib/ruvim/lang/csv.rb +18 -0
  33. data/lib/ruvim/lang/json.rb +18 -0
  34. data/lib/ruvim/lang/markdown.rb +170 -0
  35. data/lib/ruvim/lang/ruby.rb +236 -0
  36. data/lib/ruvim/lang/scheme.rb +44 -0
  37. data/lib/ruvim/lang/tsv.rb +19 -0
  38. data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
  39. data/lib/ruvim/rich_view/table_renderer.rb +176 -0
  40. data/lib/ruvim/rich_view.rb +93 -0
  41. data/lib/ruvim/screen.rb +503 -106
  42. data/lib/ruvim/terminal.rb +18 -1
  43. data/lib/ruvim/text_metrics.rb +2 -0
  44. data/lib/ruvim/version.rb +1 -1
  45. data/lib/ruvim/window.rb +2 -0
  46. data/lib/ruvim.rb +14 -0
  47. data/test/app_completion_test.rb +73 -0
  48. data/test/app_dot_repeat_test.rb +13 -0
  49. data/test/app_motion_test.rb +13 -0
  50. data/test/app_scenario_test.rb +729 -1
  51. data/test/app_startup_test.rb +187 -0
  52. data/test/arglist_test.rb +113 -0
  53. data/test/buffer_test.rb +49 -30
  54. data/test/dispatcher_test.rb +322 -0
  55. data/test/editor_register_test.rb +23 -0
  56. data/test/highlighter_test.rb +121 -0
  57. data/test/indent_test.rb +201 -0
  58. data/test/input_screen_integration_test.rb +40 -2
  59. data/test/markdown_renderer_test.rb +279 -0
  60. data/test/on_save_hook_test.rb +150 -0
  61. data/test/rich_view_test.rb +478 -0
  62. data/test/screen_test.rb +304 -0
  63. metadata +33 -2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "io/console"
2
4
 
3
5
  module RuVim
@@ -20,11 +22,26 @@ module RuVim
20
22
 
21
23
  def with_ui
22
24
  @stdin.raw do
23
- write("\e[?1049h\e[?25l")
25
+ write("\e]112\a\e[?1049h\e[?25l")
24
26
  yield
25
27
  ensure
26
28
  write("\e[?25h\e[?1049l")
27
29
  end
28
30
  end
31
+
32
+ def suspend_for_tstp
33
+ prev_tstp = Signal.trap("TSTP", "DEFAULT")
34
+ @stdin.cooked do
35
+ write("\e[?25h\e[?1049l")
36
+ Process.kill("TSTP", 0)
37
+ end
38
+ ensure
39
+ begin
40
+ Signal.trap("TSTP", prev_tstp) if defined?(prev_tstp)
41
+ rescue StandardError
42
+ nil
43
+ end
44
+ write("\e[?1049h\e[?25l")
45
+ end
29
46
  end
30
47
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuVim
2
4
  module TextMetrics
3
5
  module_function
data/lib/ruvim/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuVim
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/ruvim/window.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuVim
2
4
  class Window
3
5
  attr_reader :id
data/lib/ruvim.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "singleton"
2
4
 
3
5
  module RuVim
@@ -11,6 +13,11 @@ require_relative "ruvim/display_width"
11
13
  require_relative "ruvim/keyword_chars"
12
14
  require_relative "ruvim/text_metrics"
13
15
  require_relative "ruvim/clipboard"
16
+ require_relative "ruvim/lang/base"
17
+ require_relative "ruvim/lang/markdown"
18
+ require_relative "ruvim/lang/ruby"
19
+ require_relative "ruvim/lang/json"
20
+ require_relative "ruvim/lang/scheme"
14
21
  require_relative "ruvim/highlighter"
15
22
  require_relative "ruvim/context"
16
23
  require_relative "ruvim/buffer"
@@ -24,6 +31,13 @@ require_relative "ruvim/keymap_manager"
24
31
  require_relative "ruvim/command_line"
25
32
  require_relative "ruvim/input"
26
33
  require_relative "ruvim/terminal"
34
+ require_relative "ruvim/rich_view"
35
+
36
+ # Register renderers after RichView is defined
37
+ RuVim::RichView.register("markdown", RuVim::RichView::MarkdownRenderer)
38
+
39
+ require_relative "ruvim/lang/tsv"
40
+ require_relative "ruvim/lang/csv"
27
41
  require_relative "ruvim/screen"
28
42
  require_relative "ruvim/config_dsl"
29
43
  require_relative "ruvim/config_loader"
@@ -1,5 +1,6 @@
1
1
  require_relative "test_helper"
2
2
  require "tmpdir"
3
+ require "fileutils"
3
4
 
4
5
  class AppCompletionTest < Minitest::Test
5
6
  def setup
@@ -51,6 +52,35 @@ class AppCompletionTest < Minitest::Test
51
52
  end
52
53
  end
53
54
 
55
+ def test_path_completion_returns_relative_match_without_dot_slash_prefix
56
+ Dir.mktmpdir("ruvim-complete-rel") do |dir|
57
+ Dir.chdir(dir) do
58
+ FileUtils.mkdir_p("lib")
59
+
60
+ matches = @app.send(:path_completion_candidates, "li")
61
+
62
+ assert_includes matches, "lib/"
63
+ end
64
+ end
65
+ end
66
+
67
+ def test_path_completion_sorts_hidden_entries_after_visible_by_default
68
+ Dir.mktmpdir("ruvim-complete-hidden") do |dir|
69
+ FileUtils.mkdir_p(File.join(dir, ".git"))
70
+ File.write(File.join(dir, "aaa.txt"), "")
71
+ File.write(File.join(dir, "bbb.txt"), "")
72
+
73
+ matches = @app.send(:path_completion_candidates, File.join(dir, ""))
74
+
75
+ assert_includes matches, File.join(dir, ".git/")
76
+ visible_idx = matches.index(File.join(dir, "aaa.txt"))
77
+ hidden_idx = matches.index(File.join(dir, ".git/"))
78
+ refute_nil visible_idx
79
+ refute_nil hidden_idx
80
+ assert_operator visible_idx, :<, hidden_idx
81
+ end
82
+ end
83
+
54
84
  def test_command_line_completion_respects_wildmode_list_full_and_wildmenu
55
85
  @editor.materialize_intro_buffer!
56
86
  @editor.set_option("wildmode", "list,full", scope: :global)
@@ -103,6 +133,49 @@ class AppCompletionTest < Minitest::Test
103
133
  end
104
134
  end
105
135
 
136
+ def test_command_line_completion_full_cycles_across_original_match_set
137
+ @editor.materialize_intro_buffer!
138
+ @editor.set_option("wildmode", "full", scope: :global)
139
+
140
+ Dir.mktmpdir("ruvim-wild-full") do |dir|
141
+ FileUtils.mkdir_p(File.join(dir, ".git"))
142
+ a = File.join(dir, "aa.txt")
143
+ b = File.join(dir, "ab.txt")
144
+ File.write(a, "")
145
+ File.write(b, "")
146
+
147
+ @editor.enter_command_line_mode(":")
148
+ cmd = @editor.command_line
149
+ cmd.replace_text("e #{File.join(dir, "a")}")
150
+
151
+ @app.send(:command_line_complete)
152
+ first = cmd.text.dup
153
+ @app.send(:command_line_complete)
154
+ second = cmd.text.dup
155
+
156
+ refute_equal first, second
157
+ assert([a, b].any? { |p| first.end_with?(p) })
158
+ assert([a, b].any? { |p| second.end_with?(p) })
159
+ end
160
+ end
161
+
162
+ def test_command_line_completion_menu_is_not_limited_by_pumheight
163
+ @editor.materialize_intro_buffer!
164
+ @editor.set_option("wildmode", "list", scope: :global)
165
+ @editor.set_option("pumheight", 1, scope: :global)
166
+
167
+ @editor.enter_command_line_mode(":")
168
+ cmd = @editor.command_line
169
+ cmd.replace_text("se")
170
+
171
+ @app.send(:command_line_complete)
172
+
173
+ assert_equal "se", cmd.text
174
+ assert_includes @editor.message, "set"
175
+ assert_includes @editor.message, "setlocal"
176
+ assert_includes @editor.message, "setglobal"
177
+ end
178
+
106
179
  def test_insert_completion_respects_completeopt_noselect_and_pumheight
107
180
  @editor.materialize_intro_buffer!
108
181
  @editor.set_option("completeopt", "menu,menuone,noselect", scope: :global)
@@ -51,4 +51,17 @@ class AppDotRepeatTest < Minitest::Test
51
51
 
52
52
  assert_equal "xxcd", @buffer.line_at(0)
53
53
  end
54
+
55
+ def test_dot_repeats_substitute_char
56
+ @buffer.replace_all_lines!(["abcd"])
57
+ @win.cursor_x = 0
58
+
59
+ press("s")
60
+ @app.send(:handle_key, "X")
61
+ @app.send(:handle_key, :escape)
62
+ press("l")
63
+ press(".")
64
+
65
+ assert_equal "XbXd", @buffer.line_at(0)
66
+ end
54
67
  end
@@ -165,4 +165,17 @@ class AppMotionTest < Minitest::Test
165
165
  press("h", "h")
166
166
  assert_equal 4, @editor.current_window.cursor_x
167
167
  end
168
+
169
+ def test_e_moves_to_next_word_end_when_already_on_word_end
170
+ b = @editor.current_buffer
171
+ b.replace_all_lines!(["one two three"])
172
+ @editor.current_window.cursor_y = 0
173
+ @editor.current_window.cursor_x = 2 # end of "one"
174
+
175
+ press("e")
176
+ assert_equal 6, @editor.current_window.cursor_x # end of "two"
177
+
178
+ press("e")
179
+ assert_equal 12, @editor.current_window.cursor_x # end of "three"
180
+ end
168
181
  end