ruvim 0.3.0 → 0.6.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.
- checksums.yaml +4 -4
- data/AGENTS.md +68 -7
- data/README.md +30 -7
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +18 -1
- data/docs/command.md +156 -10
- data/docs/config.md +10 -2
- data/docs/done.md +23 -0
- data/docs/spec.md +162 -25
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +33 -1
- data/docs/vim_diff.md +31 -8
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +246 -2525
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +43 -20
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +74 -80
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +255 -0
- data/lib/ruvim/git/branch.rb +112 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +125 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +351 -77
- data/lib/ruvim/highlighter.rb +4 -11
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +43 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +40 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +30 -7
- data/lib/ruvim/screen.rb +135 -84
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +31 -4
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +65 -16
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +182 -8
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +77 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +134 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/display_width_test.rb +41 -0
- data/test/ex_command_registry_test.rb +106 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +198 -0
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +792 -0
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +169 -0
- data/test/indent_test.rb +223 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +279 -23
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/search_option_test.rb +19 -0
- data/test/stream_test.rb +165 -0
- data/test/test_helper.rb +9 -0
- data/test/window_test.rb +59 -0
- metadata +68 -2
data/lib/ruvim/text_metrics.rb
CHANGED
|
@@ -4,15 +4,21 @@ module RuVim
|
|
|
4
4
|
module TextMetrics
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
Cell
|
|
7
|
+
class Cell
|
|
8
|
+
attr_reader :glyph, :source_col, :display_width
|
|
9
|
+
|
|
10
|
+
def initialize(glyph, source_col, display_width)
|
|
11
|
+
@glyph = glyph
|
|
12
|
+
@source_col = source_col
|
|
13
|
+
@display_width = display_width
|
|
14
|
+
end
|
|
15
|
+
end
|
|
8
16
|
|
|
9
|
-
# Cursor positions in RuVim are currently "character index" (Ruby String#[] index on UTF-8),
|
|
10
|
-
# not byte offsets. Grapheme-aware movement is layered on top of that.
|
|
11
17
|
def previous_grapheme_char_index(line, char_index)
|
|
12
|
-
idx = [char_index
|
|
18
|
+
idx = [char_index, 0].max
|
|
13
19
|
return 0 if idx <= 0
|
|
14
20
|
|
|
15
|
-
left = line
|
|
21
|
+
left = line[0...idx].to_s
|
|
16
22
|
clusters = left.scan(/\X/)
|
|
17
23
|
return 0 if clusters.empty?
|
|
18
24
|
|
|
@@ -21,7 +27,7 @@ module RuVim
|
|
|
21
27
|
|
|
22
28
|
def next_grapheme_char_index(line, char_index)
|
|
23
29
|
s = line.to_s
|
|
24
|
-
idx = [[char_index
|
|
30
|
+
idx = [[char_index, 0].max, s.length].min
|
|
25
31
|
return s.length if idx >= s.length
|
|
26
32
|
|
|
27
33
|
rest = s[idx..].to_s
|
|
@@ -32,93 +38,106 @@ module RuVim
|
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def screen_col_for_char_index(line, char_index, tabstop: 2)
|
|
35
|
-
idx = [char_index
|
|
36
|
-
prefix = line
|
|
41
|
+
idx = [char_index, 0].max
|
|
42
|
+
prefix = line[0...idx].to_s
|
|
37
43
|
RuVim::DisplayWidth.display_width(prefix, tabstop:)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def char_index_for_screen_col(line, target_screen_col, tabstop: 2, align: :floor)
|
|
43
|
-
s = line.to_s
|
|
44
|
-
target = [target_screen_col.to_i, 0].max
|
|
45
|
-
screen_col = 0
|
|
46
|
-
char_index = 0
|
|
47
|
-
|
|
48
|
-
s.scan(/\X/).each do |cluster|
|
|
49
|
-
width = RuVim::DisplayWidth.display_width(cluster, tabstop:, start_col: screen_col)
|
|
50
|
-
if screen_col + width > target
|
|
51
|
-
return align == :ceil ? (char_index + cluster.length) : char_index
|
|
52
|
-
end
|
|
46
|
+
if defined?(RuVim::TextMetricsExt)
|
|
47
|
+
# ---- C extension paths ----
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
def clip_cells_for_width(text, width, source_col_start: 0, tabstop: 2)
|
|
50
|
+
TextMetricsExt.clip_cells_for_width(text, width, source_col_start:, tabstop:)
|
|
56
51
|
end
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
def char_index_for_screen_col(line, target_screen_col, tabstop: 2, align: :floor)
|
|
54
|
+
TextMetricsExt.char_index_for_screen_col(line, target_screen_col, tabstop:, align:)
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
# ---- Pure Ruby fallback ----
|
|
58
|
+
|
|
59
|
+
def char_index_for_screen_col(line, target_screen_col, tabstop: 2, align: :floor)
|
|
60
|
+
s = line.to_s
|
|
61
|
+
target = [target_screen_col, 0].max
|
|
62
|
+
screen_col = 0
|
|
63
|
+
char_index = 0
|
|
64
|
+
|
|
65
|
+
s.scan(/\X/).each do |cluster|
|
|
66
|
+
width = RuVim::DisplayWidth.display_width(cluster, tabstop:, start_col: screen_col)
|
|
67
|
+
if screen_col + width > target
|
|
68
|
+
return align == :ceil ? (char_index + cluster.length) : char_index
|
|
69
|
+
end
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
display_col = 0
|
|
65
|
-
source_col = source_col_start.to_i
|
|
71
|
+
screen_col += width
|
|
72
|
+
char_index += cluster.length
|
|
73
|
+
end
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
w = RuVim::DisplayWidth.cell_width(ch, col: display_col, tabstop:)
|
|
70
|
-
break if display_col + w > max_width
|
|
75
|
+
char_index
|
|
76
|
+
end
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
def clip_cells_for_width(text, width, source_col_start: 0, tabstop: 2)
|
|
79
|
+
max_width = [width, 0].max
|
|
80
|
+
cells = []
|
|
81
|
+
display_col = 0
|
|
82
|
+
source_col = source_col_start
|
|
83
|
+
|
|
84
|
+
text.to_s.each_char do |ch|
|
|
85
|
+
code = ch.ord
|
|
86
|
+
# Fast path: printable ASCII (0x20..0x7E) — width 1, no special handling
|
|
87
|
+
if code >= 0x20 && code <= 0x7E
|
|
88
|
+
break if display_col >= max_width
|
|
89
|
+
cells << Cell.new(ch, source_col, 1)
|
|
90
|
+
display_col += 1
|
|
91
|
+
source_col += 1
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if ch == "\t"
|
|
96
|
+
w = tabstop - (display_col % tabstop)
|
|
97
|
+
w = tabstop if w.zero?
|
|
98
|
+
break if display_col + w > max_width
|
|
99
|
+
|
|
100
|
+
w.times do
|
|
101
|
+
cells << Cell.new(" ", source_col, 1)
|
|
102
|
+
end
|
|
103
|
+
display_col += w
|
|
104
|
+
source_col += 1
|
|
105
|
+
next
|
|
74
106
|
end
|
|
75
|
-
display_col += w
|
|
76
|
-
source_col += 1
|
|
77
|
-
next
|
|
78
|
-
end
|
|
79
107
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
# Control chars (0x00..0x1F, 0x7F, 0x80..0x9F)
|
|
109
|
+
if code < 0x20 || code == 0x7F || (code >= 0x80 && code <= 0x9F)
|
|
110
|
+
break if display_col >= max_width
|
|
111
|
+
cells << Cell.new("?", source_col, 1)
|
|
112
|
+
display_col += 1
|
|
113
|
+
source_col += 1
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
w = RuVim::DisplayWidth.cell_width(ch, col: display_col, tabstop:)
|
|
83
118
|
break if display_col + w > max_width
|
|
84
119
|
|
|
85
|
-
cells << Cell.new(
|
|
120
|
+
cells << Cell.new(ch, source_col, w)
|
|
86
121
|
display_col += w
|
|
87
122
|
source_col += 1
|
|
88
|
-
next
|
|
89
123
|
end
|
|
90
|
-
break if display_col + w > max_width
|
|
91
124
|
|
|
92
|
-
cells
|
|
93
|
-
display_col += w
|
|
94
|
-
source_col += 1
|
|
125
|
+
[cells, display_col]
|
|
95
126
|
end
|
|
96
|
-
|
|
97
|
-
[cells, display_col]
|
|
98
127
|
end
|
|
99
128
|
|
|
100
129
|
def pad_plain_to_screen_width(text, width, tabstop: 2)
|
|
101
130
|
cells, used = clip_cells_for_width(text, width, tabstop:)
|
|
102
131
|
out = cells.map(&:glyph).join
|
|
103
|
-
out << (" " * [width
|
|
132
|
+
out << (" " * [width - used, 0].max)
|
|
104
133
|
out
|
|
105
134
|
end
|
|
106
135
|
|
|
107
|
-
|
|
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"
|
|
136
|
+
UNSAFE_CONTROL_CHAR_RE = /[\u0000-\u0008\u000a-\u001f\u007f\u0080-\u009f]/
|
|
113
137
|
|
|
114
|
-
|
|
115
|
-
(
|
|
116
|
-
rescue StandardError
|
|
117
|
-
false
|
|
138
|
+
def terminal_safe_text(text)
|
|
139
|
+
text.to_s.gsub(UNSAFE_CONTROL_CHAR_RE, "?")
|
|
118
140
|
end
|
|
119
141
|
|
|
120
|
-
def terminal_safe_placeholder(_ch)
|
|
121
|
-
"?"
|
|
122
|
-
end
|
|
123
142
|
end
|
|
124
143
|
end
|
data/lib/ruvim/version.rb
CHANGED
data/lib/ruvim/window.rb
CHANGED
|
@@ -19,17 +19,17 @@ module RuVim
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def cursor_x=(value)
|
|
22
|
-
@cursor_x = value
|
|
22
|
+
@cursor_x = value
|
|
23
23
|
@preferred_x = nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def cursor_y=(value)
|
|
27
|
-
@cursor_y = value
|
|
27
|
+
@cursor_y = value
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def clamp_to_buffer(buffer, max_extra_col: 0)
|
|
31
31
|
@cursor_y = [[@cursor_y, 0].max, buffer.line_count - 1].min
|
|
32
|
-
max_col = buffer.line_length(@cursor_y) + [max_extra_col
|
|
32
|
+
max_col = buffer.line_length(@cursor_y) + [max_extra_col, 0].max
|
|
33
33
|
@cursor_x = [[@cursor_x, 0].max, max_col].min
|
|
34
34
|
self
|
|
35
35
|
end
|
|
@@ -71,7 +71,7 @@ module RuVim
|
|
|
71
71
|
|
|
72
72
|
def ensure_visible(buffer, height:, width:, tabstop: 2, scrolloff: 0, sidescrolloff: 0)
|
|
73
73
|
clamp_to_buffer(buffer)
|
|
74
|
-
so = [[scrolloff
|
|
74
|
+
so = [[scrolloff, 0].max, [height - 1, 0].max].min
|
|
75
75
|
|
|
76
76
|
top_target = @cursor_y - so
|
|
77
77
|
bottom_target = @cursor_y + so
|
|
@@ -82,7 +82,7 @@ module RuVim
|
|
|
82
82
|
line = buffer.line_at(@cursor_y)
|
|
83
83
|
cursor_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @cursor_x, tabstop:)
|
|
84
84
|
offset_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @col_offset, tabstop:)
|
|
85
|
-
sso = [[sidescrolloff
|
|
85
|
+
sso = [[sidescrolloff, 0].max, [width - 1, 0].max].min
|
|
86
86
|
|
|
87
87
|
if cursor_screen_col < offset_screen_col + sso
|
|
88
88
|
target_left = [cursor_screen_col - sso, 0].max
|
data/lib/ruvim.rb
CHANGED
|
@@ -13,11 +13,33 @@ require_relative "ruvim/display_width"
|
|
|
13
13
|
require_relative "ruvim/keyword_chars"
|
|
14
14
|
require_relative "ruvim/text_metrics"
|
|
15
15
|
require_relative "ruvim/clipboard"
|
|
16
|
+
require_relative "ruvim/browser"
|
|
17
|
+
require_relative "ruvim/lang/registry"
|
|
16
18
|
require_relative "ruvim/lang/base"
|
|
17
19
|
require_relative "ruvim/lang/markdown"
|
|
18
20
|
require_relative "ruvim/lang/ruby"
|
|
19
21
|
require_relative "ruvim/lang/json"
|
|
20
22
|
require_relative "ruvim/lang/scheme"
|
|
23
|
+
require_relative "ruvim/lang/c"
|
|
24
|
+
require_relative "ruvim/lang/cpp"
|
|
25
|
+
require_relative "ruvim/lang/diff"
|
|
26
|
+
require_relative "ruvim/lang/yaml"
|
|
27
|
+
require_relative "ruvim/lang/sh"
|
|
28
|
+
require_relative "ruvim/lang/python"
|
|
29
|
+
require_relative "ruvim/lang/javascript"
|
|
30
|
+
require_relative "ruvim/lang/typescript"
|
|
31
|
+
require_relative "ruvim/lang/html"
|
|
32
|
+
require_relative "ruvim/lang/toml"
|
|
33
|
+
require_relative "ruvim/lang/go"
|
|
34
|
+
require_relative "ruvim/lang/rust"
|
|
35
|
+
require_relative "ruvim/lang/makefile"
|
|
36
|
+
require_relative "ruvim/lang/dockerfile"
|
|
37
|
+
require_relative "ruvim/lang/sql"
|
|
38
|
+
require_relative "ruvim/lang/elixir"
|
|
39
|
+
require_relative "ruvim/lang/perl"
|
|
40
|
+
require_relative "ruvim/lang/lua"
|
|
41
|
+
require_relative "ruvim/lang/ocaml"
|
|
42
|
+
require_relative "ruvim/lang/erb"
|
|
21
43
|
require_relative "ruvim/highlighter"
|
|
22
44
|
require_relative "ruvim/context"
|
|
23
45
|
require_relative "ruvim/buffer"
|
|
@@ -25,6 +47,15 @@ require_relative "ruvim/window"
|
|
|
25
47
|
require_relative "ruvim/editor"
|
|
26
48
|
require_relative "ruvim/command_registry"
|
|
27
49
|
require_relative "ruvim/ex_command_registry"
|
|
50
|
+
require_relative "ruvim/git/blame"
|
|
51
|
+
require_relative "ruvim/git/status"
|
|
52
|
+
require_relative "ruvim/git/diff"
|
|
53
|
+
require_relative "ruvim/git/log"
|
|
54
|
+
require_relative "ruvim/git/branch"
|
|
55
|
+
require_relative "ruvim/git/commit"
|
|
56
|
+
require_relative "ruvim/git/grep"
|
|
57
|
+
require_relative "ruvim/gh/link"
|
|
58
|
+
require_relative "ruvim/git/handler"
|
|
28
59
|
require_relative "ruvim/global_commands"
|
|
29
60
|
require_relative "ruvim/dispatcher"
|
|
30
61
|
require_relative "ruvim/keymap_manager"
|
|
@@ -32,10 +63,6 @@ require_relative "ruvim/command_line"
|
|
|
32
63
|
require_relative "ruvim/input"
|
|
33
64
|
require_relative "ruvim/terminal"
|
|
34
65
|
require_relative "ruvim/rich_view"
|
|
35
|
-
|
|
36
|
-
# Register renderers after RichView is defined
|
|
37
|
-
RuVim::RichView.register("markdown", RuVim::RichView::MarkdownRenderer)
|
|
38
|
-
|
|
39
66
|
require_relative "ruvim/lang/tsv"
|
|
40
67
|
require_relative "ruvim/lang/csv"
|
|
41
68
|
require_relative "ruvim/screen"
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class AppCommandTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@app = RuVim::App.new(clean: true)
|
|
8
|
+
@editor = @app.instance_variable_get(:@editor)
|
|
9
|
+
@key_handler = @app.instance_variable_get(:@key_handler)
|
|
10
|
+
@editor.materialize_intro_buffer!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def feed(*keys)
|
|
14
|
+
keys.each { |k| @key_handler.handle(k) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def buf
|
|
18
|
+
@editor.current_buffer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def win
|
|
22
|
+
@editor.current_window
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# --- cursor commands ---
|
|
26
|
+
|
|
27
|
+
def test_cursor_line_end
|
|
28
|
+
buf.replace_all_lines!(["hello world"])
|
|
29
|
+
win.cursor_x = 0
|
|
30
|
+
feed("$")
|
|
31
|
+
assert_equal buf.line_length(0), win.cursor_x
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_cursor_buffer_start_gg
|
|
35
|
+
buf.replace_all_lines!(["line1", "line2", "line3"])
|
|
36
|
+
win.cursor_y = 2
|
|
37
|
+
feed("g", "g")
|
|
38
|
+
assert_equal 0, win.cursor_y
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# --- insert mode variants ---
|
|
42
|
+
|
|
43
|
+
def test_append_mode_a
|
|
44
|
+
buf.replace_all_lines!(["abc"])
|
|
45
|
+
win.cursor_x = 1
|
|
46
|
+
feed("a", "X", :escape)
|
|
47
|
+
assert_equal ["abXc"], buf.lines
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_insert_line_start_nonblank_I
|
|
51
|
+
buf.replace_all_lines!([" hello"])
|
|
52
|
+
win.cursor_x = 4
|
|
53
|
+
feed("I", "X", :escape)
|
|
54
|
+
assert_equal [" Xhello"], buf.lines
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_open_line_below_o
|
|
58
|
+
buf.replace_all_lines!(["hello", "world"])
|
|
59
|
+
win.cursor_y = 0
|
|
60
|
+
feed("o", "new", :escape)
|
|
61
|
+
assert_equal ["hello", "new", "world"], buf.lines
|
|
62
|
+
assert_equal :normal, @editor.mode
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_open_line_above_O
|
|
66
|
+
buf.replace_all_lines!(["hello", "world"])
|
|
67
|
+
win.cursor_y = 1
|
|
68
|
+
feed("O", "new", :escape)
|
|
69
|
+
assert_equal ["hello", "new", "world"], buf.lines
|
|
70
|
+
assert_equal :normal, @editor.mode
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# --- undo / redo ---
|
|
74
|
+
|
|
75
|
+
def test_undo_and_redo
|
|
76
|
+
buf.replace_all_lines!(["hello"])
|
|
77
|
+
feed("x")
|
|
78
|
+
assert_equal ["ello"], buf.lines
|
|
79
|
+
|
|
80
|
+
feed("u")
|
|
81
|
+
assert_equal ["hello"], buf.lines
|
|
82
|
+
assert_match(/Undo/, @editor.message)
|
|
83
|
+
|
|
84
|
+
feed(:ctrl_r)
|
|
85
|
+
assert_equal ["ello"], buf.lines
|
|
86
|
+
assert_match(/Redo/, @editor.message)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_undo_at_oldest
|
|
90
|
+
buf.replace_all_lines!(["hello"])
|
|
91
|
+
buf.instance_variable_get(:@undo_stack)&.clear
|
|
92
|
+
feed("u")
|
|
93
|
+
assert_match(/oldest/, @editor.message)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# --- search ---
|
|
97
|
+
|
|
98
|
+
def test_search_backward_mode
|
|
99
|
+
buf.replace_all_lines!(["hello world"])
|
|
100
|
+
feed("?")
|
|
101
|
+
assert_equal :command_line, @editor.mode
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_search_prev_N
|
|
105
|
+
buf.replace_all_lines!(["aaa", "bbb", "aaa"])
|
|
106
|
+
win.cursor_y = 0
|
|
107
|
+
feed("/", "b", "b", "b", :enter)
|
|
108
|
+
assert_equal 1, win.cursor_y
|
|
109
|
+
|
|
110
|
+
feed("N")
|
|
111
|
+
assert_operator win.cursor_y, :>=, 0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_search_word_backward
|
|
115
|
+
buf.replace_all_lines!(["foo bar foo"])
|
|
116
|
+
win.cursor_x = 8
|
|
117
|
+
feed("#")
|
|
118
|
+
assert_equal 0, win.cursor_x
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_search_word_forward_partial
|
|
122
|
+
buf.replace_all_lines!(["foobar foo foobar"])
|
|
123
|
+
win.cursor_x = 0
|
|
124
|
+
feed("g", "*")
|
|
125
|
+
assert_operator win.cursor_x, :>, 0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_search_word_backward_partial
|
|
129
|
+
buf.replace_all_lines!(["foobar foo foobar"])
|
|
130
|
+
win.cursor_x = 11
|
|
131
|
+
feed("g", "#")
|
|
132
|
+
assert_operator win.cursor_x, :<, 11
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# --- marks ---
|
|
136
|
+
|
|
137
|
+
def test_mark_set_and_jump
|
|
138
|
+
buf.replace_all_lines!(["line1", "line2", "line3"])
|
|
139
|
+
win.cursor_y = 1
|
|
140
|
+
feed("m", "a")
|
|
141
|
+
|
|
142
|
+
win.cursor_y = 0
|
|
143
|
+
feed("'", "a")
|
|
144
|
+
assert_equal 1, win.cursor_y
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_mark_jump_unset
|
|
148
|
+
buf.replace_all_lines!(["line1"])
|
|
149
|
+
feed("`", "z")
|
|
150
|
+
assert_match(/Mark not set/, @editor.message)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_mark_pending_escape_cancels
|
|
154
|
+
buf.replace_all_lines!(["hello"])
|
|
155
|
+
feed("m", "\e")
|
|
156
|
+
assert_equal :normal, @editor.mode
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_mark_pending_invalid_char
|
|
160
|
+
buf.replace_all_lines!(["hello"])
|
|
161
|
+
feed("m", " ")
|
|
162
|
+
assert_equal :normal, @editor.mode
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# --- jump list ---
|
|
166
|
+
|
|
167
|
+
def test_jump_older_and_newer
|
|
168
|
+
buf.replace_all_lines!((1..20).map { |i| "line#{i}" })
|
|
169
|
+
win.cursor_y = 0
|
|
170
|
+
|
|
171
|
+
feed("G")
|
|
172
|
+
last_line = buf.line_count - 1
|
|
173
|
+
assert_equal last_line, win.cursor_y
|
|
174
|
+
|
|
175
|
+
feed(:ctrl_o)
|
|
176
|
+
old_y = win.cursor_y
|
|
177
|
+
|
|
178
|
+
feed(:ctrl_i)
|
|
179
|
+
new_y = win.cursor_y
|
|
180
|
+
assert_operator new_y, :>=, old_y
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_jump_older_empty
|
|
184
|
+
buf.replace_all_lines!(["line1"])
|
|
185
|
+
@editor.instance_variable_get(:@jump_list)&.clear rescue nil
|
|
186
|
+
feed(:ctrl_o)
|
|
187
|
+
assert_match(/Jump list/, @editor.message.to_s) if @editor.message
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_backtick_backtick_jumps_older
|
|
191
|
+
buf.replace_all_lines!((1..10).map { |i| "line#{i}" })
|
|
192
|
+
win.cursor_y = 0
|
|
193
|
+
feed("G")
|
|
194
|
+
feed("`", "`")
|
|
195
|
+
assert_equal 0, win.cursor_y
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_jump_pending_escape_cancels
|
|
199
|
+
buf.replace_all_lines!(["hello"])
|
|
200
|
+
feed("'", "\e")
|
|
201
|
+
assert_equal :normal, @editor.mode
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_jump_pending_invalid_mark
|
|
205
|
+
buf.replace_all_lines!(["hello"])
|
|
206
|
+
feed("'", " ")
|
|
207
|
+
assert_equal :normal, @editor.mode
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# --- visual mode ---
|
|
211
|
+
|
|
212
|
+
def test_visual_line_yank
|
|
213
|
+
buf.replace_all_lines!(["aaa", "bbb", "ccc"])
|
|
214
|
+
win.cursor_y = 0
|
|
215
|
+
feed("V", "j", "y")
|
|
216
|
+
|
|
217
|
+
reg = @editor.get_register("\"")
|
|
218
|
+
assert_equal :normal, @editor.mode
|
|
219
|
+
assert_includes reg[:text], "aaa"
|
|
220
|
+
assert_includes reg[:text], "bbb"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_visual_line_delete
|
|
224
|
+
buf.replace_all_lines!(["aaa", "bbb", "ccc"])
|
|
225
|
+
win.cursor_y = 0
|
|
226
|
+
feed("V", "j", "d")
|
|
227
|
+
|
|
228
|
+
assert_equal ["ccc"], buf.lines
|
|
229
|
+
assert_equal :normal, @editor.mode
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def test_visual_char_delete
|
|
233
|
+
buf.replace_all_lines!(["abcdef"])
|
|
234
|
+
win.cursor_x = 1
|
|
235
|
+
feed("v", "l", "l", "d")
|
|
236
|
+
|
|
237
|
+
assert_equal ["aef"], buf.lines
|
|
238
|
+
assert_equal :normal, @editor.mode
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_visual_select_text_object_iw
|
|
242
|
+
buf.replace_all_lines!(["hello world"])
|
|
243
|
+
win.cursor_x = 0
|
|
244
|
+
feed("v", "i", "w")
|
|
245
|
+
assert_equal :visual_char, @editor.mode
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# --- delete operator ---
|
|
249
|
+
|
|
250
|
+
def test_delete_gg_motion
|
|
251
|
+
buf.replace_all_lines!(["aaa", "bbb", "ccc"])
|
|
252
|
+
win.cursor_y = 2
|
|
253
|
+
feed("d", "g", "g")
|
|
254
|
+
assert_equal [""], buf.lines
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_delete_j_motion
|
|
258
|
+
buf.replace_all_lines!(["aaa", "bbb", "ccc"])
|
|
259
|
+
win.cursor_y = 0
|
|
260
|
+
feed("d", "j")
|
|
261
|
+
assert_equal ["ccc"], buf.lines
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def test_delete_k_motion
|
|
265
|
+
buf.replace_all_lines!(["aaa", "bbb", "ccc"])
|
|
266
|
+
win.cursor_y = 1
|
|
267
|
+
feed("d", "k")
|
|
268
|
+
assert_equal ["ccc"], buf.lines
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_delete_word_dw
|
|
272
|
+
buf.replace_all_lines!(["hello world"])
|
|
273
|
+
win.cursor_x = 0
|
|
274
|
+
feed("d", "w")
|
|
275
|
+
assert_equal ["world"], buf.lines
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def test_delete_aw
|
|
279
|
+
buf.replace_all_lines!(["hello world"])
|
|
280
|
+
win.cursor_x = 0
|
|
281
|
+
feed("d", "a", "w")
|
|
282
|
+
assert_equal ["world"], buf.lines
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# --- yank operator ---
|
|
286
|
+
|
|
287
|
+
def test_yank_word_yw
|
|
288
|
+
buf.replace_all_lines!(["hello world"])
|
|
289
|
+
win.cursor_x = 0
|
|
290
|
+
feed("y", "w")
|
|
291
|
+
reg = @editor.get_register("\"")
|
|
292
|
+
assert_equal "hello ", reg[:text]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_yank_gg_motion
|
|
296
|
+
buf.replace_all_lines!(["aaa", "bbb", "ccc"])
|
|
297
|
+
win.cursor_y = 2
|
|
298
|
+
feed("y", "g", "g")
|
|
299
|
+
reg = @editor.get_register("\"")
|
|
300
|
+
assert_includes reg[:text], "aaa"
|
|
301
|
+
assert_includes reg[:text], "ccc"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def test_yank_iw
|
|
305
|
+
buf.replace_all_lines!(["hello world"])
|
|
306
|
+
win.cursor_x = 0
|
|
307
|
+
feed("y", "i", "w")
|
|
308
|
+
reg = @editor.get_register("\"")
|
|
309
|
+
assert_equal "hello", reg[:text]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_yank_aw
|
|
313
|
+
buf.replace_all_lines!(["hello world"])
|
|
314
|
+
win.cursor_x = 0
|
|
315
|
+
feed("y", "a", "w")
|
|
316
|
+
reg = @editor.get_register("\"")
|
|
317
|
+
assert_equal "hello ", reg[:text]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# --- indent operator ---
|
|
321
|
+
|
|
322
|
+
def test_indent_j_motion
|
|
323
|
+
buf.replace_all_lines!([" aaa", " bbb", "ccc"])
|
|
324
|
+
win.cursor_y = 0
|
|
325
|
+
feed("=", "j")
|
|
326
|
+
assert_equal :normal, @editor.mode
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_indent_gg_motion
|
|
330
|
+
buf.replace_all_lines!([" aaa", " bbb"])
|
|
331
|
+
win.cursor_y = 1
|
|
332
|
+
feed("=", "g", "g")
|
|
333
|
+
assert_equal :normal, @editor.mode
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def test_indent_k_motion
|
|
337
|
+
buf.replace_all_lines!(["aaa", " bbb", "ccc"])
|
|
338
|
+
win.cursor_y = 1
|
|
339
|
+
feed("=", "k")
|
|
340
|
+
assert_equal :normal, @editor.mode
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def test_indent_G_motion
|
|
344
|
+
buf.replace_all_lines!([" aaa", " bbb"])
|
|
345
|
+
win.cursor_y = 0
|
|
346
|
+
feed("=", "G")
|
|
347
|
+
assert_equal :normal, @editor.mode
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# --- arrow and page keys ---
|
|
351
|
+
|
|
352
|
+
def test_arrow_keys_in_normal_mode
|
|
353
|
+
buf.replace_all_lines!(["abc", "def"])
|
|
354
|
+
win.cursor_y = 0
|
|
355
|
+
win.cursor_x = 0
|
|
356
|
+
feed(:right)
|
|
357
|
+
assert_equal 1, win.cursor_x
|
|
358
|
+
feed(:down)
|
|
359
|
+
assert_equal 1, win.cursor_y
|
|
360
|
+
feed(:left)
|
|
361
|
+
assert_equal 0, win.cursor_x
|
|
362
|
+
feed(:up)
|
|
363
|
+
assert_equal 0, win.cursor_y
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_page_keys_in_normal_mode
|
|
367
|
+
buf.replace_all_lines!((1..30).map { |i| "line#{i}" })
|
|
368
|
+
@editor.current_window_view_height_hint = 10
|
|
369
|
+
win.cursor_y = 0
|
|
370
|
+
feed(:pagedown)
|
|
371
|
+
assert_operator win.cursor_y, :>, 0
|
|
372
|
+
feed(:pageup)
|
|
373
|
+
assert_equal 0, win.cursor_y
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# --- escape clears message ---
|
|
377
|
+
|
|
378
|
+
def test_escape_clears_message
|
|
379
|
+
@editor.echo("test message")
|
|
380
|
+
feed("\e")
|
|
381
|
+
end
|
|
382
|
+
end
|