ruvim 0.1.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 (72) 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 +29 -0
  6. data/docs/command.md +101 -0
  7. data/docs/config.md +203 -84
  8. data/docs/done.md +21 -0
  9. data/docs/lib_cleanup_report.md +79 -0
  10. data/docs/plugin.md +13 -15
  11. data/docs/spec.md +195 -33
  12. data/docs/todo.md +183 -10
  13. data/docs/tutorial.md +1 -1
  14. data/docs/vim_diff.md +94 -171
  15. data/lib/ruvim/app.rb +1543 -172
  16. data/lib/ruvim/buffer.rb +35 -1
  17. data/lib/ruvim/cli.rb +12 -3
  18. data/lib/ruvim/clipboard.rb +2 -0
  19. data/lib/ruvim/command_invocation.rb +3 -1
  20. data/lib/ruvim/command_line.rb +2 -0
  21. data/lib/ruvim/command_registry.rb +2 -0
  22. data/lib/ruvim/config_dsl.rb +2 -0
  23. data/lib/ruvim/config_loader.rb +21 -5
  24. data/lib/ruvim/context.rb +2 -7
  25. data/lib/ruvim/dispatcher.rb +153 -13
  26. data/lib/ruvim/display_width.rb +28 -2
  27. data/lib/ruvim/editor.rb +622 -69
  28. data/lib/ruvim/ex_command_registry.rb +2 -0
  29. data/lib/ruvim/global_commands.rb +1386 -114
  30. data/lib/ruvim/highlighter.rb +16 -21
  31. data/lib/ruvim/input.rb +52 -29
  32. data/lib/ruvim/keymap_manager.rb +83 -0
  33. data/lib/ruvim/keyword_chars.rb +48 -0
  34. data/lib/ruvim/lang/base.rb +25 -0
  35. data/lib/ruvim/lang/csv.rb +18 -0
  36. data/lib/ruvim/lang/json.rb +18 -0
  37. data/lib/ruvim/lang/markdown.rb +170 -0
  38. data/lib/ruvim/lang/ruby.rb +236 -0
  39. data/lib/ruvim/lang/scheme.rb +44 -0
  40. data/lib/ruvim/lang/tsv.rb +19 -0
  41. data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
  42. data/lib/ruvim/rich_view/table_renderer.rb +176 -0
  43. data/lib/ruvim/rich_view.rb +93 -0
  44. data/lib/ruvim/screen.rb +851 -119
  45. data/lib/ruvim/terminal.rb +18 -1
  46. data/lib/ruvim/text_metrics.rb +28 -0
  47. data/lib/ruvim/version.rb +2 -2
  48. data/lib/ruvim/window.rb +37 -10
  49. data/lib/ruvim.rb +15 -0
  50. data/test/app_completion_test.rb +174 -0
  51. data/test/app_dot_repeat_test.rb +13 -0
  52. data/test/app_motion_test.rb +110 -2
  53. data/test/app_scenario_test.rb +998 -0
  54. data/test/app_startup_test.rb +197 -0
  55. data/test/arglist_test.rb +113 -0
  56. data/test/buffer_test.rb +49 -30
  57. data/test/config_loader_test.rb +37 -0
  58. data/test/dispatcher_test.rb +438 -0
  59. data/test/display_width_test.rb +18 -0
  60. data/test/editor_register_test.rb +23 -0
  61. data/test/fixtures/render_basic_snapshot.txt +7 -8
  62. data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
  63. data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
  64. data/test/highlighter_test.rb +121 -0
  65. data/test/indent_test.rb +201 -0
  66. data/test/input_screen_integration_test.rb +65 -14
  67. data/test/markdown_renderer_test.rb +279 -0
  68. data/test/on_save_hook_test.rb +150 -0
  69. data/test/rich_view_test.rb +478 -0
  70. data/test/screen_test.rb +470 -0
  71. data/test/window_test.rb +26 -0
  72. metadata +37 -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
@@ -76,6 +78,15 @@ module RuVim
76
78
  end
77
79
 
