ruvim 0.2.0 → 0.4.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/AGENTS.md +96 -0
  4. data/CLAUDE.md +1 -0
  5. data/README.md +15 -1
  6. data/docs/binding.md +39 -0
  7. data/docs/command.md +163 -4
  8. data/docs/config.md +12 -4
  9. data/docs/done.md +21 -0
  10. data/docs/spec.md +214 -18
  11. data/docs/todo.md +1 -5
  12. data/docs/tutorial.md +24 -0
  13. data/docs/vim_diff.md +105 -173
  14. data/lib/ruvim/app.rb +1165 -70
  15. data/lib/ruvim/buffer.rb +47 -1
  16. data/lib/ruvim/cli.rb +18 -3
  17. data/lib/ruvim/clipboard.rb +2 -0
  18. data/lib/ruvim/command_invocation.rb +3 -1
  19. data/lib/ruvim/command_line.rb +2 -0
  20. data/lib/ruvim/command_registry.rb +2 -0
  21. data/lib/ruvim/config_dsl.rb +2 -0
  22. data/lib/ruvim/config_loader.rb +2 -0
  23. data/lib/ruvim/context.rb +2 -0
  24. data/lib/ruvim/dispatcher.rb +143 -13
  25. data/lib/ruvim/display_width.rb +3 -0
  26. data/lib/ruvim/editor.rb +466 -71
  27. data/lib/ruvim/ex_command_registry.rb +2 -0
  28. data/lib/ruvim/file_watcher.rb +243 -0
  29. data/lib/ruvim/git/blame.rb +245 -0
  30. data/lib/ruvim/git/branch.rb +97 -0
  31. data/lib/ruvim/git/commit.rb +102 -0
  32. data/lib/ruvim/git/diff.rb +129 -0
  33. data/lib/ruvim/git/handler.rb +84 -0
  34. data/lib/ruvim/git/log.rb +41 -0
  35. data/lib/ruvim/git/status.rb +103 -0
  36. data/lib/ruvim/global_commands.rb +1066 -105
  37. data/lib/ruvim/highlighter.rb +19 -22
  38. data/lib/ruvim/input.rb +40 -28
  39. data/lib/ruvim/keymap_manager.rb +83 -0
  40. data/lib/ruvim/keyword_chars.rb +2 -0
  41. data/lib/ruvim/lang/base.rb +25 -0
  42. data/lib/ruvim/lang/csv.rb +18 -0
  43. data/lib/ruvim/lang/diff.rb +41 -0
  44. data/lib/ruvim/lang/json.rb +52 -0
  45. data/lib/ruvim/lang/markdown.rb +170 -0
  46. data/lib/ruvim/lang/ruby.rb +236 -0
  47. data/lib/ruvim/lang/scheme.rb +44 -0
  48. data/lib/ruvim/lang/tsv.rb +19 -0
  49. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  50. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  51. data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
  52. data/lib/ruvim/rich_view/table_renderer.rb +176 -0
  53. data/lib/ruvim/rich_view.rb +109 -0
  54. data/lib/ruvim/screen.rb +503 -109
  55. data/lib/ruvim/terminal.rb +18 -1
  56. data/lib/ruvim/text_metrics.rb +2 -0
  57. data/lib/ruvim/version.rb +1 -1
  58. data/lib/ruvim/window.rb +2 -0
  59. data/lib/ruvim.rb +24 -0
  60. data/test/app_completion_test.rb +98 -0
  61. data/test/app_dot_repeat_test.rb +13 -0
  62. data/test/app_motion_test.rb +13 -0
  63. data/test/app_scenario_test.rb +898 -1
  64. data/test/app_startup_test.rb +187 -0
  65. data/test/arglist_test.rb +113 -0
  66. data/test/buffer_test.rb +49 -30
  67. data/test/cli_test.rb +14 -0
  68. data/test/clipboard_test.rb +67 -0
  69. data/test/command_line_test.rb +118 -0
  70. data/test/config_dsl_test.rb +87 -0
  71. data/test/dispatcher_test.rb +322 -0
  72. data/test/display_width_test.rb +41 -0
  73. data/test/editor_register_test.rb +23 -0
  74. data/test/file_watcher_test.rb +197 -0
  75. data/test/follow_test.rb +199 -0
  76. data/test/git_blame_test.rb +713 -0
  77. data/test/highlighter_test.rb +165 -0
  78. data/test/indent_test.rb +287 -0
  79. data/test/input_screen_integration_test.rb +40 -2
  80. data/test/markdown_renderer_test.rb +279 -0
  81. data/test/on_save_hook_test.rb +150 -0
  82. data/test/rich_view_test.rb +734 -0
  83. data/test/screen_test.rb +304 -0
  84. data/test/search_option_test.rb +19 -0
  85. data/test/test_helper.rb +9 -0
  86. metadata +49 -2
