ruvim 0.3.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +18 -6
  3. data/README.md +15 -1
  4. data/docs/binding.md +16 -0
  5. data/docs/command.md +78 -4
  6. data/docs/config.md +10 -2
  7. data/docs/spec.md +60 -9
  8. data/docs/tutorial.md +24 -0
  9. data/docs/vim_diff.md +18 -8
  10. data/lib/ruvim/app.rb +290 -8
  11. data/lib/ruvim/buffer.rb +14 -2
  12. data/lib/ruvim/cli.rb +6 -0
  13. data/lib/ruvim/editor.rb +12 -1
  14. data/lib/ruvim/file_watcher.rb +243 -0
  15. data/lib/ruvim/git/blame.rb +245 -0
  16. data/lib/ruvim/git/branch.rb +97 -0
  17. data/lib/ruvim/git/commit.rb +102 -0
  18. data/lib/ruvim/git/diff.rb +129 -0
  19. data/lib/ruvim/git/handler.rb +84 -0
  20. data/lib/ruvim/git/log.rb +41 -0
  21. data/lib/ruvim/git/status.rb +103 -0
  22. data/lib/ruvim/global_commands.rb +176 -42
  23. data/lib/ruvim/highlighter.rb +3 -1
  24. data/lib/ruvim/input.rb +1 -0
  25. data/lib/ruvim/lang/diff.rb +41 -0
  26. data/lib/ruvim/lang/json.rb +34 -0
  27. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  28. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  29. data/lib/ruvim/rich_view.rb +16 -0
  30. data/lib/ruvim/screen.rb +9 -12
  31. data/lib/ruvim/version.rb +1 -1
  32. data/lib/ruvim.rb +10 -0
  33. data/test/app_completion_test.rb +25 -0
  34. data/test/app_scenario_test.rb +169 -0
  35. data/test/cli_test.rb +14 -0
  36. data/test/clipboard_test.rb +67 -0
  37. data/test/command_line_test.rb +118 -0
  38. data/test/config_dsl_test.rb +87 -0
  39. data/test/display_width_test.rb +41 -0
  40. data/test/file_watcher_test.rb +197 -0
  41. data/test/follow_test.rb +199 -0
  42. data/test/git_blame_test.rb +713 -0
  43. data/test/highlighter_test.rb +44 -0
  44. data/test/indent_test.rb +86 -0
  45. data/test/rich_view_test.rb +256 -0
  46. data/test/search_option_test.rb +19 -0
  47. data/test/test_helper.rb +9 -0
  48. metadata +17 -1
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "tempfile"
5
+
6
+ class FollowTest < Minitest::Test
7
+ def create_follow_app
8
+ @tmpfile = Tempfile.new(["follow_test", ".txt"])
9
+ @tmpfile.write("line1\nline2\n")
10
+ @tmpfile.flush
11
+ @path = @tmpfile.path
12
+
13
+ @app = RuVim::App.new(path: @path, clean: true)
14
+ @editor = @app.instance_variable_get(:@editor)
15
+ @dispatcher = @app.instance_variable_get(:@dispatcher)
16
+ end
17
+
18
+ def cleanup_follow_app
19
+ return unless @app
20
+
21
+ watchers = @app.instance_variable_get(:@follow_watchers)
22
+ watchers.each_value { |w| w.stop rescue nil }
23
+ watchers.clear
24
+ @tmpfile&.close!
25
+ end
26
+
27
+ def test_follow_starts_on_file_buffer
28
+ create_follow_app
29
+ @dispatcher.dispatch_ex(@editor, "follow")
30
+ watchers = @app.instance_variable_get(:@follow_watchers)
31
+ buf = @editor.current_buffer
32
+
33
+ assert !@editor.message_error?, "Unexpected error: #{@editor.message}"
34
+ assert_equal :live, buf.stream_state
35
+ assert watchers.key?(buf.id)
36
+ assert_includes @editor.message.to_s, "[follow]"
37
+ ensure
38
+ cleanup_follow_app
39
+ end
40
+
41
+ def test_follow_stops_on_ctrl_c
42
+ create_follow_app
43
+ @dispatcher.dispatch_ex(@editor, "follow")
44
+ buf = @editor.current_buffer
45
+ assert_equal :live, buf.stream_state
46
+
47
+ @app.send(:handle_key, :ctrl_c)
48
+ assert_nil buf.stream_state
49
+ assert_includes @editor.message.to_s, "stopped"
50
+ ensure
51
+ cleanup_follow_app
52
+ end
53
+
54
+ def test_follow_toggle_stops
55
+ create_follow_app
56
+ @dispatcher.dispatch_ex(@editor, "follow")
57
+ buf = @editor.current_buffer
58
+ assert_equal :live, buf.stream_state
59
+
60
+ @dispatcher.dispatch_ex(@editor, "follow")
61
+ watchers = @app.instance_variable_get(:@follow_watchers)
62
+ assert_nil buf.stream_state
63
+ refute watchers.key?(buf.id)
64
+ assert_includes @editor.message.to_s, "stopped"
65
+ ensure
66
+ cleanup_follow_app
67
+ end
68
+
69
+ def test_follow_stop_removes_sentinel_empty_line
70
+ create_follow_app
71
+ buf = @editor.current_buffer
72
+ lines_before = buf.line_count
73
+
74
+ @dispatcher.dispatch_ex(@editor, "follow")
75
+ @dispatcher.dispatch_ex(@editor, "follow")
76
+
77
+ assert_equal lines_before, buf.line_count, "Sentinel empty line should be removed on stop"
78
+ refute_equal "", buf.lines.last, "Last line should not be empty sentinel"
79
+ ensure
80
+ cleanup_follow_app
81
+ end
82
+
83
+ def test_follow_makes_buffer_not_modifiable
84
+ create_follow_app
85
+ buf = @editor.current_buffer
86
+ assert buf.modifiable?
87
+
88
+ @dispatcher.dispatch_ex(@editor, "follow")
89
+ refute buf.modifiable?, "Buffer should not be modifiable during follow"
90
+
91
+ @dispatcher.dispatch_ex(@editor, "follow")
92
+ assert buf.modifiable?, "Buffer should be modifiable after follow stops"
93
+ ensure
94
+ cleanup_follow_app
95
+ end
96
+
97
+ def test_follow_error_on_modified_buffer
98
+ create_follow_app
99
+ buf = @editor.current_buffer
100
+ buf.modified = true
101
+
102
+ @dispatcher.dispatch_ex(@editor, "follow")
103
+ assert_includes @editor.message.to_s, "unsaved changes"
104
+ assert_nil buf.stream_state
105
+ ensure
106
+ cleanup_follow_app
107
+ end
108
+
109
+ def test_follow_error_on_no_path_buffer
110
+ @app = RuVim::App.new(clean: true)
111
+ @editor = @app.instance_variable_get(:@editor)
112
+ @dispatcher = @app.instance_variable_get(:@dispatcher)
113
+ @editor.materialize_intro_buffer!
114
+ buf = @editor.current_buffer
115
+ buf.instance_variable_set(:@path, nil)
116
+
117
+ @dispatcher.dispatch_ex(@editor, "follow")
118
+ assert_includes @editor.message.to_s, "No file"
119
+ end
120
+
121
+ def test_follow_appends_data_from_file
122
+ create_follow_app
123
+ win = @editor.current_window
124
+ buf = @editor.current_buffer
125
+ win.cursor_y = buf.line_count - 1
126
+
127
+ @dispatcher.dispatch_ex(@editor, "follow")
128
+
129
+ File.open(@path, "a") { |f| f.write("line3\nline4\n") }
130
+
131
+ assert_eventually(timeout: 3) do
132
+ @app.send(:drain_stream_events!)
133
+ buf.line_count > 3
134
+ end
135
+
136
+ # line2 should NOT be joined with line3
137
+ assert_includes buf.lines, "line2"
138
+ assert_includes buf.lines, "line3"
139
+ assert_includes buf.lines, "line4"
140
+ assert_equal buf.line_count - 1, win.cursor_y
141
+ ensure
142
+ cleanup_follow_app
143
+ end
144
+
145
+ def test_follow_no_scroll_when_cursor_not_at_end
146
+ create_follow_app
147
+ buf = @editor.current_buffer
148
+ win = @editor.current_window
149
+ win.cursor_y = 0
150
+
151
+ @dispatcher.dispatch_ex(@editor, "follow")
152
+
153
+ File.open(@path, "a") { |f| f.write("line3\n") }
154
+
155
+ assert_eventually(timeout: 3) do
156
+ @app.send(:drain_stream_events!)
157
+ buf.line_count > 2
158
+ end
159
+
160
+ assert_equal 0, win.cursor_y
161
+ ensure
162
+ cleanup_follow_app
163
+ end
164
+
165
+ def test_startup_follow_applies_to_all_buffers
166
+ tmp1 = Tempfile.new(["follow_multi1", ".txt"])
167
+ tmp1.write("aaa\n"); tmp1.flush
168
+ tmp2 = Tempfile.new(["follow_multi2", ".txt"])
169
+ tmp2.write("bbb\n"); tmp2.flush
170
+
171
+ app = RuVim::App.new(paths: [tmp1.path, tmp2.path], follow: true, clean: true)
172
+ editor = app.instance_variable_get(:@editor)
173
+ watchers = app.instance_variable_get(:@follow_watchers)
174
+
175
+ bufs = editor.buffers.values.select(&:file_buffer?)
176
+ assert_equal 2, bufs.size
177
+ bufs.each do |buf|
178
+ assert_equal :live, buf.stream_state, "#{buf.display_name} should be in follow mode"
179
+ assert watchers.key?(buf.id), "#{buf.display_name} should have a watcher"
180
+ end
181
+ ensure
182
+ watchers&.each_value { |w| w.stop rescue nil }
183
+ tmp1&.close!
184
+ tmp2&.close!
185
+ end
186
+
187
+ private
188
+
189
+ def assert_eventually(timeout: 2, interval: 0.05)
190
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
191
+ loop do
192
+ return if yield
193
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
194
+ flunk "Timed out waiting for condition"
195
+ end
196
+ sleep interval
197
+ end
198
+ end
199
+ end