78
80
  w = RuVim::DisplayWidth.cell_width(ch, col: display_col, tabstop:)
81
+ if terminal_unsafe_control_char?(ch)
82
+ w = [w, 1].max
83
+ break if display_col + w > max_width
84
+
85
+ cells << Cell.new(glyph: terminal_safe_placeholder(ch), source_col:, display_width: w)
86
+ display_col += w
87
+ source_col += 1
88
+ next
89
+ end
79
90
  break if display_col + w > max_width
80
91
 
81
92
  cells << Cell.new(glyph: ch, source_col:, display_width: w)
@@ -92,5 +103,22 @@ module RuVim
92
103
  out << (" " * [width.to_i - used, 0].max)
93
104
  out
94
105
  end
106
+
107
+ def terminal_safe_text(text)
108
+ text.to_s.each_char.map { |ch| terminal_unsafe_control_char?(ch) ? terminal_safe_placeholder(ch) : ch }.join
109
+ end
110
+
111
+ def terminal_unsafe_control_char?(ch)
112
+ return false if ch.nil? || ch.empty? || ch == "\t"
113
+
114
+ code = ch.ord
115
+ (code >= 0x00 && code < 0x20) || code == 0x7F || (0x80..0x9F).cover?(code)
116
+ rescue StandardError
117
+ false
118
+ end
119
+
120
+ def terminal_safe_placeholder(_ch)
121
+ "?"
122
+ end
95
123
  end
96
124
  end
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.1.0"
5
- end
4
+ VERSION = "0.3.0"
5
+ end
data/lib/ruvim/window.rb CHANGED
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuVim
2
4
  class Window
3
5
  attr_reader :id
4
- attr_accessor :buffer_id, :cursor_x, :cursor_y, :row_offset, :col_offset
6
+ attr_accessor :buffer_id, :row_offset, :col_offset
7
+ attr_reader :cursor_x, :cursor_y
5
8
  attr_reader :options
6
9
 
7
10
  def initialize(id:, buffer_id:)
@@ -11,16 +14,28 @@ module RuVim
11
14
  @cursor_y = 0
12
15
  @row_offset = 0
13
16
  @col_offset = 0
17
+ @preferred_x = nil
14
18
  @options = {}
15
19
  end
16
20
 
17
- def clamp_to_buffer(buffer)
21
+ def cursor_x=(value)
22
+ @cursor_x = value.to_i
23
+ @preferred_x = nil
24
+ end
25
+
26
+ def cursor_y=(value)
27
+ @cursor_y = value.to_i
28
+ end
29
+
30
+ def clamp_to_buffer(buffer, max_extra_col: 0)
18
31
  @cursor_y = [[@cursor_y, 0].max, buffer.line_count - 1].min
19
- @cursor_x = [[@cursor_x, 0].max, buffer.line_length(@cursor_y)].min
32
+ max_col = buffer.line_length(@cursor_y) + [max_extra_col.to_i, 0].max
33
+ @cursor_x = [[@cursor_x, 0].max, max_col].min
20
34
  self
21
35
  end
22
36
 
23
37
  def move_left(buffer, count = 1)
38
+ @preferred_x = nil
24
39
  count.times do
25
40
  break if @cursor_x <= 0
26
41
  @cursor_x = RuVim::TextMetrics.previous_grapheme_char_index(buffer.line_at(@cursor_y), @cursor_x)
@@ -29,6 +44,7 @@ module RuVim
29
44
  end
30
45
 
31
46
  def move_right(buffer, count = 1)
47
+ @preferred_x = nil
32
48
  count.times do
33
49
  line = buffer.line_at(@cursor_y)
34
50
  break if @cursor_x >= line.length
@@ -38,30 +54,41 @@ module RuVim
38
54
  end
39
55
 
40
56
  def move_up(buffer, count = 1)
57
+ desired_x = @preferred_x || @cursor_x
41
58
  @cursor_y -= count
42
59
  clamp_to_buffer(buffer)
60
+ @cursor_x = [desired_x, buffer.line_length(@cursor_y)].min
61
+ @preferred_x = desired_x
43
62
  end
