ruvim 0.1.0 → 0.2.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.
data/lib/ruvim/window.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  module RuVim
2
2
  class Window
3
3
  attr_reader :id
4
- attr_accessor :buffer_id, :cursor_x, :cursor_y, :row_offset, :col_offset
4
+ attr_accessor :buffer_id, :row_offset, :col_offset
5
+ attr_reader :cursor_x, :cursor_y
5
6
  attr_reader :options
6
7
 
7
8
  def initialize(id:, buffer_id:)
@@ -11,16 +12,28 @@ module RuVim
11
12
  @cursor_y = 0
12
13
  @row_offset = 0
13
14
  @col_offset = 0
15
+ @preferred_x = nil
14
16
  @options = {}
15
17
  end
16
18
 
17
- def clamp_to_buffer(buffer)
19
+ def cursor_x=(value)
20
+ @cursor_x = value.to_i
21
+ @preferred_x = nil
22
+ end
23
+
24
+ def cursor_y=(value)
25
+ @cursor_y = value.to_i
26
+ end
27
+
28
+ def clamp_to_buffer(buffer, max_extra_col: 0)
18
29
  @cursor_y = [[@cursor_y, 0].max, buffer.line_count - 1].min
19
- @cursor_x = [[@cursor_x, 0].max, buffer.line_length(@cursor_y)].min
30
+ max_col = buffer.line_length(@cursor_y) + [max_extra_col.to_i, 0].max
31
+ @cursor_x = [[@cursor_x, 0].max, max_col].min
20
32
  self
21
33
  end
22
34
 
23
35
  def move_left(buffer, count = 1)
36
+ @preferred_x = nil
24
37
  count.times do
25
38
  break if @cursor_x <= 0
26
39
  @cursor_x = RuVim::TextMetrics.previous_grapheme_char_index(buffer.line_at(@cursor_y), @cursor_x)
@@ -29,6 +42,7 @@ module RuVim
29
42
  end
30
43
 
31
44
  def move_right(buffer, count = 1)
45
+ @preferred_x = nil
32
46
  count.times do
33
47
  line = buffer.line_at(@cursor_y)
34
48
  break if @cursor_x >= line.length
@@ -38,30 +52,41 @@ module RuVim
38
52
  end
39
53
 
40
54
  def move_up(buffer, count = 1)
55
+ desired_x = @preferred_x || @cursor_x
41
56
  @cursor_y -= count
42
57
  clamp_to_buffer(buffer)
58
+ @cursor_x = [desired_x, buffer.line_length(@cursor_y)].min
59
+ @preferred_x = desired_x
43
60
  end
44
61
 
45
62
  def move_down(buffer, count = 1)
63
+ desired_x = @preferred_x || @cursor_x
46
64
  @cursor_y += count
47
65
  clamp_to_buffer(buffer)
66
+ @cursor_x = [desired_x, buffer.line_length(@cursor_y)].min
67
+ @preferred_x = desired_x
48
68
  end
49
69
 
50
- def ensure_visible(buffer, height:, width:, tabstop: 2)
70
+ def ensure_visible(buffer, height:, width:, tabstop: 2, scrolloff: 0, sidescrolloff: 0)
51
71
  clamp_to_buffer(buffer)
72
+ so = [[scrolloff.to_i, 0].max, [height.to_i - 1, 0].max].min
52
73
 
53
- @row_offset = @cursor_y if @cursor_y < @row_offset
54
- @row_offset = @cursor_y - height + 1 if @cursor_y >= @row_offset + height
74
+ top_target = @cursor_y - so
75
+ bottom_target = @cursor_y + so
76
+ @row_offset = top_target if top_target < @row_offset
77
+ @row_offset = bottom_target - height + 1 if bottom_target >= @row_offset + height
55
78
  @row_offset = 0 if @row_offset.negative?
56
79
 
57
80
  line = buffer.line_at(@cursor_y)
58
81
  cursor_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @cursor_x, tabstop:)