@@ -13,4 +13,169 @@ class HighlighterTest < Minitest::Test
13
13
  assert_equal "\e[36m", cols[1] # key chars
14
14
  assert_equal "\e[33m", cols[6] # number start
15
15
  end
16
+
17
+ def test_jsonl_highlighter_reuses_json_colors
18
+ cols = RuVim::Highlighter.color_columns("jsonl", '{"a": 10}')
19
+ assert_equal "\e[36m", cols[1] # key chars
20
+ assert_equal "\e[33m", cols[6] # number start
21
+ end
22
+
23
+ def test_ruby_highlighter_marks_instance_variables_and_constants
24
+ cols = RuVim::Highlighter.color_columns("ruby", "@x = Foo")
25
+ assert_equal "\e[93m", cols[0] # @x
26
+ assert_equal "\e[96m", cols[5] # F
27
+ end
28
+
29
+ # --- Markdown ---
30
+
31
+ def test_markdown_heading_h1
32
+ cols = RuVim::Highlighter.color_columns("markdown", "# Hello")
33
+ refute_empty cols
34
+ assert_equal "\e[1;33m", cols[0] # bold yellow for H1
35
+ assert_equal "\e[1;33m", cols[6] # entire line colored
36
+ end
37
+
38
+ def test_markdown_heading_h2
39
+ cols = RuVim::Highlighter.color_columns("markdown", "## Section")
40
+ assert_equal "\e[1;36m", cols[0] # bold cyan for H2
41
+ end
42
+
43
+ def test_markdown_heading_h3_to_h6
44
+ colors = {
45
+ 3 => "\e[1;32m",
46
+ 4 => "\e[1;35m",
47
+ 5 => "\e[1;34m",
48
+ 6 => "\e[1;90m"
49
+ }
50
+ colors.each do |level, expected_color|
51
+ line = "#{"#" * level} Title"
52
+ cols = RuVim::Highlighter.color_columns("markdown", line)
53
+ assert_equal expected_color, cols[0], "H#{level} should use correct color"
54
+ end
55
+ end
56
+
57
+ def test_markdown_fence_line
58
+ cols = RuVim::Highlighter.color_columns("markdown", "```ruby")
59
+ refute_empty cols
60
+ assert_equal "\e[90m", cols[0] # dim
61
+ end
62
+
63
+ def test_markdown_hr
64
+ cols = RuVim::Highlighter.color_columns("markdown", "---")
65
+ refute_empty cols
66
+ assert_equal "\e[90m", cols[0] # dim
67
+ end
68
+
69
+ def test_markdown_block_quote_marker
70
+ cols = RuVim::Highlighter.color_columns("markdown", "> quoted text")
71
+ assert_equal "\e[36m", cols[0] # cyan for >
72
+ end
73
+
74
+ def test_markdown_inline_bold
75
+ cols = RuVim::Highlighter.color_columns("markdown", "hello **bold** world")
76
+ # ** markers and content should be bold
77
+ assert_equal "\e[1m", cols[6] # first *
78
+ assert_equal "\e[1m", cols[13] # last *
79
+ end
80
+
81
+ def test_markdown_inline_code
82
+ cols = RuVim::Highlighter.color_columns("markdown", "use `foo()` here")
83
+ assert_equal "\e[33m", cols[4] # backtick
84
+ end
85
+
86
+ def test_markdown_empty_line
87
+ cols = RuVim::Highlighter.color_columns("markdown", "")
88
+ assert_empty cols
89
+ end
90
+
91
+ def test_markdown_plain_text
92
+ cols = RuVim::Highlighter.color_columns("markdown", "plain text")
93
+ assert_empty cols
94
+ end
95
+
96
+ # --- Scheme ---
97
+
98
+ def test_scheme_keyword_define
99
+ cols = RuVim::Highlighter.color_columns("scheme", "(define x 42)")
100
+ assert_equal "\e[36m", cols[1] # "define" keyword
101
+ assert_equal "\e[36m", cols[6] # end of "define"
102
+ end
103
+
104
+ def test_scheme_keyword_lambda
105
+ cols = RuVim::Highlighter.color_columns("scheme", "(lambda (x) x)")
106
+ assert_equal "\e[36m", cols[1] # "lambda"
107
+ end
108
+
109
+ def test_scheme_string
110
+ cols = RuVim::Highlighter.color_columns("scheme", '(display "hello")')
111
+ assert_equal "\e[32m", cols[9] # opening quote
112
+ assert_equal "\e[32m", cols[15] # closing quote
113
+ end
114
+
115
+ def test_scheme_number
116
+ cols = RuVim::Highlighter.color_columns("scheme", "(+ 1 2.5)")
117
+ assert_equal "\e[33m", cols[3] # "1"
118
+ assert_equal "\e[33m", cols[5] # "2"
119
+ end
120
+
121
+ def test_scheme_boolean
122
+ cols = RuVim::Highlighter.color_columns("scheme", "(if #t #f)")
123
+ assert_equal "\e[35m", cols[4] # "#t"
124
+ assert_equal "\e[35m", cols[7] # "#f"
125
+ end
126
+
127
+ def test_scheme_comment
128
+ cols = RuVim::Highlighter.color_columns("scheme", "; this is a comment")
129
+ assert_equal "\e[90m", cols[0] # ";"
130
+ assert_equal "\e[90m", cols[18] # end of comment
131
+ end
132
+
133
+ def test_scheme_char_literal
134
+ cols = RuVim::Highlighter.color_columns("scheme", '#\a #\space')
135
+ assert_equal "\e[32m", cols[0] # "#\a"
136
+ assert_equal "\e[32m", cols[4] # "#\space"
137
+ end
138
+
139
+ def test_scheme_empty_line
140
+ cols = RuVim::Highlighter.color_columns("scheme", "")
141
+ assert_empty cols
142
+ end
143
+
144
+ # --- Diff ---
145
+
146
+ def test_diff_add_line_green
147
+ cols = RuVim::Highlighter.color_columns("diff", "+added line")
148
+ refute_empty cols
149
+ assert_equal "\e[32m", cols[0]
150
+ assert_equal "\e[32m", cols[5]
151
+ end
152
+
153
+ def test_diff_delete_line_red
154
+ cols = RuVim::Highlighter.color_columns("diff", "-removed line")
155
+ refute_empty cols
156
+ assert_equal "\e[31m", cols[0]
157
+ end
158
+
159
+ def test_diff_hunk_header_cyan
160
+ cols = RuVim::Highlighter.color_columns("diff", "@@ -1,3 +1,4 @@ def foo")
161
+ refute_empty cols
162
+ assert_equal "\e[36m", cols[0]
163
+ end
164
+
165
+ def test_diff_header_bold
166
+ cols = RuVim::Highlighter.color_columns("diff", "diff --git a/foo.rb b/foo.rb")
167
+ refute_empty cols
168
+ assert_equal "\e[1m", cols[0]
169
+ end
170
+
171
+ def test_diff_context_line_no_color
172
+ cols = RuVim::Highlighter.color_columns("diff", " context line")
173
+ assert_empty cols
174
+ end
175
+
176
+ def test_diff_meta_line_yellow
177
+ cols = RuVim::Highlighter.color_columns("diff", "index abc..def 100644")
178
+ refute_empty cols
179
+ assert_equal "\e[33m", cols[0]
180
+ end
16
181
  end
