ruvim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +15 -0
- data/README.md +135 -0
- data/Rakefile +36 -0
- data/docs/binding.md +125 -0
- data/docs/command.md +306 -0
- data/docs/config.md +155 -0
- data/docs/done.md +112 -0
- data/docs/plugin.md +559 -0
- data/docs/spec.md +655 -0
- data/docs/todo.md +63 -0
- data/docs/tutorial.md +490 -0
- data/docs/vim_diff.md +179 -0
- data/exe/ruvim +6 -0
- data/lib/ruvim/app.rb +1600 -0
- data/lib/ruvim/buffer.rb +421 -0
- data/lib/ruvim/cli.rb +264 -0
- data/lib/ruvim/clipboard.rb +73 -0
- data/lib/ruvim/command_invocation.rb +14 -0
- data/lib/ruvim/command_line.rb +63 -0
- data/lib/ruvim/command_registry.rb +38 -0
- data/lib/ruvim/config_dsl.rb +134 -0
- data/lib/ruvim/config_loader.rb +68 -0
- data/lib/ruvim/context.rb +26 -0
- data/lib/ruvim/dispatcher.rb +120 -0
- data/lib/ruvim/display_width.rb +110 -0
- data/lib/ruvim/editor.rb +1025 -0
- data/lib/ruvim/ex_command_registry.rb +80 -0
- data/lib/ruvim/global_commands.rb +1889 -0
- data/lib/ruvim/highlighter.rb +52 -0
- data/lib/ruvim/input.rb +66 -0
- data/lib/ruvim/keymap_manager.rb +96 -0
- data/lib/ruvim/screen.rb +452 -0
- data/lib/ruvim/terminal.rb +30 -0
- data/lib/ruvim/text_metrics.rb +96 -0
- data/lib/ruvim/version.rb +5 -0
- data/lib/ruvim/window.rb +71 -0
- data/lib/ruvim.rb +30 -0
- data/sig/ruvim.rbs +4 -0
- data/test/app_completion_test.rb +39 -0
- data/test/app_dot_repeat_test.rb +54 -0
- data/test/app_motion_test.rb +73 -0
- data/test/app_register_test.rb +47 -0
- data/test/app_scenario_test.rb +77 -0
- data/test/app_startup_test.rb +199 -0
- data/test/app_text_object_test.rb +54 -0
- data/test/app_unicode_behavior_test.rb +66 -0
- data/test/buffer_test.rb +72 -0
- data/test/cli_test.rb +165 -0
- data/test/config_dsl_test.rb +78 -0
- data/test/dispatcher_test.rb +124 -0
- data/test/editor_mark_test.rb +69 -0
- data/test/editor_register_test.rb +64 -0
- data/test/fixtures/render_basic_snapshot.txt +8 -0
- data/test/fixtures/render_basic_snapshot_nonumber.txt +8 -0
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +7 -0
- data/test/highlighter_test.rb +16 -0
- data/test/input_screen_integration_test.rb +69 -0
- data/test/keymap_manager_test.rb +48 -0
- data/test/render_snapshot_test.rb +70 -0
- data/test/screen_test.rb +123 -0
- data/test/search_option_test.rb +39 -0
- data/test/test_helper.rb +15 -0
- data/test/text_metrics_test.rb +42 -0
- data/test/window_test.rb +21 -0
- metadata +106 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
module Highlighter
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def color_columns(filetype, line)
|
|
6
|
+
ft = filetype.to_s
|
|
7
|
+
text = line.to_s
|
|
8
|
+
return {} if text.empty?
|
|
9
|
+
|
|
10
|
+
case ft
|
|
11
|
+
when "ruby"
|
|
12
|
+
ruby_color_columns(text)
|
|
13
|
+
when "json"
|
|
14
|
+
json_color_columns(text)
|
|
15
|
+
else
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ruby_color_columns(text)
|
|
21
|
+
cols = {}
|
|
22
|
+
apply_regex(cols, text, /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/, "\e[32m")
|
|
23
|
+
apply_regex(cols, text, /\b(?:def|class|module|end|if|elsif|else|unless|case|when|do|while|until|begin|rescue|ensure|return|yield)\b/, "\e[36m")
|
|
24
|
+
apply_regex(cols, text, /\b\d+(?:\.\d+)?\b/, "\e[33m")
|
|
25
|
+
apply_regex(cols, text, /#.*\z/, "\e[90m", override: true)
|
|
26
|
+
cols
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def json_color_columns(text)
|
|
30
|
+
cols = {}
|
|
31
|
+
apply_regex(cols, text, /"(?:\\.|[^"\\])*"\s*(?=:)/, "\e[36m")
|
|
32
|
+
apply_regex(cols, text, /"(?:\\.|[^"\\])*"/, "\e[32m")
|
|
33
|
+
apply_regex(cols, text, /\b(?:true|false|null)\b/, "\e[35m")
|
|
34
|
+
apply_regex(cols, text, /-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/, "\e[33m")
|
|
35
|
+
cols
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def apply_regex(cols, text, regex, color, override: false)
|
|
39
|
+
text.to_enum(:scan, regex).each do
|
|
40
|
+
m = Regexp.last_match
|
|
41
|
+
next unless m
|
|
42
|
+
(m.begin(0)...m.end(0)).each do |idx|
|
|
43
|
+
next if cols.key?(idx) && !override
|
|
44
|
+
|
|
45
|
+
cols[idx] = color
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module_function :ruby_color_columns, :json_color_columns, :apply_regex
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/ruvim/input.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class Input
|
|
3
|
+
def initialize(stdin: STDIN)
|
|
4
|
+
@stdin = stdin
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def read_key(timeout: nil, wakeup_ios: [])
|
|
8
|
+
ios = [@stdin, *wakeup_ios].compact
|
|
9
|
+
readable = IO.select(ios, nil, nil, timeout)
|
|
10
|
+
return nil unless readable
|
|
11
|
+
|
|
12
|
+
ready = readable[0]
|
|
13
|
+
wakeups = ready - [@stdin]
|
|
14
|
+
wakeups.each { |io| drain_io(io) }
|
|
15
|
+
return nil unless ready.include?(@stdin)
|
|
16
|
+
|
|
17
|
+
ch = @stdin.getch
|
|
18
|
+
return :ctrl_c if ch == "\u0003"
|
|
19
|
+
return :ctrl_i if ch == "\u0009"
|
|
20
|
+
return :ctrl_n if ch == "\u000e"
|
|
21
|
+
return :ctrl_o if ch == "\u000f"
|
|
22
|
+
return :ctrl_p if ch == "\u0010"
|
|
23
|
+
return :ctrl_r if ch == "\u0012"
|
|
24
|
+
return :ctrl_v if ch == "\u0016"
|
|
25
|
+
return :ctrl_w if ch == "\u0017"
|
|
26
|
+
return :enter if ch == "\r" || ch == "\n"
|
|
27
|
+
return :backspace if ch == "\u007f" || ch == "\b"
|
|
28
|
+
|
|
29
|
+
return read_escape_sequence if ch == "\e"
|
|
30
|
+
|
|
31
|
+
ch
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def drain_io(io)
|
|
37
|
+
loop do
|
|
38
|
+
io.read_nonblock(1024)
|
|
39
|
+
end
|
|
40
|
+
rescue IO::WaitReadable, EOFError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_escape_sequence
|
|
45
|
+
extra = +""
|
|
46
|
+
begin
|
|
47
|
+
while IO.select([@stdin], nil, nil, 0.005)
|
|
48
|
+
extra << @stdin.read_nonblock(1)
|
|
49
|
+
end
|
|
50
|
+
rescue IO::WaitReadable, EOFError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
case extra
|
|
54
|
+
when "" then :escape
|
|
55
|
+
when "[A" then :up
|
|
56
|
+
when "[B" then :down
|
|
57
|
+
when "[C" then :right
|
|
58
|
+
when "[D" then :left
|
|
59
|
+
when "[5~" then :pageup
|
|
60
|
+
when "[6~" then :pagedown
|
|
61
|
+
else
|
|
62
|
+
[:escape_sequence, extra]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class KeymapManager
|
|
3
|
+
Match = Struct.new(:status, :invocation, keyword_init: true)
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@mode_maps = Hash.new { |h, k| h[k] = {} }
|
|
7
|
+
@global_map = {}
|
|
8
|
+
@buffer_maps = Hash.new { |h, k| h[k] = {} }
|
|
9
|
+
@filetype_maps = Hash.new { |h, k| h[k] = Hash.new { |hh, m| hh[m] = {} } }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def bind(mode, seq, id, argv: [], kwargs: {}, bang: false)
|
|
13
|
+
tokens = normalize_seq(seq)
|
|
14
|
+
@mode_maps[mode.to_sym][tokens] = build_invocation(id, argv:, kwargs:, bang:, tokens:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def bind_global(seq, id, argv: [], kwargs: {}, bang: false)
|
|
18
|
+
tokens = normalize_seq(seq)
|
|
19
|
+
@global_map[tokens] = build_invocation(id, argv:, kwargs:, bang:, tokens:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bind_buffer(buffer_id, seq, id, argv: [], kwargs: {}, bang: false)
|
|
23
|
+
tokens = normalize_seq(seq)
|
|
24
|
+
@buffer_maps[buffer_id][tokens] = build_invocation(id, argv:, kwargs:, bang:, tokens:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def bind_filetype(filetype, seq, id, mode: :normal, argv: [], kwargs: {}, bang: false)
|
|
28
|
+
tokens = normalize_seq(seq)
|
|
29
|
+
@filetype_maps[filetype.to_s][mode.to_sym][tokens] = build_invocation(id, argv:, kwargs:, bang:, tokens:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve(mode, pending_tokens)
|
|
33
|
+
resolve_layers([@mode_maps[mode.to_sym]], pending_tokens)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_with_context(mode, pending_tokens, editor:)
|
|
37
|
+
buffer = editor.current_buffer
|
|
38
|
+
filetype = detect_filetype(buffer)
|
|
39
|
+
layers = []
|
|
40
|
+
layers << @filetype_maps[filetype][mode.to_sym] if filetype && @filetype_maps.key?(filetype)
|
|
41
|
+
layers << @buffer_maps[buffer.id] if @buffer_maps.key?(buffer.id)
|
|
42
|
+
layers << @mode_maps[mode.to_sym]
|
|
43
|
+
layers << @global_map
|
|
44
|
+
resolve_layers(layers, pending_tokens)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_invocation(id, argv:, kwargs:, bang:, tokens:)
|
|
50
|
+
CommandInvocation.new(
|
|
51
|
+
id: id,
|
|
52
|
+
argv: argv,
|
|
53
|
+
kwargs: kwargs,
|
|
54
|
+
bang: bang,
|
|
55
|
+
raw_keys: tokens
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_layers(layers, pending_tokens)
|
|
60
|
+
layers = layers.compact
|
|
61
|
+
return Match.new(status: :none) if layers.empty?
|
|
62
|
+
|
|
63
|
+
layers.each do |layer|
|
|
64
|
+
next if layer.empty?
|
|
65
|
+
|
|
66
|
+
if (exact = layer[pending_tokens])
|
|
67
|
+
longer = layer.keys.any? { |k| k.length > pending_tokens.length && k[0, pending_tokens.length] == pending_tokens }
|
|
68
|
+
return Match.new(status: (longer ? :ambiguous : :match), invocation: exact)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
has_prefix = layers.any? { |layer| layer.keys.any? { |k| k[0, pending_tokens.length] == pending_tokens } }
|
|
73
|
+
Match.new(status: has_prefix ? :pending : :none)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def detect_filetype(buffer)
|
|
77
|
+
ft = buffer.options["filetype"] if buffer.respond_to?(:options)
|
|
78
|
+
return ft if ft && !ft.empty?
|
|
79
|
+
|
|
80
|
+
path = buffer.path.to_s
|
|
81
|
+
ext = File.extname(path)
|
|
82
|
+
return nil if ext.empty?
|
|
83
|
+
|
|
84
|
+
ext.delete_prefix(".")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalize_seq(seq)
|
|
88
|
+
case seq
|
|
89
|
+
when Array
|
|
90
|
+
seq.map(&:to_s).freeze
|
|
91
|
+
else
|
|
92
|
+
seq.to_s.each_char.map(&:to_s).freeze
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/ruvim/screen.rb
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class Screen
|
|
3
|
+
DEFAULT_TABSTOP = 2
|
|
4
|
+
SYNTAX_CACHE_LIMIT = 2048
|
|
5
|
+
def initialize(terminal:)
|
|
6
|
+
@terminal = terminal
|
|
7
|
+
@last_frame = nil
|
|
8
|
+
@syntax_color_cache = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def invalidate_cache!
|
|
12
|
+
@last_frame = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(editor)
|
|
16
|
+
rows, cols = @terminal.winsize
|
|
17
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
18
|
+
text_rows = [text_rows, 1].max
|
|
19
|
+
text_cols = [text_cols, 1].max
|
|
20
|
+
|
|
21
|
+
rects = window_rects(editor, text_rows:, text_cols:)
|
|
22
|
+
editor.window_order.each do |win_id|
|
|
23
|
+
win = editor.windows.fetch(win_id)
|
|
24
|
+
buf = editor.buffers.fetch(win.buffer_id)
|
|
25
|
+
rect = rects[win_id]
|
|
26
|
+
next unless rect
|
|
27
|
+
content_width = [rect[:width] - number_column_width(editor, win, buf), 1].max
|
|
28
|
+
win.ensure_visible(
|
|
29
|
+
buf,
|
|
30
|
+
height: [rect[:height], 1].max,
|
|
31
|
+
width: content_width,
|
|
32
|
+
tabstop: tabstop_for(editor, win, buf)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
frame = build_frame(editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
37
|
+
out = if can_diff_render?(frame)
|
|
38
|
+
render_diff(frame)
|
|
39
|
+
else
|
|
40
|
+
render_full(frame)
|
|
41
|
+
end
|
|
42
|
+
cursor_row, cursor_col = cursor_screen_position(editor, text_rows, rects)
|
|
43
|
+
out << "\e[#{cursor_row};#{cursor_col}H"
|
|
44
|
+
out << "\e[?25h"
|
|
45
|
+
@last_frame = frame.merge(cursor_row:, cursor_col:)
|
|
46
|
+
@terminal.write(out)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def current_window_view_height(editor)
|
|
50
|
+
rows, cols = @terminal.winsize
|
|
51
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
52
|
+
text_rows = [text_rows, 1].max
|
|
53
|
+
text_cols = [text_cols, 1].max
|
|
54
|
+
rect = window_rects(editor, text_rows:, text_cols:)[editor.current_window_id]
|
|
55
|
+
[rect ? rect[:height].to_i : text_rows, 1].max
|
|
56
|
+
rescue StandardError
|
|
57
|
+
1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_frame(editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
63
|
+
lines = {}
|
|
64
|
+
render_window_area(editor, lines, rects, text_rows:, text_cols:)
|
|
65
|
+
|
|
66
|
+
status_row = text_rows + 1
|
|
67
|
+
lines[status_row] = "\e[7m#{truncate(status_line(editor, cols), cols)}\e[m"
|
|
68
|
+
|
|
69
|
+
if editor.command_line_active?
|
|
70
|
+
cmd = editor.command_line
|
|
71
|
+
lines[status_row + 1] = truncate("#{cmd.prefix}#{cmd.text}", cols)
|
|
72
|
+
elsif editor.message_error?
|
|
73
|
+
lines[status_row + 1] = error_message_line(editor.message.to_s, cols)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
rows: rows,
|
|
78
|
+
cols: cols,
|
|
79
|
+
lines: lines,
|
|
80
|
+
rects: rects
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_window_area(editor, lines, rects, text_rows:, text_cols:)
|
|
85
|
+
if rects.values.any? { |r| r[:separator] == :vertical }
|
|
86
|
+
render_vertical_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
87
|
+
else
|
|
88
|
+
render_horizontal_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_horizontal_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
93
|
+
1.upto(text_rows) { |row_no| lines[row_no] = " " * text_cols }
|
|
94
|
+
|
|
95
|
+
editor.window_order.each do |win_id|
|
|
96
|
+
rect = rects[win_id]
|
|
97
|
+
next unless rect
|
|
98
|
+
|
|
99
|
+
window = editor.windows.fetch(win_id)
|
|
100
|
+
buffer = editor.buffers.fetch(window.buffer_id)
|
|
101
|
+
gutter_w = number_column_width(editor, window, buffer)
|
|
102
|
+
content_w = [rect[:width] - gutter_w, 1].max
|
|
103
|
+
rect[:height].times do |dy|
|
|
104
|
+
row_no = rect[:top] + dy
|
|
105
|
+
buffer_row = window.row_offset + dy
|
|
106
|
+
text =
|
|
107
|
+
if buffer_row < buffer.line_count
|
|
108
|
+
render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
109
|
+
else
|
|
110
|
+
line_number_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
|
|
111
|
+
end
|
|
112
|
+
lines[row_no] = text
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
rects.each_value do |rect|
|
|
117
|
+
next unless rect[:separator] == :horizontal
|
|
118
|
+
lines[rect[:sep_row]] = ("-" * text_cols)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def render_vertical_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
123
|
+
1.upto(text_rows) do |row_no|
|
|
124
|
+
pieces = +""
|
|
125
|
+
editor.window_order.each_with_index do |win_id, idx|
|
|
126
|
+
rect = rects[win_id]
|
|
127
|
+
next unless rect
|
|
128
|
+
window = editor.windows.fetch(win_id)
|
|
129
|
+
buffer = editor.buffers.fetch(window.buffer_id)
|
|
130
|
+
gutter_w = number_column_width(editor, window, buffer)
|
|
131
|
+
content_w = [rect[:width] - gutter_w, 1].max
|
|
132
|
+
dy = row_no - rect[:top]
|
|
133
|
+
text =
|
|
134
|
+
if dy >= 0 && dy < rect[:height]
|
|
135
|
+
buffer_row = window.row_offset + dy
|
|
136
|
+
if buffer_row < buffer.line_count
|
|
137
|
+
render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
138
|
+
else
|
|
139
|
+
line_number_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
" " * rect[:width]
|
|
143
|
+
end
|
|
144
|
+
pieces << text
|
|
145
|
+
pieces << "|" if idx < editor.window_order.length - 1
|
|
146
|
+
end
|
|
147
|
+
lines[row_no] = pieces
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def can_diff_render?(frame)
|
|
152
|
+
return false unless @last_frame
|
|
153
|
+
@last_frame[:rows] == frame[:rows] && @last_frame[:cols] == frame[:cols]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def render_full(frame)
|
|
157
|
+
out = +""
|
|
158
|
+
out << "\e[?25l"
|
|
159
|
+
out << "\e[H"
|
|
160
|
+
1.upto(frame[:rows]) do |row_no|
|
|
161
|
+
out << "\e[2K"
|
|
162
|
+
out << (frame[:lines][row_no] || "")
|
|
163
|
+
out << "\r\n" unless row_no == frame[:rows]
|
|
164
|
+
end
|
|
165
|
+
out
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def render_diff(frame)
|
|
169
|
+
out = +""
|
|
170
|
+
out << "\e[?25l"
|
|
171
|
+
max_rows = [frame[:rows], @last_frame[:rows]].max
|
|
172
|
+
1.upto(max_rows) do |row_no|
|
|
173
|
+
new_line = frame[:lines][row_no] || ""
|
|
174
|
+
old_line = @last_frame[:lines][row_no] || ""
|
|
175
|
+
next if new_line == old_line
|
|
176
|
+
|
|
177
|
+
out << "\e[#{row_no};1H"
|
|
178
|
+
out << "\e[2K"
|
|
179
|
+
out << new_line
|
|
180
|
+
end
|
|
181
|
+
out
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def render_text_line(text, editor, buffer_row:, window:, buffer:, width:)
|
|
185
|
+
tabstop = tabstop_for(editor, window, buffer)
|
|
186
|
+
cells, display_col = RuVim::TextMetrics.clip_cells_for_width(text, width, source_col_start: window.col_offset, tabstop:)
|
|
187
|
+
highlighted = +""
|
|
188
|
+
visual = (editor.current_window_id == window.id && editor.visual_active?) ? editor.visual_selection(window) : nil
|
|
189
|
+
search_cols = search_highlight_source_cols(editor, text, source_col_offset: window.col_offset)
|
|
190
|
+
syntax_cols = syntax_highlight_source_cols(editor, window, buffer, text, source_col_offset: window.col_offset)
|
|
191
|
+
|
|
192
|
+
cells.each_with_index do |cell, idx|
|
|
193
|
+
ch = cell.glyph
|
|
194
|
+
buffer_col = cell.source_col
|
|
195
|
+
selected = selected_in_visual?(visual, buffer_row, buffer_col)
|
|
196
|
+
cursor_here = (editor.current_window_id == window.id && window.cursor_y == buffer_row && window.cursor_x == buffer_col)
|
|
197
|
+
if selected || cursor_here
|
|
198
|
+
highlighted << "\e[7m#{ch}\e[m"
|
|
199
|
+
elsif search_cols[buffer_col]
|
|
200
|
+
highlighted << "\e[43m#{ch}\e[m"
|
|
201
|
+
elsif (syntax_color = syntax_cols[buffer_col])
|
|
202
|
+
highlighted << "#{syntax_color}#{ch}\e[m"
|
|
203
|
+
else
|
|
204
|
+
highlighted << ch
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if editor.current_window_id == window.id && window.cursor_y == buffer_row
|
|
209
|
+
col = window.cursor_x - window.col_offset
|
|
210
|
+
if col >= cells.length && col >= 0 && display_col < width
|
|
211
|
+
highlighted << "\e[7m \e[m"
|
|
212
|
+
display_col += 1
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
highlighted << (" " * [width - display_col, 0].max)
|
|
217
|
+
highlighted
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
221
|
+
line = buffer.line_at(buffer_row)
|
|
222
|
+
line = line[window.col_offset..] || ""
|
|
223
|
+
prefix = line_number_prefix(editor, window, buffer, buffer_row, gutter_w)
|
|
224
|
+
body = render_text_line(line, editor, buffer_row:, window:, buffer:, width: content_w)
|
|
225
|
+
prefix + body
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def tabstop_for(editor, window, buffer)
|
|
229
|
+
val = editor.effective_option("tabstop", window:, buffer:)
|
|
230
|
+
iv = val.to_i
|
|
231
|
+
iv.positive? ? iv : DEFAULT_TABSTOP
|
|
232
|
+
rescue StandardError
|
|
233
|
+
DEFAULT_TABSTOP
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def number_column_width(editor, window, buffer)
|
|
237
|
+
enabled = editor.effective_option("number", window:, buffer:) || editor.effective_option("relativenumber", window:, buffer:)
|
|
238
|
+
return 0 unless enabled
|
|
239
|
+
|
|
240
|
+
[buffer.line_count.to_s.length, 1].max + 1
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def line_number_prefix(editor, window, buffer, buffer_row, width)
|
|
244
|
+
return "" if width <= 0
|
|
245
|
+
show_abs = editor.effective_option("number", window:, buffer:)
|
|
246
|
+
show_rel = editor.effective_option("relativenumber", window:, buffer:)
|
|
247
|
+
return " " * width unless show_abs || show_rel
|
|
248
|
+
return " " * (width - 1) + " " if buffer_row.nil?
|
|
249
|
+
|
|
250
|
+
num =
|
|
251
|
+
if show_rel && buffer_row != window.cursor_y
|
|
252
|
+
(buffer_row - window.cursor_y).abs.to_s
|
|
253
|
+
elsif show_abs
|
|
254
|
+
(buffer_row + 1).to_s
|
|
255
|
+
else
|
|
256
|
+
"0"
|
|
257
|
+
end
|
|
258
|
+
num.rjust(width - 1) + " "
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def pad_plain_display(text, width)
|
|
262
|
+
RuVim::TextMetrics.pad_plain_to_screen_width(text, width, tabstop: DEFAULT_TABSTOP)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def status_line(editor, width)
|
|
266
|
+
buffer = editor.current_buffer
|
|
267
|
+
window = editor.current_window
|
|
268
|
+
mode = case editor.mode
|
|
269
|
+
when :insert then "-- INSERT --"
|
|
270
|
+
when :command_line then "-- COMMAND --"
|
|
271
|
+
when :visual_char then "-- VISUAL --"
|
|
272
|
+
when :visual_line then "-- VISUAL LINE --"
|
|
273
|
+
when :visual_block then "-- VISUAL BLOCK --"
|
|
274
|
+
else "-- NORMAL --"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
path = buffer.display_name
|
|
278
|
+
ft = editor.effective_option("filetype", buffer:, window:) || File.extname(buffer.path.to_s).delete_prefix(".")
|
|
279
|
+
ft = "-" if ft.empty?
|
|
280
|
+
mod = buffer.modified? ? " [+]" : ""
|
|
281
|
+
msg = editor.message_error? ? "" : editor.message.to_s
|
|
282
|
+
win_idx = (editor.window_order.index(editor.current_window_id) || 0) + 1
|
|
283
|
+
win_total = editor.window_order.length
|
|
284
|
+
tab_info = "t#{editor.current_tabpage_number}/#{editor.tabpage_count}"
|
|
285
|
+
left = "#{mode} #{tab_info} w#{win_idx}/#{win_total} b#{buffer.id} #{path} [ft=#{ft}]#{mod}"
|
|
286
|
+
right = " #{window.cursor_y + 1}:#{window.cursor_x + 1} "
|
|
287
|
+
body_width = [width - right.length, 0].max
|
|
288
|
+
"#{compose_status_body(left, msg, body_width)}#{right}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def compose_status_body(left, msg, width)
|
|
292
|
+
w = [width.to_i, 0].max
|
|
293
|
+
return "" if w.zero?
|
|
294
|
+
return left.ljust(w)[0, w] if msg.to_s.empty?
|
|
295
|
+
|
|
296
|
+
msg_part = " | #{msg}"
|
|
297
|
+
if msg_part.length >= w
|
|
298
|
+
return msg_part[0, w]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
left_budget = w - msg_part.length
|
|
302
|
+
"#{left.ljust(left_budget)[0, left_budget]}#{msg_part}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def truncate(str, width)
|
|
306
|
+
str.to_s.ljust(width)[0, width]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def error_message_line(msg, cols)
|
|
310
|
+
"\e[97;41m#{truncate(msg, cols)}\e[m"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def cursor_screen_position(editor, text_rows, rects)
|
|
314
|
+
window = editor.current_window
|
|
315
|
+
|
|
316
|
+
if editor.command_line_active?
|
|
317
|
+
row = text_rows + 2
|
|
318
|
+
col = 1 + editor.command_line.prefix.length + editor.command_line.cursor
|
|
319
|
+
return [row, col]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
rect = rects[window.id] || { top: 1, left: 1 }
|
|
323
|
+
row = rect[:top] + (window.cursor_y - window.row_offset)
|
|
324
|
+
line = editor.current_buffer.line_at(window.cursor_y)
|
|
325
|
+
gutter_w = number_column_width(editor, window, editor.current_buffer)
|
|
326
|
+
tabstop = tabstop_for(editor, window, editor.current_buffer)
|
|
327
|
+
prefix_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) -
|
|
328
|
+
RuVim::TextMetrics.screen_col_for_char_index(line, window.col_offset, tabstop:)
|
|
329
|
+
col = rect[:left] + gutter_w + [prefix_screen_col, 0].max
|
|
330
|
+
[row, col]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def window_rects(editor, text_rows:, text_cols:)
|
|
334
|
+
ids = editor.window_order
|
|
335
|
+
return {} if ids.empty?
|
|
336
|
+
return { ids.first => { top: 1, left: 1, height: text_rows, width: text_cols } } if ids.length == 1 || editor.window_layout == :single
|
|
337
|
+
|
|
338
|
+
if editor.window_layout == :vertical
|
|
339
|
+
sep = ids.length - 1
|
|
340
|
+
usable = [text_cols - sep, ids.length].max
|
|
341
|
+
widths = split_sizes(usable, ids.length)
|
|
342
|
+
left = 1
|
|
343
|
+
rects = {}
|
|
344
|
+
ids.each_with_index do |id, i|
|
|
345
|
+
w = widths[i]
|
|
346
|
+
rects[id] = { top: 1, left: left, height: text_rows, width: w, separator: :vertical }
|
|
347
|
+
left += w + 1
|
|
348
|
+
end
|
|
349
|
+
rects
|
|
350
|
+
else
|
|
351
|
+
sep = ids.length - 1
|
|
352
|
+
usable = [text_rows - sep, ids.length].max
|
|
353
|
+
heights = split_sizes(usable, ids.length)
|
|
354
|
+
top = 1
|
|
355
|
+
rects = {}
|
|
356
|
+
ids.each_with_index do |id, i|
|
|
357
|
+
h = heights[i]
|
|
358
|
+
rects[id] = { top: top, left: 1, height: h, width: text_cols, separator: :horizontal }
|
|
359
|
+
top += h
|
|
360
|
+
if i < ids.length - 1
|
|
361
|
+
rects[id][:sep_row] = top
|
|
362
|
+
top += 1
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
rects
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def split_sizes(total, n)
|
|
370
|
+
base = total / n
|
|
371
|
+
rem = total % n
|
|
372
|
+
Array.new(n) { |i| base + (i < rem ? 1 : 0) }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def selected_in_visual?(visual, row, col)
|
|
376
|
+
return false unless visual
|
|
377
|
+
|
|
378
|
+
if visual[:mode] == :linewise
|
|
379
|
+
row >= visual[:start_row] && row <= visual[:end_row]
|
|
380
|
+
elsif visual[:mode] == :blockwise
|
|
381
|
+
row >= visual[:start_row] && row <= visual[:end_row] &&
|
|
382
|
+
col >= visual[:start_col] && col < visual[:end_col]
|
|
383
|
+
else
|
|
384
|
+
return false if row < visual[:start_row] || row > visual[:end_row]
|
|
385
|
+
if visual[:start_row] == visual[:end_row]
|
|
386
|
+
col >= visual[:start_col] && col < visual[:end_col]
|
|
387
|
+
elsif row == visual[:start_row]
|
|
388
|
+
col >= visual[:start_col]
|
|
389
|
+
elsif row == visual[:end_row]
|
|
390
|
+
col < visual[:end_col]
|
|
391
|
+
else
|
|
392
|
+
true
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def search_highlight_source_cols(editor, source_line_text, source_col_offset:)
|
|
398
|
+
search = editor.last_search
|
|
399
|
+
return {} unless search && search[:pattern]
|
|
400
|
+
return {} unless editor.effective_option("hlsearch")
|
|
401
|
+
|
|
402
|
+
regex = build_screen_search_regex(editor, search[:pattern])
|
|
403
|
+
cols = {}
|
|
404
|
+
offset = 0
|
|
405
|
+
while (m = regex.match(source_line_text, offset))
|
|
406
|
+
from = m.begin(0)
|
|
407
|
+
to = [m.end(0), from + 1].max
|
|
408
|
+
(from...to).each { |i| cols[source_col_offset + i] = true }
|
|
409
|
+
offset = to
|
|
410
|
+
break if offset > source_line_text.length
|
|
411
|
+
end
|
|
412
|
+
cols
|
|
413
|
+
rescue RegexpError
|
|
414
|
+
{}
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def build_screen_search_regex(editor, pattern)
|
|
418
|
+
ignorecase = !!editor.effective_option("ignorecase")
|
|
419
|
+
smartcase = !!editor.effective_option("smartcase")
|
|
420
|
+
flags = if ignorecase && !(smartcase && pattern.to_s.match?(/[A-Z]/))
|
|
421
|
+
Regexp::IGNORECASE
|
|
422
|
+
else
|
|
423
|
+
0
|
|
424
|
+
end
|
|
425
|
+
Regexp.new(pattern.to_s, flags)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def syntax_highlight_source_cols(editor, window, buffer, source_line_text, source_col_offset:)
|
|
429
|
+
filetype = editor.effective_option("filetype", buffer:, window:)
|
|
430
|
+
rel = cached_syntax_color_columns(filetype, source_line_text)
|
|
431
|
+
return {} if rel.empty?
|
|
432
|
+
|
|
433
|
+
rel.each_with_object({}) do |(idx, color), h|
|
|
434
|
+
h[source_col_offset + idx] = color
|
|
435
|
+
end
|
|
436
|
+
rescue StandardError
|
|
437
|
+
{}
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def cached_syntax_color_columns(filetype, source_line_text)
|
|
441
|
+
key = [filetype.to_s, source_line_text.to_s]
|
|
442
|
+
if (cached = @syntax_color_cache[key])
|
|
443
|
+
return cached
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
cols = RuVim::Highlighter.color_columns(filetype, source_line_text)
|
|
447
|
+
@syntax_color_cache[key] = cols
|
|
448
|
+
@syntax_color_cache.shift while @syntax_color_cache.length > SYNTAX_CACHE_LIMIT
|
|
449
|
+
cols
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class Terminal
|
|
5
|
+
def initialize(stdin: STDIN, stdout: STDOUT)
|
|
6
|
+
@stdin = stdin
|
|
7
|
+
@stdout = stdout
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def winsize
|
|
11
|
+
IO.console.winsize
|
|
12
|
+
rescue StandardError
|
|
13
|
+
[24, 80]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write(str)
|
|
17
|
+
@stdout.write(str)
|
|
18
|
+
@stdout.flush
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def with_ui
|
|
22
|
+
@stdin.raw do
|
|
23
|
+
write("\e[?1049h\e[?25l")
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
write("\e[?25h\e[?1049l")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|