59
82
  offset_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @col_offset, tabstop:)
83
+ sso = [[sidescrolloff.to_i, 0].max, [width.to_i - 1, 0].max].min
60
84
 
61
- if cursor_screen_col < offset_screen_col
62
- @col_offset = RuVim::TextMetrics.char_index_for_screen_col(line, cursor_screen_col, tabstop:)
63
- elsif cursor_screen_col >= offset_screen_col + width
64
- target_left = cursor_screen_col - width + 1
85
+ if cursor_screen_col < offset_screen_col + sso
86
+ target_left = [cursor_screen_col - sso, 0].max
87
+ @col_offset = RuVim::TextMetrics.char_index_for_screen_col(line, target_left, tabstop:)
88
+ elsif cursor_screen_col >= offset_screen_col + width - sso
89
+ target_left = cursor_screen_col - width + sso + 1
65
90
  @col_offset = RuVim::TextMetrics.char_index_for_screen_col(line, target_left, tabstop:, align: :ceil)
66
91
  end
67
92
  @col_offset = 0 if @col_offset.negative?
data/lib/ruvim.rb CHANGED
@@ -8,6 +8,7 @@ end
8
8
  require_relative "ruvim/version"
9
9
  require_relative "ruvim/command_invocation"
10
10
  require_relative "ruvim/display_width"
11
+ require_relative "ruvim/keyword_chars"
11
12
  require_relative "ruvim/text_metrics"
12
13
  require_relative "ruvim/clipboard"
13
14
  require_relative "ruvim/highlighter"
@@ -1,4 +1,5 @@
1
1
  require_relative "test_helper"
2
+ require "tmpdir"
2
3
 
3
4
  class AppCompletionTest < Minitest::Test
4
5
  def setup
@@ -36,4 +37,104 @@ class AppCompletionTest < Minitest::Test
36
37
  assert_equal "foobar", b.line_at(0)
37
38
  assert_equal 6, @editor.current_window.cursor_x
38
39
  end