@@ -0,0 +1,287 @@
1
+ require_relative "test_helper"
2
+
3
+ class JsonIndentTest < Minitest::Test
4
+ def calc(lines, target_row, sw = 2)
5
+ RuVim::Lang::Json.calculate_indent(lines, target_row, sw)
6
+ end
7
+
8
+ def test_first_line_is_zero
9
+ assert_equal 0, calc(["{"], 0)
10
+ end
11
+
12
+ def test_after_open_brace
13
+ lines = ["{", ' "key": "value"']
14
+ assert_equal 2, calc(lines, 1)
15
+ end
16
+
17
+ def test_close_brace
18
+ lines = ["{", ' "key": "value"', "}"]
19
+ assert_equal 0, calc(lines, 2)
20
+ end
21
+
22
+ def test_after_open_bracket
23
+ lines = ["[", " 1"]
24
+ assert_equal 2, calc(lines, 1)
25
+ end
26
+
27
+ def test_close_bracket
28
+ lines = ["[", " 1,", " 2", "]"]
29
+ assert_equal 0, calc(lines, 3)
30
+ end
31
+
32
+ def test_nested_objects
33
+ lines = [
34
+ "{",
35
+ ' "a": {',
36
+ ' "b": 1',
37
+ " }",
38
+ "}"
39
+ ]
40
+ assert_equal 2, calc(lines, 1)
41
+ assert_equal 4, calc(lines, 2)
42
+ assert_equal 2, calc(lines, 3)
43
+ assert_equal 0, calc(lines, 4)
44
+ end
45
+
46
+ def test_array_in_object
47
+ lines = [
48
+ "{",
49
+ ' "items": [',
50
+ " 1,",
51
+ " 2",
52
+ " ]",
53
+ "}"
54
+ ]
55
+ assert_equal 2, calc(lines, 1)
56
+ assert_equal 4, calc(lines, 2)
57
+ assert_equal 4, calc(lines, 3)
58
+ assert_equal 2, calc(lines, 4)
59
+ assert_equal 0, calc(lines, 5)
60
+ end
61
+
62
+ def test_shiftwidth_4
63
+ lines = ["{", ' "key": 1', "}"]
64
+ assert_equal 4, calc(lines, 1, 4)
65
+ assert_equal 0, calc(lines, 2, 4)
66
+ end
67
+
68
+ def test_indent_trigger_open_brace
69
+ assert RuVim::Lang::Json.indent_trigger?("{")
70
+ assert RuVim::Lang::Json.indent_trigger?(' "key": {')
71
+ assert RuVim::Lang::Json.indent_trigger?(' "key": [')
72
+ end
73
+
74
+ def test_indent_trigger_no_trigger
75
+ refute RuVim::Lang::Json.indent_trigger?(' "key": "value"')
76
+ refute RuVim::Lang::Json.indent_trigger?("}")
77
+ end
78
+
79
+ def test_dedent_trigger_close_brace
80
+ assert_kind_of Regexp, RuVim::Lang::Json.dedent_trigger("}")
81
+ assert_kind_of Regexp, RuVim::Lang::Json.dedent_trigger("]")
82
+ end
83
+
84
+ def test_dedent_trigger_no_trigger
85
+ assert_nil RuVim::Lang::Json.dedent_trigger("a")
86
+ end
87
+ end
88
+
89
+ class RubyIndentTest < Minitest::Test
90
+ def calc(lines, target_row, sw = 2)
91
+ RuVim::Lang::Ruby.calculate_indent(lines, target_row, sw)
92
+ end
93
+
94
+ def test_first_line_is_zero
95
+ assert_equal 0, calc(["hello"], 0)
96
+ end
97
+
98
+ def test_after_def
99
+ lines = ["def foo", " bar"]
100
+ assert_equal 2, calc(lines, 1)
101
+ end
102
+
103
+ def test_end_returns_to_zero
104
+ lines = ["def foo", " bar", "end"]
105
+ assert_equal 0, calc(lines, 2)
106
+ end
107
+
108
+ def test_class_def_end_nesting
109
+ lines = [
110
+ "class Foo",
111
+ " def bar",
112
+ " baz",
113
+ " end",
114
+ "end"
115
+ ]
116
+ assert_equal 2, calc(lines, 1) # def bar
117
+ assert_equal 4, calc(lines, 2) # baz
118
+ assert_equal 2, calc(lines, 3) # end (inner)
119
+ assert_equal 0, calc(lines, 4) # end (outer)
120
+ end
121
+
122
+ def test_if_else_end
123
+ lines = [
124
+ "if cond",
125
+ " a",
126
+ "else",
127
+ " b",
128
+ "end"
129
+ ]
130
+ assert_equal 2, calc(lines, 1) # a
131
+ assert_equal 0, calc(lines, 2) # else
132
+ assert_equal 2, calc(lines, 3) # b
133
+ assert_equal 0, calc(lines, 4) # end
134
+ end
135
+
136
+ def test_if_elsif_else_end
137
+ lines = [
138
+ "if a",
139
+ " x",
140
+ "elsif b",
141
+ " y",
142
+ "else",
143
+ " z",
144
+ "end"
145
+ ]
146
+ assert_equal 2, calc(lines, 1) # x
147
+ assert_equal 0, calc(lines, 2) # elsif
148
+ assert_equal 2, calc(lines, 3) # y
149
+ assert_equal 0, calc(lines, 4) # else
150
+ assert_equal 2, calc(lines, 5) # z
151
+ assert_equal 0, calc(lines, 6) # end
152
+ end
153
+
154
+ def test_modifier_if_does_not_increase_indent
155
+ lines = [
156
+ "def foo",
157
+ " return if true",
158
+ " bar",
159
+ "end"
160
+ ]
161
+ assert_equal 2, calc(lines, 1) # return if true
162
+ assert_equal 2, calc(lines, 2) # bar
163
+ assert_equal 0, calc(lines, 3) # end
164
+ end
165
+
166
+ def test_do_end_block
167
+ lines = [
168
+ "items.each do |x|",
169
+ " puts x",
170
+ "end"
171
+ ]
172
+ assert_equal 2, calc(lines, 1) # puts x
173
+ assert_equal 0, calc(lines, 2) # end
174
+ end
175
+
176
+ def test_brace_block
177
+ lines = [
178
+ "items.map {",
179
+ " |x| x + 1",
180
+ "}"
181
+ ]
182
+ assert_equal 2, calc(lines, 1)
183
+ assert_equal 0, calc(lines, 2)
184
+ end
185
+
186
+ def test_case_when
187
+ lines = [
188
+ "case x",
189
+ "when 1",
190
+ " a",
191
+ "when 2",
192
+ " b",
193
+ "end"
194
+ ]
195
+ assert_equal 0, calc(lines, 1) # when 1
196
+ assert_equal 2, calc(lines, 2) # a
197
+ assert_equal 0, calc(lines, 3) # when 2
198
+ assert_equal 2, calc(lines, 4) # b
199
+ assert_equal 0, calc(lines, 5) # end
200
+ end
201
+
202
+ def test_rescue_ensure
203
+ lines = [
204
+ "begin",
205
+ " risky",
206
+ "rescue => e",
207
+ " handle",
208
+ "ensure",
209
+ " cleanup",
210
+ "end"
211
+ ]
212
+ assert_equal 2, calc(lines, 1) # risky
213
+ assert_equal 0, calc(lines, 2) # rescue
214
+ assert_equal 2, calc(lines, 3) # handle
215
+ assert_equal 0, calc(lines, 4) # ensure
216
+ assert_equal 2, calc(lines, 5) # cleanup
217
+ assert_equal 0, calc(lines, 6) # end
218
+ end
219
+
220
+ def test_unless_until_while_for
221
+ %w[unless until while for].each do |kw|
222
+ lines = ["#{kw} cond", " body", "end"]
223
+ assert_equal 2, calc(lines, 1), "body after #{kw}"
224
+ assert_equal 0, calc(lines, 2), "end after #{kw}"
225
+ end
226
+ end
227
+
228
+ def test_module_nesting
229
+ lines = [
230
+ "module A",
231
+ " module B",
232
+ " def foo",
233
+ " bar",
234
+ " end",
235
+ " end",
236
+ "end"
237
+ ]
238
+ assert_equal 2, calc(lines, 1) # module B
239
+ assert_equal 4, calc(lines, 2) # def foo
240
+ assert_equal 6, calc(lines, 3) # bar
241
+ assert_equal 4, calc(lines, 4) # end (inner)
242
+ assert_equal 2, calc(lines, 5) # end (mid)
243
+ assert_equal 0, calc(lines, 6) # end (outer)
244
+ end
245
+
246
+ def test_comment_lines_are_skipped
247
+ lines = [
248
+ "def foo",
249
+ " # comment",
250
+ " bar",
251
+ "end"
252
+ ]
253
+ assert_equal 2, calc(lines, 1) # comment
254
+ assert_equal 2, calc(lines, 2) # bar
255
+ assert_equal 0, calc(lines, 3) # end
256
+ end
257
+
258
+ def test_shiftwidth_4
259
+ lines = ["def foo", " bar", "end"]
260
+ assert_equal 4, calc(lines, 1, 4)
261
+ assert_equal 0, calc(lines, 2, 4)
262
+ end
263
+
264
+ def test_paren_bracket_nesting
265
+ lines = [
266
+ "x = [",
267
+ " 1,",
268
+ " 2",
269
+ "]"
270
+ ]
271
+ assert_equal 2, calc(lines, 1)
272
+ assert_equal 2, calc(lines, 2)
273
+ assert_equal 0, calc(lines, 3)
274
+ end
275
+
276
+ def test_empty_lines_preserve_depth
277
+ lines = [
278
+ "def foo",
279
+ "",
280
+ " bar",
281
+ "end"
282
+ ]
283
+ assert_equal 2, calc(lines, 1) # empty line inside def
284
+ assert_equal 2, calc(lines, 2) # bar
285
+ assert_equal 0, calc(lines, 3) # end
286
+ end
287
+ end
@@ -56,7 +56,7 @@ class InputScreenIntegrationTest < Minitest::Test
56
56
  app.instance_variable_set(:@screen, screen)
