ruvim 0.1.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 (66) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +15 -0
  3. data/README.md +135 -0
  4. data/Rakefile +36 -0
  5. data/docs/binding.md +125 -0
  6. data/docs/command.md +306 -0
  7. data/docs/config.md +155 -0
  8. data/docs/done.md +112 -0
  9. data/docs/plugin.md +559 -0
  10. data/docs/spec.md +655 -0
  11. data/docs/todo.md +63 -0
  12. data/docs/tutorial.md +490 -0
  13. data/docs/vim_diff.md +179 -0
  14. data/exe/ruvim +6 -0
  15. data/lib/ruvim/app.rb +1600 -0
  16. data/lib/ruvim/buffer.rb +421 -0
  17. data/lib/ruvim/cli.rb +264 -0
  18. data/lib/ruvim/clipboard.rb +73 -0
  19. data/lib/ruvim/command_invocation.rb +14 -0
  20. data/lib/ruvim/command_line.rb +63 -0
  21. data/lib/ruvim/command_registry.rb +38 -0
  22. data/lib/ruvim/config_dsl.rb +134 -0
  23. data/lib/ruvim/config_loader.rb +68 -0
  24. data/lib/ruvim/context.rb +26 -0
  25. data/lib/ruvim/dispatcher.rb +120 -0
  26. data/lib/ruvim/display_width.rb +110 -0
  27. data/lib/ruvim/editor.rb +1025 -0
  28. data/lib/ruvim/ex_command_registry.rb +80 -0
  29. data/lib/ruvim/global_commands.rb +1889 -0
  30. data/lib/ruvim/highlighter.rb +52 -0
  31. data/lib/ruvim/input.rb +66 -0
  32. data/lib/ruvim/keymap_manager.rb +96 -0
  33. data/lib/ruvim/screen.rb +452 -0
  34. data/lib/ruvim/terminal.rb +30 -0
  35. data/lib/ruvim/text_metrics.rb +96 -0
  36. data/lib/ruvim/version.rb +5 -0
  37. data/lib/ruvim/window.rb +71 -0
  38. data/lib/ruvim.rb +30 -0
  39. data/sig/ruvim.rbs +4 -0
  40. data/test/app_completion_test.rb +39 -0
  41. data/test/app_dot_repeat_test.rb +54 -0
  42. data/test/app_motion_test.rb +73 -0
  43. data/test/app_register_test.rb +47 -0
  44. data/test/app_scenario_test.rb +77 -0
  45. data/test/app_startup_test.rb +199 -0
  46. data/test/app_text_object_test.rb +54 -0
  47. data/test/app_unicode_behavior_test.rb +66 -0
  48. data/test/buffer_test.rb +72 -0
  49. data/test/cli_test.rb +165 -0
  50. data/test/config_dsl_test.rb +78 -0
  51. data/test/dispatcher_test.rb +124 -0
  52. data/test/editor_mark_test.rb +69 -0
  53. data/test/editor_register_test.rb +64 -0
  54. data/test/fixtures/render_basic_snapshot.txt +8 -0
  55. data/test/fixtures/render_basic_snapshot_nonumber.txt +8 -0
  56. data/test/fixtures/render_unicode_scrolled_snapshot.txt +7 -0
  57. data/test/highlighter_test.rb +16 -0
  58. data/test/input_screen_integration_test.rb +69 -0
  59. data/test/keymap_manager_test.rb +48 -0
  60. data/test/render_snapshot_test.rb +70 -0
  61. data/test/screen_test.rb +123 -0
  62. data/test/search_option_test.rb +39 -0
  63. data/test/test_helper.rb +15 -0
  64. data/test/text_metrics_test.rb +42 -0
  65. data/test/window_test.rb +21 -0
  66. metadata +106 -0