44
63
 
45
64
  def move_down(buffer, count = 1)
65
+ desired_x = @preferred_x || @cursor_x
46
66
  @cursor_y += count
47
67
  clamp_to_buffer(buffer)
68
+ @cursor_x = [desired_x, buffer.line_length(@cursor_y)].min
69
+ @preferred_x = desired_x
48
70
  end
49
71
 
50
- def ensure_visible(buffer, height:, width:, tabstop: 2)
72
+ def ensure_visible(buffer, height:, width:, tabstop: 2, scrolloff: 0, sidescrolloff: 0)
51
73
  clamp_to_buffer(buffer)
74
+ so = [[scrolloff.to_i, 0].max, [height.to_i - 1, 0].max].min
52
75
 
53
- @row_offset = @cursor_y if @cursor_y < @row_offset
54
- @row_offset = @cursor_y - height + 1 if @cursor_y >= @row_offset + height
76
+ top_target = @cursor_y - so
77
+ bottom_target = @cursor_y + so
78
+ @row_offset = top_target if top_target < @row_offset
79
+ @row_offset = bottom_target - height + 1 if bottom_target >= @row_offset + height
55
80
  @row_offset = 0 if @row_offset.negative?
56
81
 
57
82
  line = buffer.line_at(@cursor_y)
58
83
  cursor_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @cursor_x, tabstop:)
59
84
  offset_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @col_offset, tabstop:)
85
+ sso = [[sidescrolloff.to_i, 0].max, [width.to_i - 1, 0].max].min
60
86
 
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
87
+ if cursor_screen_col < offset_screen_col + sso
88
+ target_left = [cursor_screen_col - sso, 0].max
89
+ @col_offset = RuVim::TextMetrics.char_index_for_screen_col(line, target_left, tabstop:)
90
+ elsif cursor_screen_col >= offset_screen_col + width - sso
91
+ target_left = cursor_screen_col - width + sso + 1
65
92
  @col_offset = RuVim::TextMetrics.char_index_for_screen_col(line, target_left, tabstop:, align: :ceil)
66
93
  end
67
94
  @col_offset = 0 if @col_offset.negative?
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
@@ -8,8 +10,14 @@ end
8
10
  require_relative "ruvim/version"
9
11
  require_relative "ruvim/command_invocation"
10
12
  require_relative "ruvim/display_width"
13
+ require_relative "ruvim/keyword_chars"
11
14
  require_relative "ruvim/text_metrics"
12
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"
13
21
  require_relative "ruvim/highlighter"
14
22
  require_relative "ruvim/context"
15
23
  require_relative "ruvim/buffer"
@@ -23,6 +31,13 @@ require_relative "ruvim/keymap_manager"
23
31
  require_relative "ruvim/command_line"
24
32
  require_relative "ruvim/input"
25
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"
26
41
  require_relative "ruvim/screen"
27
42
  require_relative "ruvim/config_dsl"
28
43
  require_relative "ruvim/config_loader"
@@ -1,4 +1,6 @@
1
1
  require_relative "test_helper"
2
+ require "tmpdir"
3
+ require "fileutils"
2
4
 
3
5
  class AppCompletionTest < Minitest::Test
4
6
  def setup
@@ -36,4 +38,176 @@ class AppCompletionTest < Minitest::Test
36
38
  assert_equal "foobar", b.line_at(0)
37
39
  assert_equal 6, @editor.current_window.cursor_x
38
40
  end