57
57
 
58
58
  stdin = FakeTTY.new("\e[6~")
59
- input = RuVim::Input.new(stdin: stdin)
59
+ input = RuVim::Input.new(stdin)
60
60
 
61
61
  with_fake_select do
62
62
  key = input.read_key(timeout: 0.2)
@@ -72,11 +72,49 @@ class InputScreenIntegrationTest < Minitest::Test
72
72
 
73
73
  def test_input_keeps_repeated_arrow_sequences_separate
74
74
  stdin = FakeTTY.new("\e[A\e[A")
75
- input = RuVim::Input.new(stdin: stdin)
75
+ input = RuVim::Input.new(stdin)
76
76
 
77
77
  with_fake_select do
78
78
  assert_equal :up, input.read_key(timeout: 0.2)
79
79
  assert_equal :up, input.read_key(timeout: 0.2)
80
80
  end
81
81
  end
82
+
83
+ def test_input_reads_ctrl_z
84
+ stdin = FakeTTY.new("\u001a")
85
+ input = RuVim::Input.new(stdin)
86
+
87
+ with_fake_select do
88
+ assert_equal :ctrl_z, input.read_key(timeout: 0.2)
89
+ end
90
+ end
91
+
92
+ def test_input_shift_arrow_sequences
93
+ {
94
+ "\e[1;2A" => :shift_up,
95
+ "\e[1;2B" => :shift_down,
96
+ "\e[1;2C" => :shift_right,
97
+ "\e[1;2D" => :shift_left
98
+ }.each do |seq, expected|
99
+ stdin = FakeTTY.new(seq)
100
+ input = RuVim::Input.new(stdin)
101
+
102
+ with_fake_select do
103
+ assert_equal expected, input.read_key(timeout: 0.2), "Expected #{expected} for sequence #{seq.inspect}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def test_has_pending_input_returns_true_when_data_available
109
+ stdin = FakeTTY.new("abc")
110
+ input = RuVim::Input.new(stdin)
111
+
112
+ with_fake_select do
113
+ assert input.has_pending_input?
114
+ input.read_key(timeout: 0)
115
+ input.read_key(timeout: 0)
116
+ input.read_key(timeout: 0)
117
+ refute input.has_pending_input?
118
+ end
119
+ end
82
120
  end