40
+
41
+ def test_path_completion_respects_wildignore
42
+ Dir.mktmpdir("ruvim-complete") do |dir|
43
+ File.write(File.join(dir, "a.rb"), "")
44
+ File.write(File.join(dir, "a.o"), "")
45
+ @editor.set_option("wildignore", "*.o", scope: :global)
46
+
47
+ matches = @app.send(:path_completion_candidates, File.join(dir, "a"))
48
+
49
+ assert_includes matches, File.join(dir, "a.rb")
50
+ refute_includes matches, File.join(dir, "a.o")
51
+ end
52
+ end
53
+
54
+ def test_command_line_completion_respects_wildmode_list_full_and_wildmenu
55
+ @editor.materialize_intro_buffer!
56
+ @editor.set_option("wildmode", "list,full", scope: :global)
57
+ @editor.set_option("wildmenu", true, scope: :global)
58
+
59
+ Dir.mktmpdir("ruvim-wild") do |dir|
60
+ a = File.join(dir, "aa.txt")
61
+ b = File.join(dir, "ab.txt")
62
+ File.write(a, "")
63
+ File.write(b, "")
64
+
65
+ @editor.enter_command_line_mode(":")
66
+ cmd = @editor.command_line
67
+ cmd.replace_text("e #{File.join(dir, "a")}")
68
+
69
+ @app.send(:command_line_complete)
70
+ first = cmd.text.dup
71
+ first_msg = @editor.message.dup
72
+ @app.send(:command_line_complete)
73
+ second = cmd.text.dup
74
+ second_msg = @editor.message.dup
75
+
76
+ assert_equal "e #{File.join(dir, "a")}", first
77
+ refute_equal first, second
78
+ assert_includes first_msg, "aa.txt"
79
+ assert_includes second_msg, "["
80
+ assert([a, b].any? { |p| second.end_with?(p) })
81
+ end
82
+ end
83
+
84
+ def test_command_line_completion_accepts_wildmode_list_colon_full
85
+ @editor.materialize_intro_buffer!
86
+ @editor.set_option("wildmode", "list:full", scope: :global)
87
+
88
+ Dir.mktmpdir("ruvim-wild-colon") do |dir|
89
+ a = File.join(dir, "aa.txt")
90
+ b = File.join(dir, "ab.txt")
91
+ File.write(a, "")
92
+ File.write(b, "")
93
+
94
+ @editor.enter_command_line_mode(":")
95
+ cmd = @editor.command_line
96
+ cmd.replace_text("e #{File.join(dir, "a")}")
97
+
98
+ @app.send(:command_line_complete)
99
+ assert_equal "e #{File.join(dir, "a")}", cmd.text
100
+
101
+ @app.send(:command_line_complete)
102
+ assert([a, b].any? { |p| cmd.text.end_with?(p) })
103
+ end
104
+ end
105
+
106
+ def test_insert_completion_respects_completeopt_noselect_and_pumheight
107
+ @editor.materialize_intro_buffer!
108
+ @editor.set_option("completeopt", "menu,menuone,noselect", scope: :global)
109
+ @editor.set_option("pumheight", 1, scope: :global)
110
+ b = @editor.current_buffer
111
+ b.replace_all_lines!(["fo", "foobar", "fookey"])
112
+ @editor.current_window.cursor_y = 0
113
+ @editor.current_window.cursor_x = 2
114
+ @editor.enter_insert_mode
115
+
116
+ @app.send(:handle_insert_key, :ctrl_n)
117
+ assert_equal "fo", b.line_at(0)
118
+ assert_includes @editor.message, "..."
119
+
120
+ @app.send(:handle_insert_key, :ctrl_n)
121
+ assert_equal "foobar", b.line_at(0)
122
+ end
123
+
124
+ def test_insert_completion_respects_completeopt_noinsert
125
+ @editor.materialize_intro_buffer!
126
+ @editor.set_option("completeopt", "menu,noinsert", scope: :global)
127
+ b = @editor.current_buffer
128
+ b.replace_all_lines!(["fo", "foobar", "fookey"])
129
+ @editor.current_window.cursor_y = 0
130
+ @editor.current_window.cursor_x = 2
131
+ @editor.enter_insert_mode
132
+
133
+ @app.send(:handle_insert_key, :ctrl_n)
134
+ assert_equal "fo", b.line_at(0)
135
+ assert_includes @editor.message, "["
136
+
137
+ @app.send(:handle_insert_key, :ctrl_n)
138
+ assert_equal "foobar", b.line_at(0)
139
+ end
39
140
  end
@@ -57,8 +57,7 @@ class AppMotionTest < Minitest::Test
57
57
  @editor.current_window.cursor_y = 0
58
58
  @editor.current_window.cursor_x = 0
59
59
 
60
- screen = @app.instance_variable_get(:@screen)
61
- screen.define_singleton_method(:current_window_view_height) { |_editor| 5 }
60
+ @editor.current_window_view_height_hint = 5
62
61
 
63
62
  @app.send(:handle_normal_key, :pagedown)
64
63
  assert_equal 4, @editor.current_window.cursor_y
@@ -70,4 +69,100 @@ class AppMotionTest < Minitest::Test
70
69
  @app.send(:handle_normal_key, :pageup)
71
70
  assert_equal 8, @editor.current_window.cursor_y
72
71
  end