@@ -0,0 +1,124 @@
1
+ require_relative "test_helper"
2
+
3
+ class DispatcherTest < Minitest::Test
4
+ def setup
5
+ @app = RuVim::App.new
6
+ @editor = @app.instance_variable_get(:@editor)
7
+ @dispatcher = RuVim::Dispatcher.new
8
+ end
9
+
10
+ def test_parse_ex_with_bang_and_args
11
+ parsed = @dispatcher.parse_ex("w! foo.txt")
12
+ assert_equal "w", parsed.name
13
+ assert parsed.bang
14
+ assert_equal ["foo.txt"], parsed.argv
15
+ end
16
+
17
+ def test_dispatch_ex_help_sets_message
18
+ @dispatcher.dispatch_ex(@editor, "help")
19
+ assert_equal "[Help] help", @editor.message
20
+ assert_equal :help, @editor.current_buffer.kind
21
+ assert @editor.current_buffer.readonly?
22
+ refute @editor.current_buffer.modifiable?
23
+ assert_includes @editor.current_buffer.lines.join("\n"), "help"
24
+ assert_equal :normal, @editor.mode
25
+ end
26
+
27
+ def test_dispatch_ex_help_topic_for_command
28
+ @dispatcher.dispatch_ex(@editor, "help w")
29
+ assert_equal "[Help] w", @editor.message
30
+ body = @editor.current_buffer.lines.join("\n")
31
+ assert_includes body, ":w"
32
+ assert_includes body, "Write current buffer"
33
+ end
34
+
35
+ def test_dispatch_ex_command_and_ruby
36
+ @dispatcher.dispatch_ex(@editor, "command Hi help")
37
+ assert_equal "Defined :Hi", @editor.message
38
+
39
+ @dispatcher.dispatch_ex(@editor, "Hi")
40
+ assert_equal "[Help] help", @editor.message
41
+
42
+ @dispatcher.dispatch_ex(@editor, "ruby 1+2")
43
+ assert_equal "ruby: 3", @editor.message
44
+ end
45
+
46
+ def test_dispatch_ex_set_commands
47
+ @dispatcher.dispatch_ex(@editor, "set number")
48
+ assert_equal true, @editor.current_window.options["number"]
49
+
50
+ @dispatcher.dispatch_ex(@editor, "setlocal tabstop=4")
51
+ assert_equal 4, @editor.current_buffer.options["tabstop"]
52
+
53
+ @dispatcher.dispatch_ex(@editor, "setglobal tabstop=8")
54
+ assert_equal 8, @editor.global_options["tabstop"]
55
+ assert_equal 4, @editor.effective_option("tabstop")
56
+ end
57
+
58
+ def test_q_closes_current_window_when_multiple_windows_exist
59
+ @editor.split_current_window(layout: :horizontal)
60
+ assert_equal 2, @editor.window_count
61
+
62
+ @dispatcher.dispatch_ex(@editor, "q")
63
+
64
+ assert_equal 1, @editor.window_count
65
+ assert @editor.running?
66
+ assert_equal "closed window", @editor.message
67
+ end
68
+
69
+ def test_q_closes_current_tab_when_multiple_tabs_exist
70
+ @editor.tabnew
71
+ assert_equal 2, @editor.tabpage_count
72
+ assert_equal 1, @editor.window_count
73
+
74
+ @dispatcher.dispatch_ex(@editor, "q")
75
+
76
+ assert_equal 1, @editor.tabpage_count
77
+ assert @editor.running?
78
+ assert_equal "closed tab", @editor.message
79
+ end
80
+
81
+ def test_q_exits_app_when_last_window
82
+ assert_equal 1, @editor.window_count
83
+ assert_equal 1, @editor.tabpage_count
84
+
85
+ @dispatcher.dispatch_ex(@editor, "q")
86
+
87
+ refute @editor.running?
88
+ end
89
+
90
+ def test_dispatch_ex_error_marks_message_as_error
91
+ @dispatcher.dispatch_ex(@editor, "no_such_command")
92
+
93
+ assert_match(/Error:/, @editor.message)
94
+ assert_equal true, @editor.message_error?
95
+ end
96
+
97
+ def test_vimgrep_populates_quickfix_and_cnext_moves
98
+ @editor.materialize_intro_buffer!
99
+ @editor.current_buffer.replace_all_lines!(["foo", "bar foo", "baz"])
100
+ @dispatcher.dispatch_ex(@editor, "vimgrep /foo/")
101
+
102
+ assert_equal 2, @editor.quickfix_items.length
103
+ assert_equal 0, @editor.current_window.cursor_y
104
+
105
+ @dispatcher.dispatch_ex(@editor, "cnext")
106
+ assert_equal 1, @editor.current_window.cursor_y
107
+
108
+ @dispatcher.dispatch_ex(@editor, "copen")
109
+ qf_windows = @editor.find_window_ids_by_buffer_kind(:quickfix)
110
+ refute_empty qf_windows
111
+ end
112
+
113
+ def test_lvimgrep_populates_location_list_and_lnext_moves
114
+ @editor.materialize_intro_buffer!
115
+ @editor.current_buffer.replace_all_lines!(["aa", "bb aa", "cc aa"])
116
+ wid = @editor.current_window_id
117
+
118
+ @dispatcher.dispatch_ex(@editor, "lvimgrep /aa/")
119
+ assert_equal 3, @editor.location_items(wid).length
120
+
121
+ @dispatcher.dispatch_ex(@editor, "lnext")
122
+ assert_equal 1, @editor.current_window.cursor_y
123
+ end
124
+ end
@@ -0,0 +1,69 @@
1
+ require_relative "test_helper"
2
+
3
+ class EditorMarkTest < Minitest::Test
4
+ def test_local_and_global_marks
5
+ editor = fresh_editor
6
+ buffer = editor.current_buffer
7
+ window = editor.current_window
8
+
9
+ buffer.replace_all_lines!([" abc", "def"])
10
+ window.cursor_y = 1
11
+ window.cursor_x = 2
12
+
13
+ assert editor.set_mark("a")
14
+ assert editor.set_mark("A")
15
+
16
+ assert_equal 1, editor.mark_location("a")[:row]
17
+ assert_equal 2, editor.mark_location("A")[:col]
18
+ end
19
+
20
+ def test_jump_to_mark_linewise_uses_first_nonblank
21
+ editor = fresh_editor
22
+ buffer = editor.current_buffer
23
+ window = editor.current_window
24
+ buffer.replace_all_lines!([" abc"])
25
+ window.cursor_y = 0
26
+ window.cursor_x = 4
27
+ editor.set_mark("a")
28
+
29
+ window.cursor_x = 0
30
+ editor.jump_to_mark("a", linewise: true)
31
+
32
+ assert_equal 2, window.cursor_x
33
+ end
34
+
35
+ def test_jumplist_older_and_newer
36
+ editor = fresh_editor
37
+ buffer = editor.current_buffer
38
+ window = editor.current_window
39
+ buffer.replace_all_lines!(["a", "b", "c"])
40
+ window.cursor_y = 0
41
+ window.cursor_x = 0
42
+ editor.push_jump_location
43
+
44
+ window.cursor_y = 2
45
+ window.cursor_x = 0
46
+ editor.push_jump_location
47
+
48
+ editor.jump_older
49
+ assert_equal 0, window.cursor_y
50
+
51
+ editor.jump_newer
52
+ assert_equal 2, window.cursor_y
53
+ end
54
+
55
+ def test_macro_recording_and_append
56
+ editor = fresh_editor
57
+
58
+ assert editor.start_macro_recording("a")
59
+ editor.record_macro_key("i")
60
+ editor.record_macro_key("x")
61
+ editor.stop_macro_recording
62
+ assert_equal %w[i x], editor.macro_keys("a")
63
+
64
+ assert editor.start_macro_recording("A")
65
+ editor.record_macro_key("y")
66
+ editor.stop_macro_recording
67
+ assert_equal %w[i x y], editor.macro_keys("a")
68
+ end
69
+ end
@@ -0,0 +1,64 @@
1
+ require_relative "test_helper"
2
+
3
+ class EditorRegisterTest < Minitest::Test
4
+ def test_named_register_and_append_register
5
+ editor = fresh_editor
6
+
7
+ editor.set_register("a", text: "foo", type: :charwise)
8
+ assert_equal({ text: "foo", type: :charwise }, editor.get_register("a"))
9
+ assert_equal({ text: "foo", type: :charwise }, editor.get_register("\""))
10
+
11
+ editor.set_register("A", text: "bar", type: :charwise)
12
+ assert_equal({ text: "foobar", type: :charwise }, editor.get_register("a"))
13
+ assert_equal({ text: "foobar", type: :charwise }, editor.get_register("\""))
14
+ end
15
+
16
+ def test_active_register_is_consumed_once
17
+ editor = fresh_editor
18
+ editor.set_active_register("b")
19
+
20
+ assert_equal "b", editor.consume_active_register
21
+ assert_equal "\"", editor.consume_active_register
22
+ end
23
+
24
+ def test_black_hole_register_discards_explicit_write
25
+ editor = fresh_editor
26
+ editor.set_register("\"", text: "keep", type: :charwise)
27
+
28
+ editor.set_register("_", text: "drop", type: :charwise)
29
+
30
+ assert_equal({ text: "keep", type: :charwise }, editor.get_register("\""))
31
+ assert_nil editor.get_register("_")
32
+ end
33
+
34
+ def test_operator_register_updates_yank_zero_and_delete_numbered
35
+ editor = fresh_editor
36
+
37
+ editor.store_operator_register("\"", text: "yank", type: :charwise, kind: :yank)
38
+ assert_equal({ text: "yank", type: :charwise }, editor.get_register("\""))
39
+ assert_equal({ text: "yank", type: :charwise }, editor.get_register("0"))
40
+
41
+ editor.store_operator_register("\"", text: "del1", type: :charwise, kind: :delete)
42
+ editor.store_operator_register("\"", text: "del2", type: :linewise, kind: :delete)
43
+ assert_equal({ text: "del2", type: :linewise }, editor.get_register("1"))
44
+ assert_equal({ text: "del1", type: :charwise }, editor.get_register("2"))
45
+ assert_equal({ text: "yank", type: :charwise }, editor.get_register("0"))
46
+ end
47
+
48
+ def test_black_hole_skips_auto_operator_registers
49
+ editor = fresh_editor
50
+ editor.store_operator_register("\"", text: "seed", type: :charwise, kind: :yank)
51
+
52
+ editor.store_operator_register("_", text: "drop", type: :charwise, kind: :delete)
53
+
54
+ assert_equal({ text: "seed", type: :charwise }, editor.get_register("\""))
55
+ assert_equal({ text: "seed", type: :charwise }, editor.get_register("0"))
56
+ assert_nil editor.get_register("1")
57
+ end
58
+
59
+ def test_detect_filetype_on_opened_buffer
60
+ editor = RuVim::Editor.new
61
+ buffer = editor.add_empty_buffer(path: "/tmp/example.rb")
62
+ assert_equal "ruby", buffer.options["filetype"]
63
+ end
64
+ end
@@ -0,0 +1,8 @@
1
+ 1 # title
2
+ 2
3
+ 3 foo
4
+ 4 bar 日本語 編集
5
+ 5 baz
6
+ ~
7
+ ~
8
+ -- NORMAL -- t1/1 w 1:1
@@ -0,0 +1,8 @@
1
+ # title
2
+
3
+ foo
4
+ bar 日本語 編集
5
+ baz
6
+ ~
7
+ ~
8
+ -- NORMAL -- t1/1 w 1:1
@@ -0,0 +1,7 @@
1
+ 1 # title
2
+ 2
3
+ 3 foo
4
+ 4 bar 日本語 編集
5
+ 5 baz
6
+ 6 qux
7
+ -- NORMAL -- t1 4:5
@@ -0,0 +1,16 @@
1
+ require_relative "test_helper"
2
+
3
+ class HighlighterTest < Minitest::Test
4
+ def test_ruby_highlighter_marks_keyword_and_string
5
+ cols = RuVim::Highlighter.color_columns("ruby", 'def x; "hi"; end')
6
+ refute_empty cols
7
+ assert_equal "\e[36m", cols[0] # "def"
8
+ assert_equal "\e[32m", cols[7] # opening quote
9
+ end
10
+
11
+ def test_json_highlighter_marks_key_and_number
12
+ cols = RuVim::Highlighter.color_columns("json", '{"a": 10}')
13
+ assert_equal "\e[36m", cols[1] # key chars
14
+ assert_equal "\e[33m", cols[6] # number start
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ require_relative "test_helper"
2
+
3
+ class InputScreenIntegrationTest < Minitest::Test
4
+ TerminalStub = Struct.new(:winsize) do
5
+ attr_reader :writes
6
+
7
+ def write(data)
8
+ @writes ||= []
9
+ @writes << data
10
+ end
11
+ end
12
+
13
+ class FakeTTY
14
+ def initialize(bytes)
15
+ @bytes = bytes.dup
16
+ end
17
+
18
+ def getch
19
+ @bytes.slice!(0)
20
+ end
21
+
22
+ def read_nonblock(_n)
23
+ raise IO::WaitReadable if @bytes.empty?
24
+
25
+ @bytes.slice!(0)
26
+ end
27
+
28
+ def ready?
29
+ !@bytes.empty?
30
+ end
31
+ end
32
+
33
+ def test_input_pagedown_to_app_and_screen_render
34
+ app = RuVim::App.new(clean: true)
35
+ editor = app.instance_variable_get(:@editor)
36
+ editor.materialize_intro_buffer!
37
+ editor.current_buffer.replace_all_lines!((1..30).map { |i| "line #{i}" })
38
+
39
+ term = TerminalStub.new([8, 40])
40
+ screen = RuVim::Screen.new(terminal: term)
41
+ app.instance_variable_set(:@screen, screen)
42
+
43
+ stdin = FakeTTY.new("\e[6~")
44
+ input = RuVim::Input.new(stdin: stdin)
45
+
46
+ io_sc = IO.singleton_class
47
+ verbose, $VERBOSE = $VERBOSE, nil
48
+ io_sc.alias_method(:__orig_select_for_input_screen_test, :select)
49
+ io_sc.define_method(:select) do |readers, *_rest|
50
+ ready = Array(readers).select { |io| io.respond_to?(:ready?) && io.ready? }
51
+ ready.empty? ? nil : [ready, [], []]
52
+ end
53
+
54
+ begin
55
+ key = input.read_key(timeout: 0.2)
56
+ assert_equal :pagedown, key
57
+
58
+ app.send(:handle_normal_key, key)
59
+ screen.render(editor)
60
+
61
+ assert_operator editor.current_window.cursor_y, :>, 0
62
+ assert_includes term.writes.last, "line "
63
+ ensure
64
+ io_sc.alias_method(:select, :__orig_select_for_input_screen_test)
65
+ io_sc.remove_method(:__orig_select_for_input_screen_test) rescue nil
66
+ $VERBOSE = verbose
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "test_helper"
2
+
3
+ class KeymapManagerTest < Minitest::Test
4
+ def setup
5
+ @km = RuVim::KeymapManager.new
6
+ @editor = fresh_editor
7
+ end
8
+
9
+ def test_mode_local_beats_global
10
+ @km.bind_global("x", "global.x")
11
+ @km.bind(:normal, "x", "normal.x")
12
+
13
+ match = @km.resolve_with_context(:normal, ["x"], editor: @editor)
14
+ assert_equal :match, match.status
15
+ assert_equal "normal.x", match.invocation.id
16
+ end
17
+
18
+ def test_buffer_local_beats_mode_local
19
+ @km.bind(:normal, "x", "normal.x")
20
+ @km.bind_buffer(@editor.current_buffer.id, "x", "buffer.x")
21
+
22
+ match = @km.resolve_with_context(:normal, ["x"], editor: @editor)
23
+ assert_equal "buffer.x", match.invocation.id
24
+ end
25
+
26
+ def test_prefix_pending_and_match
27
+ @km.bind(:normal, "dd", "delete.line")
28
+
29
+ pending = @km.resolve_with_context(:normal, ["d"], editor: @editor)
30
+ assert_equal :pending, pending.status
31
+
32
+ exact = @km.resolve_with_context(:normal, %w[d d], editor: @editor)
33
+ assert_equal :match, exact.status
34
+ assert_equal "delete.line", exact.invocation.id
35
+ end
36
+
37
+ def test_filetype_local_map_respects_mode
38
+ @editor.current_buffer.options["filetype"] = "ruby"
39
+ @km.bind(:normal, "x", "normal.x")
40
+ @km.bind_filetype("ruby", "x", "ruby.insert.x", mode: :insert)
41
+
42
+ normal = @km.resolve_with_context(:normal, ["x"], editor: @editor)
43
+ assert_equal "normal.x", normal.invocation.id
44
+
45
+ insert = @km.resolve_with_context(:insert, ["x"], editor: @editor)
46
+ assert_equal "ruby.insert.x", insert.invocation.id
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ require_relative "test_helper"
2
+
3
+ class RenderSnapshotTest < Minitest::Test
4
+ TerminalStub = Struct.new(:winsize) do
5
+ def write(_data); end
6
+ end
7
+
8
+ FIXTURE = File.expand_path("fixtures/render_basic_snapshot.txt", __dir__)
9
+ FIXTURE_NONUM = File.expand_path("fixtures/render_basic_snapshot_nonumber.txt", __dir__)
10
+ FIXTURE_UNICODE_SCROLL = File.expand_path("fixtures/render_unicode_scrolled_snapshot.txt", __dir__)
11
+
12
+ def test_basic_render_frame_matches_snapshot
13
+ snapshot = build_snapshot(
14
+ lines: ["# title", "", "foo", "bar 日本語 編集", "baz"],
15
+ winsize: [8, 24],
16
+ number: true
17
+ )
18
+ expected = File.read(FIXTURE)
19
+ assert_equal expected, snapshot
20
+ end
21
+
22
+ def test_basic_render_frame_without_number_matches_snapshot
23
+ snapshot = build_snapshot(
24
+ lines: ["# title", "", "foo", "bar 日本語 編集", "baz"],
25
+ winsize: [8, 24],
26
+ number: false
27
+ )
28
+ expected = File.read(FIXTURE_NONUM)
29
+ assert_equal expected, snapshot
30
+ end
31
+
32
+ def test_unicode_scrolled_render_matches_snapshot
33
+ snapshot = build_snapshot(
34
+ lines: ["# title", "", "foo", "bar 日本語 編集", "baz", "qux", "quux"],
35
+ winsize: [7, 20],
36
+ number: true,
37
+ cursor_y: 3,
38
+ cursor_x: 4
39
+ )
40
+ expected = File.read(FIXTURE_UNICODE_SCROLL)
41
+ assert_equal expected, snapshot
42
+ end
43
+
44
+ private
45
+
46
+ def build_snapshot(lines:, winsize:, number:, cursor_y: 0, cursor_x: 0)
47
+ editor = RuVim::Editor.new
48
+ buf = editor.add_empty_buffer
49
+ win = editor.add_window(buffer_id: buf.id)
50
+ buf.replace_all_lines!(lines)
51
+ editor.set_option("number", number, scope: :window, window: win, buffer: buf)
52
+ win.cursor_y = cursor_y
53
+ win.cursor_x = cursor_x
54
+
55
+ term = TerminalStub.new(winsize)
56
+ screen = RuVim::Screen.new(terminal: term)
57
+
58
+ rows, cols = term.winsize
59
+ text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
60
+ rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
61
+ win.ensure_visible(buf, height: text_rows, width: text_cols, tabstop: 2)
62
+ frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
63
+
64
+ (1..rows).map { |row| strip_ansi(frame[:lines][row].to_s) }.join("\n")
65
+ end
66
+
67
+ def strip_ansi(str)
68
+ str.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
69
+ end
70
+ end
@@ -0,0 +1,123 @@
1
+ require_relative "test_helper"
2
+
3
+ class ScreenTest < Minitest::Test
4
+ TerminalStub = Struct.new(:winsize) do
5
+ attr_reader :writes
6
+
7
+ def write(data)
8
+ @writes ||= []
9
+ @writes << data
10
+ end
11
+ end
12
+
13
+ def test_horizontal_render_draws_all_visible_rows
14
+ editor = RuVim::Editor.new
15
+ buf = editor.add_empty_buffer
16
+ editor.add_window(buffer_id: buf.id)
17
+ buf.replace_all_lines!(["# title", "", "foo", "bar 日本語 編集", "baz"])
18
+
19
+ term = TerminalStub.new([8, 20])
20
+ screen = RuVim::Screen.new(terminal: term)
21
+
22
+ rows, cols = term.winsize
23
+ text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
24
+ rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
25
+ editor.window_order.each do |win_id|
26
+ w = editor.windows.fetch(win_id)
27
+ b = editor.buffers.fetch(w.buffer_id)
28
+ w.ensure_visible(b, height: text_rows, width: text_cols)
29
+ end
30
+ frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
31
+
32
+ assert_includes frame[:lines][1], "#"
33
+ assert frame[:lines].key?(2), "row 2 should be rendered"
34
+ assert_includes frame[:lines][3], "foo"
35
+ assert_includes frame[:lines][4], "bar"
36
+ assert_includes frame[:lines][5], "baz"
37
+ end
38
+
39
+ def test_line_number_prefix_supports_relativenumber
40
+ editor = RuVim::Editor.new
41
+ buf = editor.add_empty_buffer
42
+ win = editor.add_window(buffer_id: buf.id)
43
+ buf.replace_all_lines!(["aa", "bb", "cc", "dd"])
44
+ win.cursor_y = 2
45
+ editor.set_option("relativenumber", true, scope: :window, window: win, buffer: buf)
46
+
47
+ term = TerminalStub.new([8, 20])
48
+ screen = RuVim::Screen.new(terminal: term)
49
+
50
+ assert_equal " 2 ", screen.send(:line_number_prefix, editor, win, buf, 0, 3)
51
+ assert_equal " 1 ", screen.send(:line_number_prefix, editor, win, buf, 1, 3)
52
+ assert_equal " 0 ", screen.send(:line_number_prefix, editor, win, buf, 2, 3)
53
+
54
+ editor.set_option("number", true, scope: :window, window: win, buffer: buf)
55
+ assert_equal " 3 ", screen.send(:line_number_prefix, editor, win, buf, 2, 3) # current line is absolute when both enabled
56
+ end
57
+
58
+ def test_render_shows_error_message_on_command_line_row_with_highlight
59
+ editor = RuVim::Editor.new
60
+ buf = editor.add_empty_buffer
61
+ editor.add_window(buffer_id: buf.id)
62
+ term = TerminalStub.new([6, 20])
63
+ screen = RuVim::Screen.new(terminal: term)
64
+
65
+ editor.echo_error("boom")
66
+ screen.render(editor)
67
+ out = term.writes.last
68
+ assert_includes out, "\e[97;41m"
69
+ assert_includes out, "boom"
70
+ end
71
+
72
+ def test_render_reuses_syntax_highlight_cache_for_same_line
73
+ editor = RuVim::Editor.new
74
+ buf = editor.add_empty_buffer
75
+ win = editor.add_window(buffer_id: buf.id)
76
+ buf.replace_all_lines!(['def x; "hi"; end'])
77
+ editor.set_option("filetype", "ruby", scope: :buffer, buffer: buf, window: win)
78
+
79
+ term = TerminalStub.new([6, 40])
80
+ screen = RuVim::Screen.new(terminal: term)
81
+
82
+ calls = 0
83
+ mod = RuVim::Highlighter.singleton_class
84
+ verbose, $VERBOSE = $VERBOSE, nil
85
+ mod.alias_method(:__orig_color_columns_for_screen_test, :color_columns)
86
+ mod.define_method(:color_columns) do |*args, **kwargs|
87
+ calls += 1
88
+ __orig_color_columns_for_screen_test(*args, **kwargs)
89
+ end
90
+
91
+ begin
92
+ screen.render(editor)
93
+ screen.render(editor)
94
+ ensure
95
+ mod.alias_method(:color_columns, :__orig_color_columns_for_screen_test)
96
+ mod.remove_method(:__orig_color_columns_for_screen_test) rescue nil
97
+ $VERBOSE = verbose
98
+ end
99
+
100
+ assert_equal 1, calls
101
+ end
102
+
103
+ def test_render_text_line_with_cursor_search_and_syntax_highlights_fits_width
104
+ editor = RuVim::Editor.new
105
+ buf = editor.add_empty_buffer
106
+ win = editor.add_window(buffer_id: buf.id)
107
+ buf.replace_all_lines!(['def x; "日本語"; end'])
108
+ editor.set_option("filetype", "ruby", scope: :buffer, buffer: buf, window: win)
109
+ editor.set_last_search(pattern: "日本", direction: :forward)
110
+ win.cursor_y = 0
111
+ win.cursor_x = 8 # around string body
112
+
113
+ term = TerminalStub.new([6, 18])
114
+ screen = RuVim::Screen.new(terminal: term)
115
+ line = buf.line_at(0)
116
+
117
+ out = screen.send(:render_text_line, line, editor, buffer_row: 0, window: win, buffer: buf, width: 10)
118
+ visible = out.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
119
+
120
+ assert_equal 10, RuVim::DisplayWidth.display_width(visible, tabstop: 2)
121
+ refute_includes out, "\n"
122
+ end
123
+ end
@@ -0,0 +1,39 @@
1
+ require_relative "test_helper"
2
+
3
+ class SearchOptionTest < Minitest::Test
4
+ TerminalStub = Struct.new(:winsize) do
5
+ def write(_data); end
6
+ end
7
+
8
+ def setup
9
+ @editor = RuVim::Editor.new
10
+ @buf = @editor.add_empty_buffer
11
+ @win = @editor.add_window(buffer_id: @buf.id)
12
+ @gc = RuVim::GlobalCommands.instance
13
+ end
14
+
15
+ def test_ignorecase_and_smartcase_affect_search_regex
16
+ regex = @gc.send(:compile_search_regex, "abc", editor: @editor, window: @win, buffer: @buf)
17
+ refute regex.match?("ABC")
18
+
19
+ @editor.set_option("ignorecase", true, scope: :global)
20
+ regex = @gc.send(:compile_search_regex, "abc", editor: @editor, window: @win, buffer: @buf)
21
+ assert regex.match?("ABC")
22
+
23
+ @editor.set_option("smartcase", true, scope: :global)
24
+ regex = @gc.send(:compile_search_regex, "Abc", editor: @editor, window: @win, buffer: @buf)
25
+ refute regex.match?("abc")
26
+ end
27
+
28
+ def test_hlsearch_option_controls_search_highlight
29
+ screen = RuVim::Screen.new(terminal: TerminalStub.new([10, 40]))
30
+ @editor.set_last_search(pattern: "foo", direction: :forward)
31
+
32
+ cols = screen.send(:search_highlight_source_cols, @editor, "foo bar", source_col_offset: 0)
33
+ assert_equal true, cols[0]
34
+
35
+ @editor.set_option("hlsearch", false, scope: :global)
36
+ cols = screen.send(:search_highlight_source_cols, @editor, "foo bar", source_col_offset: 0)
37
+ assert_equal({}, cols)
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
2
+ require "minitest/autorun"
3
+ require "ruvim"
4
+
5
+ module RuVimTestHelpers
6
+ def fresh_editor
7
+ editor = RuVim::Editor.new
8
+ editor.ensure_bootstrap_buffer!
9
+ editor
10
+ end
11
+ end
12
+
13
+ class Minitest::Test
14
+ include RuVimTestHelpers
15
+ end