41
+
42
+ def test_path_completion_respects_wildignore
43
+ Dir.mktmpdir("ruvim-complete") do |dir|
44
+ File.write(File.join(dir, "a.rb"), "")
45
+ File.write(File.join(dir, "a.o"), "")
46
+ @editor.set_option("wildignore", "*.o", scope: :global)
47
+
48
+ matches = @app.send(:path_completion_candidates, File.join(dir, "a"))
49
+
50
+ assert_includes matches, File.join(dir, "a.rb")
51
+ refute_includes matches, File.join(dir, "a.o")
52
+ end
53
+ end
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
+
84
+ def test_command_line_completion_respects_wildmode_list_full_and_wildmenu
85
+ @editor.materialize_intro_buffer!
86
+ @editor.set_option("wildmode", "list,full", scope: :global)
87
+ @editor.set_option("wildmenu", true, scope: :global)
88
+
89
+ Dir.mktmpdir("ruvim-wild") do |dir|
90
+ a = File.join(dir, "aa.txt")
91
+ b = File.join(dir, "ab.txt")
92
+ File.write(a, "")
93
+ File.write(b, "")
94
+
95
+ @editor.enter_command_line_mode(":")
96
+ cmd = @editor.command_line
97
+ cmd.replace_text("e #{File.join(dir, "a")}")
98
+
99
+ @app.send(:command_line_complete)
100
+ first = cmd.text.dup
101
+ first_msg = @editor.message.dup
102
+ @app.send(:command_line_complete)
103
+ second = cmd.text.dup
104
+ second_msg = @editor.message.dup
105
+
106
+ assert_equal "e #{File.join(dir, "a")}", first
107
+ refute_equal first, second
108
+ assert_includes first_msg, "aa.txt"
109
+ assert_includes second_msg, "["
110
+ assert([a, b].any? { |p| second.end_with?(p) })
111
+ end
112
+ end
113
+
114
+ def test_command_line_completion_accepts_wildmode_list_colon_full
115
+ @editor.materialize_intro_buffer!
116
+ @editor.set_option("wildmode", "list:full", scope: :global)
117
+
118
+ Dir.mktmpdir("ruvim-wild-colon") do |dir|
119
+ a = File.join(dir, "aa.txt")
120
+ b = File.join(dir, "ab.txt")
121
+ File.write(a, "")
122
+ File.write(b, "")
123
+
124
+ @editor.enter_command_line_mode(":")
125
+ cmd = @editor.command_line
126
+ cmd.replace_text("e #{File.join(dir, "a")}")
127
+
128
+ @app.send(:command_line_complete)
129
+ assert_equal "e #{File.join(dir, "a")}", cmd.text
130
+
131
+ @app.send(:command_line_complete)
132
+ assert([a, b].any? { |p| cmd.text.end_with?(p) })
133
+ end
134
+ end
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
+
179
+ def test_insert_completion_respects_completeopt_noselect_and_pumheight
180
+ @editor.materialize_intro_buffer!
181
+ @editor.set_option("completeopt", "menu,menuone,noselect", scope: :global)
182
+ @editor.set_option("pumheight", 1, scope: :global)
183
+ b = @editor.current_buffer
184
+ b.replace_all_lines!(["fo", "foobar", "fookey"])
185
+ @editor.current_window.cursor_y = 0
186
+ @editor.current_window.cursor_x = 2
187
+ @editor.enter_insert_mode
188
+
189
+ @app.send(:handle_insert_key, :ctrl_n)
190
+ assert_equal "fo", b.line_at(0)
191
+ assert_includes @editor.message, "..."
192
+
193
+ @app.send(:handle_insert_key, :ctrl_n)
194
+ assert_equal "foobar", b.line_at(0)
195
+ end
196
+
197
+ def test_insert_completion_respects_completeopt_noinsert
198
+ @editor.materialize_intro_buffer!
199
+ @editor.set_option("completeopt", "menu,noinsert", scope: :global)
200
+ b = @editor.current_buffer
201
+ b.replace_all_lines!(["fo", "foobar", "fookey"])
202
+ @editor.current_window.cursor_y = 0
203
+ @editor.current_window.cursor_x = 2
204
+ @editor.enter_insert_mode
205
+
206
+ @app.send(:handle_insert_key, :ctrl_n)
207
+ assert_equal "fo", b.line_at(0)
208
+ assert_includes @editor.message, "["
209
+
210
+ @app.send(:handle_insert_key, :ctrl_n)
211
+ assert_equal "foobar", b.line_at(0)
212
+ end
39
213
  end
@@ -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
@@ -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,113 @@ 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
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
73
181
  end