72
+
73
+ def test_ctrl_d_u_and_ctrl_f_b_move_by_half_and_full_page
74
+ b = @editor.current_buffer
75
+ b.replace_all_lines!((1..40).map { |i| "line#{i}" })
76
+ @editor.current_window.cursor_y = 0
77
+ @editor.current_window.cursor_x = 0
78
+
79
+ @editor.current_window_view_height_hint = 10
80
+
81
+ @app.send(:handle_normal_key, :ctrl_d)
82
+ assert_equal 5, @editor.current_window.cursor_y
83
+
84
+ @app.send(:handle_normal_key, :ctrl_u)
85
+ assert_equal 0, @editor.current_window.cursor_y
86
+
87
+ @app.send(:handle_normal_key, :ctrl_f)
88
+ assert_equal 9, @editor.current_window.cursor_y
89
+
90
+ @app.send(:handle_normal_key, :ctrl_b)
91
+ assert_equal 0, @editor.current_window.cursor_y
92
+ end
93
+
94
+ def test_ctrl_e_and_ctrl_y_scroll_window_without_primary_cursor_motion
95
+ b = @editor.current_buffer
96
+ b.replace_all_lines!((1..40).map { |i| "line#{i}" })
97
+ @editor.current_window.cursor_y = 6
98
+ @editor.current_window.cursor_x = 0
99
+ @editor.current_window.row_offset = 5
100
+
101
+ @editor.current_window_view_height_hint = 10
102
+
103
+ @app.send(:handle_normal_key, :ctrl_e)
104
+ assert_equal 6, @editor.current_window.row_offset
105
+ assert_equal 6, @editor.current_window.cursor_y
106
+
107
+ @app.send(:handle_normal_key, :ctrl_y)
108
+ assert_equal 5, @editor.current_window.row_offset
109
+ assert_equal 6, @editor.current_window.cursor_y
110
+ end
111
+
112
+ def test_ctrl_d_can_be_overridden_by_normal_keymap
113
+ b = @editor.current_buffer
114
+ b.replace_all_lines!((1..40).map { |i| "line#{i}" })
115
+ @editor.current_window.cursor_y = 0
116
+ @editor.current_window.cursor_x = 0
117
+
118
+ keymaps = @app.instance_variable_get(:@keymaps)
119
+ keymaps.bind(:normal, ["<C-d>"], "cursor.down")
120
+
121
+ @app.send(:handle_normal_key, :ctrl_d)
122
+ assert_equal 1, @editor.current_window.cursor_y
123
+ end
124
+
125
+ def test_pagedown_can_be_overridden_by_normal_keymap
126
+ b = @editor.current_buffer
127
+ b.replace_all_lines!((1..40).map { |i| "line#{i}" })
128
+ @editor.current_window.cursor_y = 0
129
+ @editor.current_window.cursor_x = 0
130
+
131
+ keymaps = @app.instance_variable_get(:@keymaps)
132
+ keymaps.bind(:normal, ["<PageDown>"], "cursor.down")
133
+
134
+ @app.send(:handle_normal_key, :pagedown)
135
+ assert_equal 1, @editor.current_window.cursor_y
136
+ end
137
+
138
+ def test_virtualedit_onemore_allows_right_move_past_eol_once
139
+ b = @editor.current_buffer
140
+ b.replace_all_lines!(["abc"])
141
+ @editor.current_window.cursor_y = 0
142
+ @editor.current_window.cursor_x = 3
143
+
144
+ press("l")
145
+ assert_equal 3, @editor.current_window.cursor_x
146
+
147
+ @editor.set_option("virtualedit", "onemore", scope: :global)
148
+ press("l")
149
+ assert_equal 4, @editor.current_window.cursor_x
150
+
151
+ press("l")
152
+ assert_equal 4, @editor.current_window.cursor_x
153
+ end
154
+
155
+ def test_virtualedit_all_allows_multiple_columns_past_eol
156
+ b = @editor.current_buffer
157
+ b.replace_all_lines!(["abc"])
158
+ @editor.current_window.cursor_y = 0
159
+ @editor.current_window.cursor_x = 3
160
+ @editor.set_option("virtualedit", "all", scope: :global)
161
+
162
+ press("l", "l", "l")
163
+ assert_equal 6, @editor.current_window.cursor_x
164
+
165
+ press("h", "h")
166
+ assert_equal 4, @editor.current_window.cursor_x
167
+ end
73
168
  end
