ruvim 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +18 -6
- data/README.md +15 -1
- data/docs/binding.md +16 -0
- data/docs/command.md +78 -4
- data/docs/config.md +10 -2
- data/docs/spec.md +60 -9
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +18 -8
- data/lib/ruvim/app.rb +290 -8
- data/lib/ruvim/buffer.rb +14 -2
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/editor.rb +12 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +176 -42
- data/lib/ruvim/highlighter.rb +3 -1
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +34 -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.rb +16 -0
- data/lib/ruvim/screen.rb +9 -12
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim.rb +10 -0
- data/test/app_completion_test.rb +25 -0
- data/test/app_scenario_test.rb +169 -0
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/display_width_test.rb +41 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +44 -0
- data/test/indent_test.rb +86 -0
- data/test/rich_view_test.rb +256 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +17 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
module Lang
|
|
5
|
+
module Diff
|
|
6
|
+
ADD_COLOR = "\e[32m" # green
|
|
7
|
+
DELETE_COLOR = "\e[31m" # red
|
|
8
|
+
HUNK_COLOR = "\e[36m" # cyan
|
|
9
|
+
HEADER_COLOR = "\e[1m" # bold
|
|
10
|
+
META_COLOR = "\e[33m" # yellow
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def color_columns(text)
|
|
15
|
+
cols = {}
|
|
16
|
+
color = line_color(text)
|
|
17
|
+
return cols unless color
|
|
18
|
+
|
|
19
|
+
text.length.times { |i| cols[i] = color }
|
|
20
|
+
cols
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def line_color(text)
|
|
24
|
+
return nil if text.empty?
|
|
25
|
+
|
|
26
|
+
case text
|
|
27
|
+
when /\A@@/
|
|
28
|
+
HUNK_COLOR
|
|
29
|
+
when /\Adiff /
|
|
30
|
+
HEADER_COLOR
|
|
31
|
+
when /\A\+/
|
|
32
|
+
ADD_COLOR
|
|
33
|
+
when /\A-/
|
|
34
|
+
DELETE_COLOR
|
|
35
|
+
when /\Aindex /, /\Anew file/, /\Adeleted file/, /\Arename/, /\Asimilarity/
|
|
36
|
+
META_COLOR
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/ruvim/lang/json.rb
CHANGED
|
@@ -5,6 +5,40 @@ module RuVim
|
|
|
5
5
|
module Json
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
+
INDENT_OPEN_RE = /[\[{]\s*$/
|
|
9
|
+
INDENT_CLOSE_RE = /\A\s*[\]}]/
|
|
10
|
+
|
|
11
|
+
DEDENT_TRIGGERS = {
|
|
12
|
+
"}" => /\A(\s*)\}/,
|
|
13
|
+
"]" => /\A(\s*)\]/
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def calculate_indent(lines, target_row, shiftwidth)
|
|
17
|
+
depth = 0
|
|
18
|
+
(0...target_row).each do |row|
|
|
19
|
+
line = lines[row].to_s
|
|
20
|
+
line.each_char do |ch|
|
|
21
|
+
case ch
|
|
22
|
+
when "{", "[" then depth += 1
|
|
23
|
+
when "}", "]" then depth -= 1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
target_line = lines[target_row].to_s.lstrip
|
|
29
|
+
depth -= 1 if target_line.match?(INDENT_CLOSE_RE)
|
|
30
|
+
depth = 0 if depth < 0
|
|
31
|
+
depth * shiftwidth
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def indent_trigger?(line)
|
|
35
|
+
line.to_s.rstrip.match?(INDENT_OPEN_RE)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dedent_trigger(char)
|
|
39
|
+
DEDENT_TRIGGERS[char]
|
|
40
|
+
end
|
|
41
|
+
|
|
8
42
|
def color_columns(text)
|
|
9
43
|
cols = {}
|
|
10
44
|
Highlighter.apply_regex(cols, text, /"(?:\\.|[^"\\])*"\s*(?=:)/, "\e[36m")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module RichView
|
|
7
|
+
module JsonRenderer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Signal that this renderer creates a virtual buffer
|
|
11
|
+
# instead of entering rich mode on the same buffer.
|
|
12
|
+
def open_view!(editor)
|
|
13
|
+
buffer = editor.current_buffer
|
|
14
|
+
window = editor.current_window
|
|
15
|
+
text = buffer.lines.join("\n")
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
parsed = JSON.parse(text)
|
|
19
|
+
rescue JSON::ParserError => e
|
|
20
|
+
editor.echo_error("JSON parse error: #{e.message}")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Compute cursor's significant char offset before formatting
|
|
25
|
+
cursor_offset = char_offset_for(buffer.lines, window.cursor_y, window.cursor_x)
|
|
26
|
+
sig_count = significant_char_count(text, cursor_offset + 1)
|
|
27
|
+
|
|
28
|
+
formatted = JSON.pretty_generate(parsed)
|
|
29
|
+
lines = formatted.lines(chomp: true)
|
|
30
|
+
|
|
31
|
+
target_line = line_for_significant_count(formatted, sig_count)
|
|
32
|
+
|
|
33
|
+
buf = editor.add_virtual_buffer(
|
|
34
|
+
kind: :json_formatted,
|
|
35
|
+
name: "[JSON Formatted]",
|
|
36
|
+
lines: lines,
|
|
37
|
+
filetype: "json",
|
|
38
|
+
readonly: true,
|
|
39
|
+
modifiable: false
|
|
40
|
+
)
|
|
41
|
+
editor.switch_to_buffer(buf.id)
|
|
42
|
+
RichView.bind_close_keys(editor, buf.id)
|
|
43
|
+
window.cursor_y = [target_line, lines.length - 1].min
|
|
44
|
+
window.cursor_x = 0
|
|
45
|
+
editor.echo("[JSON Formatted] #{lines.length} lines")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Count significant (non-whitespace-outside-strings) characters
|
|
49
|
+
# in text[0...byte_offset].
|
|
50
|
+
def significant_char_count(text, byte_offset)
|
|
51
|
+
in_string = false
|
|
52
|
+
escape = false
|
|
53
|
+
count = 0
|
|
54
|
+
text.each_char.with_index do |ch, i|
|
|
55
|
+
break if i >= byte_offset
|
|
56
|
+
if in_string
|
|
57
|
+
if escape
|
|
58
|
+
escape = false
|
|
59
|
+
elsif ch == "\\"
|
|
60
|
+
escape = true
|
|
61
|
+
elsif ch == '"'
|
|
62
|
+
in_string = false
|
|
63
|
+
end
|
|
64
|
+
count += 1
|
|
65
|
+
else
|
|
66
|
+
case ch
|
|
67
|
+
when '"'
|
|
68
|
+
in_string = true
|
|
69
|
+
count += 1
|
|
70
|
+
when " ", "\n", "\r", "\t"
|
|
71
|
+
# skip whitespace outside strings
|
|
72
|
+
else
|
|
73
|
+
count += 1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
count
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Find the line number in text where the N-th significant character falls.
|
|
81
|
+
def line_for_significant_count(text, target_count)
|
|
82
|
+
return 0 if target_count <= 0
|
|
83
|
+
|
|
84
|
+
in_string = false
|
|
85
|
+
escape = false
|
|
86
|
+
count = 0
|
|
87
|
+
line = 0
|
|
88
|
+
text.each_char do |ch|
|
|
89
|
+
if ch == "\n" && !in_string
|
|
90
|
+
line += 1
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
if in_string
|
|
94
|
+
if escape
|
|
95
|
+
escape = false
|
|
96
|
+
elsif ch == "\\"
|
|
97
|
+
escape = true
|
|
98
|
+
elsif ch == '"'
|
|
99
|
+
in_string = false
|
|
100
|
+
end
|
|
101
|
+
count += 1
|
|
102
|
+
else
|
|
103
|
+
case ch
|
|
104
|
+
when '"'
|
|
105
|
+
in_string = true
|
|
106
|
+
count += 1
|
|
107
|
+
when " ", "\r", "\t"
|
|
108
|
+
# skip
|
|
109
|
+
else
|
|
110
|
+
count += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
return line if count >= target_count
|
|
114
|
+
end
|
|
115
|
+
line
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Compute character offset in joined text from cursor row/col.
|
|
119
|
+
def char_offset_for(lines, row, col)
|
|
120
|
+
offset = 0
|
|
121
|
+
lines.each_with_index do |line, i|
|
|
122
|
+
if i == row
|
|
123
|
+
return offset + [col, line.length].min
|
|
124
|
+
end
|
|
125
|
+
offset += line.length + 1 # +1 for "\n"
|
|
126
|
+
end
|
|
127
|
+
offset
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module RichView
|
|
7
|
+
module JsonlRenderer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
SEPARATOR = "---"
|
|
11
|
+
|
|
12
|
+
def open_view!(editor)
|
|
13
|
+
buffer = editor.current_buffer
|
|
14
|
+
window = editor.current_window
|
|
15
|
+
cursor_row = window.cursor_y
|
|
16
|
+
|
|
17
|
+
output_lines = []
|
|
18
|
+
# Map from source line index to starting line in output
|
|
19
|
+
line_map = {}
|
|
20
|
+
|
|
21
|
+
buffer.lines.each_with_index do |raw_line, idx|
|
|
22
|
+
line = raw_line.to_s.strip
|
|
23
|
+
next if line.empty?
|
|
24
|
+
|
|
25
|
+
output_lines << SEPARATOR unless output_lines.empty?
|
|
26
|
+
line_map[idx] = output_lines.length
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
parsed = JSON.parse(line)
|
|
30
|
+
formatted = JSON.pretty_generate(parsed)
|
|
31
|
+
formatted.each_line(chomp: true) { |l| output_lines << l }
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
output_lines << "// PARSE ERROR: #{raw_line}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
output_lines << "" if output_lines.empty?
|
|
38
|
+
|
|
39
|
+
target_line = line_map[cursor_row] || 0
|
|
40
|
+
|
|
41
|
+
buf = editor.add_virtual_buffer(
|
|
42
|
+
kind: :jsonl_formatted,
|
|
43
|
+
name: "[JSONL Formatted]",
|
|
44
|
+
lines: output_lines,
|
|
45
|
+
filetype: "json",
|
|
46
|
+
readonly: true,
|
|
47
|
+
modifiable: false
|
|
48
|
+
)
|
|
49
|
+
editor.switch_to_buffer(buf.id)
|
|
50
|
+
RichView.bind_close_keys(editor, buf.id)
|
|
51
|
+
window.cursor_y = [target_line, output_lines.length - 1].min
|
|
52
|
+
window.cursor_x = 0
|
|
53
|
+
editor.echo("[JSONL Formatted] #{output_lines.length} lines")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/ruvim/rich_view.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "rich_view/table_renderer"
|
|
4
4
|
require_relative "rich_view/markdown_renderer"
|
|
5
|
+
require_relative "rich_view/json_renderer"
|
|
6
|
+
require_relative "rich_view/jsonl_renderer"
|
|
5
7
|
|
|
6
8
|
module RuVim
|
|
7
9
|
module RichView
|
|
@@ -46,6 +48,11 @@ module RuVim
|
|
|
46
48
|
renderer = @renderers[format]
|
|
47
49
|
raise RuVim::CommandError, "No renderer for format: #{format}" unless renderer
|
|
48
50
|
|
|
51
|
+
if renderer.respond_to?(:open_view!)
|
|
52
|
+
renderer.open_view!(editor)
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
49
56
|
delimiter = renderer.delimiter_for(format)
|
|
50
57
|
editor.enter_rich_mode(format: format, delimiter: delimiter)
|
|
51
58
|
editor.echo("[Rich: #{format}]")
|
|
@@ -89,5 +96,14 @@ module RuVim
|
|
|
89
96
|
|
|
90
97
|
editor.exit_rich_mode
|
|
91
98
|
end
|
|
99
|
+
|
|
100
|
+
# Bind Esc and C-c to close a virtual buffer created by a renderer.
|
|
101
|
+
def bind_close_keys(editor, buffer_id)
|
|
102
|
+
km = editor.keymap_manager
|
|
103
|
+
return unless km
|
|
104
|
+
|
|
105
|
+
km.bind_buffer(buffer_id, "\e", "rich.close_buffer")
|
|
106
|
+
km.bind_buffer(buffer_id, "<C-c>", "rich.close_buffer")
|
|
107
|
+
end
|
|
92
108
|
end
|
|
93
109
|
end
|
data/lib/ruvim/screen.rb
CHANGED
|
@@ -871,15 +871,19 @@ module RuVim
|
|
|
871
871
|
end
|
|
872
872
|
|
|
873
873
|
def search_bg_seq(editor)
|
|
874
|
-
|
|
874
|
+
term_color(editor, "\e[48;2;255;215;0m", "\e[43m")
|
|
875
875
|
end
|
|
876
876
|
|
|
877
877
|
def colorcolumn_bg_seq(editor)
|
|
878
|
-
|
|
878
|
+
term_color(editor, "\e[48;2;72;72;72m", "\e[48;5;238m")
|
|
879
879
|
end
|
|
880
880
|
|
|
881
881
|
def cursorline_bg_seq(editor)
|
|
882
|
-
|
|
882
|
+
term_color(editor, "\e[48;2;58;58;58m", "\e[48;5;236m")
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def term_color(editor, truecolor_seq, fallback_seq)
|
|
886
|
+
truecolor_enabled?(editor) ? truecolor_seq : fallback_seq
|
|
883
887
|
end
|
|
884
888
|
|
|
885
889
|
def line_number_fg_seq(editor, current_line: false)
|
|
@@ -911,7 +915,7 @@ module RuVim
|
|
|
911
915
|
|
|
912
916
|
path = buffer.display_name
|
|
913
917
|
mod = buffer.modified? ? " [+]" : ""
|
|
914
|
-
stream =
|
|
918
|
+
stream = buffer.stream_status ? " [#{buffer.stream_status}]" : ""
|
|
915
919
|
loading = file_loading_status_token(buffer)
|
|
916
920
|
tab = tab_status_token(editor)
|
|
917
921
|
msg = editor.message_error? ? "" : editor.message.to_s
|
|
@@ -921,14 +925,6 @@ module RuVim
|
|
|
921
925
|
"#{compose_status_body(left, msg, body_width)}#{right}"
|
|
922
926
|
end
|
|
923
927
|
|
|
924
|
-
def stream_status_token(buffer)
|
|
925
|
-
return "" unless buffer.respond_to?(:stream_state)
|
|
926
|
-
return "" unless buffer.kind == :stream
|
|
927
|
-
|
|
928
|
-
state = (buffer.stream_state || :live).to_s
|
|
929
|
-
" [stdin/#{state}]"
|
|
930
|
-
end
|
|
931
|
-
|
|
932
928
|
def file_loading_status_token(buffer)
|
|
933
929
|
return "" unless buffer.respond_to?(:loading_state)
|
|
934
930
|
return "" unless buffer.file_buffer?
|
|
@@ -1130,6 +1126,7 @@ module RuVim
|
|
|
1130
1126
|
search = editor.last_search
|
|
1131
1127
|
return {} unless search && search[:pattern]
|
|
1132
1128
|
return {} unless editor.effective_option("hlsearch")
|
|
1129
|
+
return {} if editor.hlsearch_suppressed?
|
|
1133
1130
|
|
|
1134
1131
|
regex = build_screen_search_regex(editor, search[:pattern])
|
|
1135
1132
|
cols = {}
|
data/lib/ruvim/version.rb
CHANGED
data/lib/ruvim.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "ruvim/lang/markdown"
|
|
|
18
18
|
require_relative "ruvim/lang/ruby"
|
|
19
19
|
require_relative "ruvim/lang/json"
|
|
20
20
|
require_relative "ruvim/lang/scheme"
|
|
21
|
+
require_relative "ruvim/lang/diff"
|
|
21
22
|
require_relative "ruvim/highlighter"
|
|
22
23
|
require_relative "ruvim/context"
|
|
23
24
|
require_relative "ruvim/buffer"
|
|
@@ -25,6 +26,13 @@ require_relative "ruvim/window"
|
|
|
25
26
|
require_relative "ruvim/editor"
|
|
26
27
|
require_relative "ruvim/command_registry"
|
|
27
28
|
require_relative "ruvim/ex_command_registry"
|
|
29
|
+
require_relative "ruvim/git/blame"
|
|
30
|
+
require_relative "ruvim/git/status"
|
|
31
|
+
require_relative "ruvim/git/diff"
|
|
32
|
+
require_relative "ruvim/git/log"
|
|
33
|
+
require_relative "ruvim/git/branch"
|
|
34
|
+
require_relative "ruvim/git/commit"
|
|
35
|
+
require_relative "ruvim/git/handler"
|
|
28
36
|
require_relative "ruvim/global_commands"
|
|
29
37
|
require_relative "ruvim/dispatcher"
|
|
30
38
|
require_relative "ruvim/keymap_manager"
|
|
@@ -35,6 +43,8 @@ require_relative "ruvim/rich_view"
|
|
|
35
43
|
|
|
36
44
|
# Register renderers after RichView is defined
|
|
37
45
|
RuVim::RichView.register("markdown", RuVim::RichView::MarkdownRenderer)
|
|
46
|
+
RuVim::RichView.register("json", RuVim::RichView::JsonRenderer)
|
|
47
|
+
RuVim::RichView.register("jsonl", RuVim::RichView::JsonlRenderer)
|
|
38
48
|
|
|
39
49
|
require_relative "ruvim/lang/tsv"
|
|
40
50
|
require_relative "ruvim/lang/csv"
|
data/test/app_completion_test.rb
CHANGED
|
@@ -210,4 +210,29 @@ class AppCompletionTest < Minitest::Test
|
|
|
210
210
|
@app.send(:handle_insert_key, :ctrl_n)
|
|
211
211
|
assert_equal "foobar", b.line_at(0)
|
|
212
212
|
end
|
|
213
|
+
|
|
214
|
+
def test_git_subcommand_completion
|
|
215
|
+
@editor.materialize_intro_buffer!
|
|
216
|
+
@editor.enter_command_line_mode(":")
|
|
217
|
+
cmd = @editor.command_line
|
|
218
|
+
cmd.replace_text("git bl")
|
|
219
|
+
|
|
220
|
+
@app.send(:command_line_complete)
|
|
221
|
+
|
|
222
|
+
# "bl" matches "blame", "blameback", "blamecommit", "blameprev", "branch"
|
|
223
|
+
# With multiple matches, wildmode applies (longest common prefix or cycle)
|
|
224
|
+
text = cmd.text
|
|
225
|
+
assert text.start_with?("git bl"), "Expected completion to keep 'git bl' prefix, got: #{text}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_git_subcommand_completion_unique
|
|
229
|
+
@editor.materialize_intro_buffer!
|
|
230
|
+
@editor.enter_command_line_mode(":")
|
|
231
|
+
cmd = @editor.command_line
|
|
232
|
+
cmd.replace_text("git co")
|
|
233
|
+
|
|
234
|
+
@app.send(:command_line_complete)
|
|
235
|
+
|
|
236
|
+
assert_equal "git commit", cmd.text
|
|
237
|
+
end
|
|
213
238
|
end
|
data/test/app_scenario_test.rb
CHANGED
|
@@ -695,6 +695,28 @@ class AppScenarioTest < Minitest::Test
|
|
|
695
695
|
assert_equal :normal, @editor.mode
|
|
696
696
|
end
|
|
697
697
|
|
|
698
|
+
def test_paste_batch_suppresses_autoindent
|
|
699
|
+
@editor.current_buffer.replace_all_lines!([" hello"])
|
|
700
|
+
@editor.current_window.cursor_y = 0
|
|
701
|
+
@editor.current_window.cursor_x = 0
|
|
702
|
+
|
|
703
|
+
# Normal enter in insert mode should autoindent
|
|
704
|
+
feed("A", :enter, :escape)
|
|
705
|
+
assert_equal [" hello", " "], @editor.current_buffer.lines
|
|
706
|
+
|
|
707
|
+
# Simulate paste batch: autoindent should be suppressed
|
|
708
|
+
@editor.current_buffer.replace_all_lines!([" hello"])
|
|
709
|
+
@editor.current_window.cursor_y = 0
|
|
710
|
+
@editor.current_window.cursor_x = 0
|
|
711
|
+
feed("A")
|
|
712
|
+
@app.instance_variable_set(:@paste_batch, true)
|
|
713
|
+
feed(:enter, *"world".chars)
|
|
714
|
+
@app.instance_variable_set(:@paste_batch, false)
|
|
715
|
+
feed(:escape)
|
|
716
|
+
|
|
717
|
+
assert_equal [" hello", "world"], @editor.current_buffer.lines
|
|
718
|
+
end
|
|
719
|
+
|
|
698
720
|
def test_batch_insert_stops_on_escape
|
|
699
721
|
@editor.current_buffer.replace_all_lines!([""])
|
|
700
722
|
# Escape exits insert mode; subsequent keys are normal-mode commands
|
|
@@ -1072,4 +1094,151 @@ class AppScenarioTest < Minitest::Test
|
|
|
1072
1094
|
assert_equal :hsplit, tree[:type]
|
|
1073
1095
|
assert_equal 3, tree[:children].length
|
|
1074
1096
|
end
|
|
1097
|
+
|
|
1098
|
+
# --- search filter (g/) ---
|
|
1099
|
+
|
|
1100
|
+
def test_filter_creates_buffer_with_matching_lines
|
|
1101
|
+
@editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot", "cherry"])
|
|
1102
|
+
feed("/", "a", "p", :enter) # search for "ap"
|
|
1103
|
+
feed("g", "/")
|
|
1104
|
+
|
|
1105
|
+
buf = @editor.current_buffer
|
|
1106
|
+
assert_equal :filter, buf.kind
|
|
1107
|
+
assert_equal ["apple", "apricot"], buf.lines
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
def test_filter_enter_jumps_to_original_line_and_closes_filter
|
|
1111
|
+
@editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot", "cherry"])
|
|
1112
|
+
original_buf_id = @editor.current_buffer.id
|
|
1113
|
+
feed("/", "a", "p", :enter)
|
|
1114
|
+
feed("g", "/")
|
|
1115
|
+
|
|
1116
|
+
# Move to second match line ("apricot", originally line 2)
|
|
1117
|
+
feed("j")
|
|
1118
|
+
feed(:enter)
|
|
1119
|
+
|
|
1120
|
+
assert_equal original_buf_id, @editor.current_buffer.id
|
|
1121
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
def test_filter_quit_returns_to_previous_buffer
|
|
1125
|
+
@editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot"])
|
|
1126
|
+
original_buf_id = @editor.current_buffer.id
|
|
1127
|
+
feed("/", "a", "p", :enter)
|
|
1128
|
+
feed("g", "/")
|
|
1129
|
+
|
|
1130
|
+
assert_equal :filter, @editor.current_buffer.kind
|
|
1131
|
+
feed(":", "q", :enter)
|
|
1132
|
+
|
|
1133
|
+
assert_equal original_buf_id, @editor.current_buffer.id
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def test_filter_recursive_filtering
|
|
1137
|
+
@editor.current_buffer.replace_all_lines!(["apple pie", "apricot jam", "apple sauce", "cherry"])
|
|
1138
|
+
feed("/", "a", "p", :enter)
|
|
1139
|
+
feed("g", "/")
|
|
1140
|
+
|
|
1141
|
+
assert_equal ["apple pie", "apricot jam", "apple sauce"], @editor.current_buffer.lines
|
|
1142
|
+
|
|
1143
|
+
# Search within filter and filter again
|
|
1144
|
+
feed("/", "p", "l", "e", :enter)
|
|
1145
|
+
feed("g", "/")
|
|
1146
|
+
|
|
1147
|
+
assert_equal ["apple pie", "apple sauce"], @editor.current_buffer.lines
|
|
1148
|
+
|
|
1149
|
+
# Enter jumps to original buffer
|
|
1150
|
+
feed("j") # "apple sauce" - originally line 2 of buffer
|
|
1151
|
+
feed(:enter)
|
|
1152
|
+
|
|
1153
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
def test_filter_inherits_filetype
|
|
1157
|
+
@editor.current_buffer.replace_all_lines!(["a\tb", "c\td", "a\te"])
|
|
1158
|
+
@editor.current_buffer.options["filetype"] = "tsv"
|
|
1159
|
+
feed("/", "a", :enter)
|
|
1160
|
+
feed("g", "/")
|
|
1161
|
+
|
|
1162
|
+
assert_equal "tsv", @editor.current_buffer.options["filetype"]
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1165
|
+
def test_filter_without_search_pattern_shows_error
|
|
1166
|
+
@editor.current_buffer.replace_all_lines!(["apple", "banana"])
|
|
1167
|
+
feed("g", "/")
|
|
1168
|
+
|
|
1169
|
+
assert @editor.message_error?
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def test_filter_quit_restores_cursor_position
|
|
1173
|
+
@editor.current_buffer.replace_all_lines!(["aaa", "bbb", "aab", "ccc", "aac"])
|
|
1174
|
+
original_buf_id = @editor.current_buffer.id
|
|
1175
|
+
feed("/", "a", "a", :enter)
|
|
1176
|
+
# Search moves cursor to line 0 (first match)
|
|
1177
|
+
feed("n")
|
|
1178
|
+
# Now on line 2 ("aab")
|
|
1179
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
1180
|
+
feed("g", "/")
|
|
1181
|
+
|
|
1182
|
+
assert_equal :filter, @editor.current_buffer.kind
|
|
1183
|
+
feed(":", "q", :enter)
|
|
1184
|
+
|
|
1185
|
+
assert_equal original_buf_id, @editor.current_buffer.id
|
|
1186
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
def test_filter_ex_command
|
|
1190
|
+
@editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot"])
|
|
1191
|
+
feed("/", "a", "p", :enter)
|
|
1192
|
+
feed(":", "f", "i", "l", "t", "e", "r", :enter)
|
|
1193
|
+
|
|
1194
|
+
assert_equal :filter, @editor.current_buffer.kind
|
|
1195
|
+
assert_equal ["apple", "apricot"], @editor.current_buffer.lines
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# --- dG / dgg / yG / ygg / cG / cgg ---
|
|
1199
|
+
|
|
1200
|
+
def test_dG_deletes_from_cursor_to_end
|
|
1201
|
+
@editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd", "ee"])
|
|
1202
|
+
@editor.current_window.cursor_y = 2
|
|
1203
|
+
feed("d", "G")
|
|
1204
|
+
|
|
1205
|
+
assert_equal ["aa", "bb"], @editor.current_buffer.lines
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def test_dgg_deletes_from_cursor_to_start
|
|
1209
|
+
@editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd", "ee"])
|
|
1210
|
+
@editor.current_window.cursor_y = 2
|
|
1211
|
+
feed("d", "g", "g")
|
|
1212
|
+
|
|
1213
|
+
assert_equal ["dd", "ee"], @editor.current_buffer.lines
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
def test_yG_yanks_from_cursor_to_end
|
|
1217
|
+
@editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd"])
|
|
1218
|
+
@editor.current_window.cursor_y = 1
|
|
1219
|
+
feed("y", "G")
|
|
1220
|
+
|
|
1221
|
+
reg = @editor.get_register('"')&.fetch(:text, "")
|
|
1222
|
+
assert_includes reg, "bb"
|
|
1223
|
+
assert_includes reg, "dd"
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
def test_ygg_yanks_from_cursor_to_start
|
|
1227
|
+
@editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd"])
|
|
1228
|
+
@editor.current_window.cursor_y = 2
|
|
1229
|
+
feed("y", "g", "g")
|
|
1230
|
+
|
|
1231
|
+
reg = @editor.get_register('"')&.fetch(:text, "")
|
|
1232
|
+
assert_includes reg, "aa"
|
|
1233
|
+
assert_includes reg, "cc"
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
def test_cG_changes_from_cursor_to_end
|
|
1237
|
+
@editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd"])
|
|
1238
|
+
@editor.current_window.cursor_y = 2
|
|
1239
|
+
feed("c", "G")
|
|
1240
|
+
|
|
1241
|
+
assert_equal ["aa", "bb", ""], @editor.current_buffer.lines
|
|
1242
|
+
assert_equal :insert, @editor.mode
|
|
1243
|
+
end
|
|
1075
1244
|
end
|
data/test/cli_test.rb
CHANGED
|
@@ -153,6 +153,20 @@ class CLITest < Minitest::Test
|
|
|
153
153
|
assert_equal "", err.string
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
def test_parse_follow_option
|
|
157
|
+
opts = RuVim::CLI.parse(["-f", "log.txt"])
|
|
158
|
+
|
|
159
|
+
assert_equal ["log.txt"], opts.files
|
|
160
|
+
assert_equal true, opts.follow
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_help_mentions_follow_option
|
|
164
|
+
out = StringIO.new
|
|
165
|
+
err = StringIO.new
|
|
166
|
+
RuVim::CLI.run(["--help"], stdout: out, stderr: err, stdin: StringIO.new)
|
|
167
|
+
assert_match(/-f\s+Open file in follow mode/, out.string)
|
|
168
|
+
end
|
|
169
|
+
|
|
156
170
|
def test_run_returns_error_for_missing_config_file
|
|
157
171
|
out = StringIO.new
|
|
158
172
|
err = StringIO.new
|