@@ -1,9 +1,12 @@
1
1
  require_relative "test_helper"
2
+ require "fileutils"
3
+ require "tmpdir"
2
4
 
3
5
  class AppScenarioTest < Minitest::Test
4
6
  def setup
5
7
  @app = RuVim::App.new(clean: true)
6
8
  @editor = @app.instance_variable_get(:@editor)
9
+ @dispatcher = @app.instance_variable_get(:@dispatcher)
7
10
  @editor.materialize_intro_buffer!
8
11
  end
9
12
 
@@ -74,4 +77,271 @@ class AppScenarioTest < Minitest::Test
74
77
 
75
78
  assert_equal ["bc", "bc", "bc"], @editor.current_buffer.lines
76
79
  end
80
+
81
+ def test_expandtab_and_autoindent_in_insert_mode
82
+ @editor.set_option("expandtab", true, scope: :buffer)
83
+ @editor.set_option("tabstop", 4, scope: :buffer)
84
+ @editor.set_option("softtabstop", 4, scope: :buffer)
85
+ @editor.set_option("autoindent", true, scope: :buffer)
86
+ @editor.current_buffer.replace_all_lines!([" foo"])
87
+
88
+ feed("A", :ctrl_i, :enter, :escape)
89
+
90
+ assert_equal [" foo ", " "], @editor.current_buffer.lines
91
+ end
92
+
93
+ def test_smartindent_adds_shiftwidth_after_open_brace
94
+ @editor.set_option("autoindent", true, scope: :buffer)
95
+ @editor.set_option("smartindent", true, scope: :buffer)
96
+ @editor.set_option("shiftwidth", 2, scope: :buffer)
97
+ @editor.current_buffer.replace_all_lines!(["if x {"])
98
+ @editor.current_window.cursor_y = 0
99
+ @editor.current_window.cursor_x = @editor.current_buffer.line_length(0)
100
+
101
+ feed("A", :enter, :escape)
102
+
103
+ assert_equal ["if x {", " "], @editor.current_buffer.lines
104
+ end
105
+
106
+ def test_incsearch_moves_cursor_while_typing_and_escape_restores
107
+ @editor.set_option("incsearch", true, scope: :global)
108
+ @editor.current_buffer.replace_all_lines!(["alpha", "beta", "gamma"])
109
+ @editor.current_window.cursor_y = 0
110
+ @editor.current_window.cursor_x = 0
111
+
112
+ feed("/", "b")
113
+ assert_equal :command_line, @editor.mode
114
+ assert_equal 1, @editor.current_window.cursor_y
115
+ assert_equal 0, @editor.current_window.cursor_x
116
+
117
+ feed(:escape)
118
+ assert_equal :normal, @editor.mode
119
+ assert_equal 0, @editor.current_window.cursor_y
120
+ assert_equal 0, @editor.current_window.cursor_x
121
+ end
122
+
123
+ def test_search_command_line_backspace_on_empty_cancels
124
+ @editor.current_buffer.replace_all_lines!(["alpha", "beta"])
125
+ @editor.current_window.cursor_y = 0
126
+ @editor.current_window.cursor_x = 0
127
+
128
+ feed("/")
129
+ assert_equal :command_line, @editor.mode
130
+ assert_equal "/", @editor.command_line.prefix
131
+ assert_equal "", @editor.command_line.text
132
+
133
+ feed(:backspace)
134
+
135
+ assert_equal :normal, @editor.mode
136
+ assert_equal 0, @editor.current_window.cursor_y
137
+ assert_equal 0, @editor.current_window.cursor_x
138
+ end
139
+
140
+ def test_lopen_enter_jumps_to_selected_location_and_returns_to_source_window
141
+ @editor.current_buffer.replace_all_lines!(["aa", "bb aa", "cc aa"])
142
+ source_window_id = @editor.current_window_id
143
+
144
+ @dispatcher.dispatch_ex(@editor, "lvimgrep /aa/")
145
+ @dispatcher.dispatch_ex(@editor, "lopen")
146
+
147
+ assert_equal :location_list, @editor.current_buffer.kind
148
+ assert_equal 2, @editor.window_count
149
+
150
+ # Header lines are: title, blank, then items...
151
+ @editor.current_window.cursor_y = 4 # 3rd item
152
+ feed(:enter)
153
+
154
+ refute_equal :location_list, @editor.current_buffer.kind
155
+ assert_equal source_window_id, @editor.current_window_id
156
+ assert_equal 2, @editor.current_window.cursor_y
157
+ assert_equal 3, @editor.current_window.cursor_x
158
+ end
159
+
160
+ def test_incsearch_submit_stays_on_previewed_match
161
+ @editor.set_option("incsearch", true, scope: :global)
162
+ @editor.current_buffer.replace_all_lines!(["foo", "bar", "baz", "bar"])
163
+ @editor.current_window.cursor_y = 0
164
+ @editor.current_window.cursor_x = 0
165
+
166
+ feed("/", "b", "a", "r", :enter)
167
+
168
+ assert_equal :normal, @editor.mode
169
+ assert_equal 1, @editor.current_window.cursor_y
170
+ assert_equal 0, @editor.current_window.cursor_x
171
+ end
172
+
173
+ def test_whichwrap_allows_h_and_l_to_cross_lines
174
+ @editor.set_option("whichwrap", "h,l", scope: :global)
175
+ @editor.current_buffer.replace_all_lines!(["ab", "cd"])
176
+ @editor.current_window.cursor_y = 1
177
+ @editor.current_window.cursor_x = 0
178
+
179
+ feed("h")
180
+ assert_equal [0, 2], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
181
+
182
+ feed("l")
183
+ assert_equal [1, 0], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
184
+ end
185
+
186
+ def test_iskeyword_affects_word_motion
187
+ @editor.set_option("iskeyword", "@,-", scope: :buffer)
188
+ @editor.current_buffer.replace_all_lines!(["foo-bar baz"])
189
+ @editor.current_window.cursor_y = 0
190
+ @editor.current_window.cursor_x = 0
191
+
192
+ feed("w")
193
+
194
+ assert_equal 8, @editor.current_window.cursor_x
195
+ end
196
+
197
+ def test_backspace_start_option_blocks_deleting_before_insert_start
198
+ @editor.set_option("backspace", "indent,eol", scope: :global)
199
+ @editor.current_buffer.replace_all_lines!(["ab"])
200
+ @editor.current_window.cursor_x = 1
201
+
202
+ feed("i", :backspace)
203
+
204
+ assert_equal ["ab"], @editor.current_buffer.lines
205
+ assert_equal 1, @editor.current_window.cursor_x
206
+ assert_equal :insert, @editor.mode
207
+ end
208
+
209
+ def test_backspace_eol_option_blocks_joining_previous_line
210
+ @editor.set_option("backspace", "start", scope: :global)
211
+ @editor.current_buffer.replace_all_lines!(["a", "b"])
212
+ @editor.current_window.cursor_y = 1
213
+ @editor.current_window.cursor_x = 0
214
+
215
+ feed("i", :backspace)
216
+
217
+ assert_equal ["a", "b"], @editor.current_buffer.lines
218
+ assert_equal [1, 0], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
219
+ end
220
+
221
+ def test_backspace_indent_allows_deleting_autoindent_before_insert_start
222
+ @editor.set_option("backspace", "indent,eol", scope: :global)
223
+ @editor.current_buffer.replace_all_lines!([" abc"])
224
+ @editor.current_window.cursor_x = 2
225
+
226
+ feed("i", :backspace)
227
+
228
+ assert_equal [" abc"], @editor.current_buffer.lines
229
+ assert_equal 1, @editor.current_window.cursor_x
230
+ end
231
+
232
+ def test_softtabstop_backspace_deletes_spaces_in_chunks_when_expandtab
233
+ @editor.set_option("expandtab", true, scope: :buffer)
234
+ @editor.set_option("tabstop", 4, scope: :buffer)
235
+ @editor.set_option("softtabstop", 4, scope: :buffer)
236
+ @editor.current_buffer.replace_all_lines!([" "])
237
+ @editor.current_window.cursor_x = 8
238
+
239
+ feed("i", :backspace)
240
+
241
+ assert_equal [" "], @editor.current_buffer.lines
242
+ assert_equal 4, @editor.current_window.cursor_x
243
+ end
244
+
245
+ def test_gf_uses_path_and_suffixesadd
246
+ Dir.mktmpdir("ruvim-gf") do |dir|
247
+ FileUtils.mkdir_p(File.join(dir, "lib"))
248
+ target = File.join(dir, "lib", "foo.rb")
249
+ File.write(target, "puts :ok\n")
250
+ @editor.current_buffer.path = File.join(dir, "main.txt")
251
+ @editor.current_buffer.replace_all_lines!(["foo"])
252
+ @editor.current_window.cursor_y = 0
253
+ @editor.current_window.cursor_x = 0
254
+ @editor.set_option("hidden", true, scope: :global)
255
+ @editor.set_option("path", "lib", scope: :buffer)
256
+ @editor.set_option("suffixesadd", ".rb", scope: :buffer)
257
+
258
+ feed("g", "f")
259
+
260
+ assert_equal File.expand_path(target), File.expand_path(@editor.current_buffer.path)
261
+ assert_equal "puts :ok", @editor.current_buffer.line_at(0)
262
+ end
263
+ end
264
+
265
+ def test_gf_supports_recursive_path_entry_with_double_star
266
+ Dir.mktmpdir("ruvim-gf-rec") do |dir|
267
+ FileUtils.mkdir_p(File.join(dir, "lib", "deep", "nest"))
268
+ target = File.join(dir, "lib", "deep", "nest", "foo.rb")
269
+ File.write(target, "puts :deep\n")
270
+ @editor.current_buffer.path = File.join(dir, "main.txt")
271
+ @editor.current_buffer.replace_all_lines!(["foo"])
272
+ @editor.current_window.cursor_y = 0
273
+ @editor.current_window.cursor_x = 0
274
+ @editor.set_option("hidden", true, scope: :global)
275
+ @editor.set_option("path", "lib/**", scope: :buffer)
276
+ @editor.set_option("suffixesadd", ".rb", scope: :buffer)
277
+
278
+ feed("g", "f")
279
+
280
+ assert_equal File.expand_path(target), File.expand_path(@editor.current_buffer.path)
281
+ end
282
+ end
283
+
284
+ def test_showmatch_message_respects_matchtime_and_clears
285
+ @editor.set_option("showmatch", true, scope: :global)
286
+ @editor.set_option("matchtime", 1, scope: :global) # 0.1 sec
287
+ @editor.current_buffer.replace_all_lines!([""])
288
+
289
+ feed("i", ")")
290
+ assert_equal "match", @editor.message
291
+
292
+ sleep 0.12
293
+ @app.send(:clear_expired_transient_message_if_any)
294
+ assert_equal "", @editor.message
295
+ end
296
+
297
+ def test_insert_arrow_left_respects_whichwrap
298
+ @editor.set_option("whichwrap", "left,right", scope: :global)
299
+ @editor.current_buffer.replace_all_lines!(["ab", "cd"])
300
+ @editor.current_window.cursor_y = 1
301
+ @editor.current_window.cursor_x = 0
302
+
303
+ feed("i", :left)
304
+
305
+ assert_equal :insert, @editor.mode
306
+ assert_equal [0, 2], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
307
+ end
308
+
309
+ def test_star_search_uses_iskeyword
310
+ @editor.set_option("iskeyword", "@,-", scope: :buffer)
311
+ @editor.current_buffer.replace_all_lines!(["foo-bar x", "foo y", "foo-bar z"])
312
+ @editor.current_window.cursor_y = 0
313
+ @editor.current_window.cursor_x = 1
314
+
315
+ feed("*")
316
+
317
+ assert_equal 2, @editor.current_window.cursor_y
318
+ assert_equal 0, @editor.current_window.cursor_x
319
+ assert_includes @editor.last_search[:pattern], "foo\\-bar"
320
+ end
321
+
322
+ def test_unknown_key_error_clears_on_next_successful_key
323
+ @editor.current_buffer.replace_all_lines!(["a", "b"])
324
+ @editor.current_window.cursor_y = 0
325
+
326
+ feed("z")
327
+ assert @editor.message_error?
328
+ assert_match(/Unknown key:/, @editor.message)
329
+
330
+ feed("j")
331
+ refute @editor.message_error?
332
+ assert_equal "", @editor.message
333
+ assert_equal 1, @editor.current_window.cursor_y
334
+ end
335
+
336
+ def test_normal_message_clears_on_next_key
337
+ @editor.current_buffer.replace_all_lines!(["a", "b"])
338
+ @editor.current_window.cursor_y = 0
339
+ @editor.echo("written")
340
+
341
+ feed("j")
342
+
343
+ refute @editor.message_error?
344
+ assert_equal "", @editor.message
345
+ assert_equal 1, @editor.current_window.cursor_y
346
+ end
77
347
  end
@@ -131,6 +131,16 @@ class AppStartupTest < Minitest::Test
131
131
  assert_match(/Restricted mode/, editor.message)
132
132
  end
133
133
 
134
+ def test_restricted_mode_disables_ex_shell
135
+ app = RuVim::App.new(clean: true, restricted: true)
136
+ editor = app.instance_variable_get(:@editor)
137
+ dispatcher = app.instance_variable_get(:@dispatcher)
138
+
139
+ dispatcher.dispatch_ex(editor, "!echo hi")
140
+
141
+ assert_match(/Restricted mode/, editor.message)
142
+ end
143
+
134
144
  def test_verbose_logs_startup_and_startup_ex_actions
135
145
  log = StringIO.new
136
146
  app = RuVim::App.new(
@@ -0,0 +1,37 @@
1
+ require_relative "test_helper"
2
+ require "fileutils"
3
+ require "tmpdir"
4
+
5
+ class ConfigLoaderTest < Minitest::Test
6
+ def setup
7
+ @loader = RuVim::ConfigLoader.new(
8
+ command_registry: RuVim::CommandRegistry.instance,
9
+ ex_registry: RuVim::ExCommandRegistry.instance,
10
+ keymaps: RuVim::KeymapManager.new,
11
+ command_host: RuVim::GlobalCommands.instance
12
+ )
13
+ end
14
+
15
+ def test_load_ftplugin_rejects_path_traversal_filetype
16
+ Dir.mktmpdir("ruvim-ftplugin") do |dir|
17
+ xdg = File.join(dir, "xdg")
18
+ FileUtils.mkdir_p(File.join(xdg, "ruvim", "ftplugin"))
19
+ evil = File.join(xdg, "ruvim", "evil.rb")
20
+ File.write(evil, "raise 'should not load'\n")
21
+
22
+ editor = RuVim::Editor.new
23
+ buffer = editor.add_empty_buffer
24
+ editor.add_window(buffer_id: buffer.id)
25
+ buffer.options["filetype"] = "../evil"
26
+
27
+ old = ENV["XDG_CONFIG_HOME"]
28
+ ENV["XDG_CONFIG_HOME"] = xdg
29
+ begin
30
+ assert_nil @loader.load_ftplugin!(editor, buffer)
31
+ refute_equal "../evil", buffer.options["__ftplugin_loaded__"]
32
+ ensure
33
+ ENV["XDG_CONFIG_HOME"] = old
34
+ end
35
+ end
36
+ end
37
+ end