ruvim 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +23 -0
- data/docs/command.md +85 -0
- data/docs/config.md +2 -2
- data/docs/done.md +21 -0
- data/docs/spec.md +157 -12
- data/docs/todo.md +1 -5
- data/docs/vim_diff.md +94 -172
- data/lib/ruvim/app.rb +882 -69
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +455 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +890 -63
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +39 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/json.rb +18 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +503 -106
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +14 -0
- data/test/app_completion_test.rb +73 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +729 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/dispatcher_test.rb +322 -0
- data/test/editor_register_test.rb +23 -0
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +478 -0
- data/test/screen_test.rb +304 -0
- metadata +33 -2
data/lib/ruvim/screen.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class Screen
|
|
3
5
|
DEFAULT_TABSTOP = 2
|
|
4
6
|
SYNTAX_CACHE_LIMIT = 2048
|
|
7
|
+
WRAP_SEGMENTS_CACHE_LIMIT = 1024
|
|
5
8
|
def initialize(terminal:)
|
|
6
9
|
@terminal = terminal
|
|
7
10
|
@last_frame = nil
|
|
8
11
|
@syntax_color_cache = {}
|
|
12
|
+
@wrapped_segments_cache = {}
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def invalidate_cache!
|
|
@@ -13,6 +17,8 @@ module RuVim
|
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
def render(editor)
|
|
20
|
+
@rich_render_info = nil
|
|
21
|
+
|
|
16
22
|
rows, cols = @terminal.winsize
|
|
17
23
|
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
18
24
|
text_rows = [text_rows, 1].max
|
|
@@ -28,15 +34,31 @@ module RuVim
|
|
|
28
34
|
rect = rects[win_id]
|
|
29
35
|
next unless rect
|
|
30
36
|
content_width = [rect[:width] - number_column_width(editor, win, buf), 1].max
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
if RuVim::RichView.active?(editor)
|
|
38
|
+
# Vertical scrolling only — keep raw col_offset untouched
|
|
39
|
+
win.ensure_visible(
|
|
40
|
+
buf,
|
|
41
|
+
height: [rect[:height], 1].max,
|
|
42
|
+
width: content_width,
|
|
43
|
+
tabstop: tabstop_for(editor, win, buf),
|
|
44
|
+
scrolloff: editor.effective_option("scrolloff", window: win, buffer: buf),
|
|
45
|
+
sidescrolloff: editor.effective_option("sidescrolloff", window: win, buffer: buf)
|
|
46
|
+
)
|
|
47
|
+
ensure_visible_rich(editor, win, buf, rect, content_width)
|
|
48
|
+
else
|
|
49
|
+
win.col_offset = 0 if wrap_enabled?(editor, win, buf)
|
|
50
|
+
win.ensure_visible(
|
|
51
|
+
buf,
|
|
52
|
+
height: [rect[:height], 1].max,
|
|
53
|
+
width: content_width,
|
|
54
|
+
tabstop: tabstop_for(editor, win, buf),
|
|
55
|
+
scrolloff: editor.effective_option("scrolloff", window: win, buffer: buf),
|
|
56
|
+
sidescrolloff: editor.effective_option("sidescrolloff", window: win, buffer: buf)
|
|
57
|
+
)
|
|
58
|
+
if wrap_enabled?(editor, win, buf)
|
|
59
|
+
ensure_visible_under_wrap(editor, win, buf, height: [rect[:height], 1].max, content_w: content_width)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
40
62
|
end
|
|
41
63
|
|
|
42
64
|
frame = build_frame(editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
@@ -71,15 +93,19 @@ module RuVim
|
|
|
71
93
|
lines = {}
|
|
72
94
|
render_window_area(editor, lines, rects, text_rows:, text_cols:)
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
if editor.hit_enter_active? && editor.hit_enter_lines
|
|
97
|
+
render_hit_enter_overlay(editor, lines, text_rows:, cols:)
|
|
98
|
+
else
|
|
99
|
+
status_row = text_rows + 1
|
|
100
|
+
lines[status_row] = "\e[7m#{truncate(status_line(editor, cols), cols)}\e[m"
|
|
101
|
+
lines[status_row + 1] = ""
|
|
102
|
+
|
|
103
|
+
if editor.command_line_active?
|
|
104
|
+
cmd = editor.command_line
|
|
105
|
+
lines[status_row + 1] = truncate("#{cmd.prefix}#{cmd.text}", cols)
|
|
106
|
+
elsif editor.message_error?
|
|
107
|
+
lines[status_row + 1] = error_message_line(editor.message.to_s, cols)
|
|
108
|
+
end
|
|
83
109
|
end
|
|
84
110
|
|
|
85
111
|
{
|
|
@@ -90,66 +116,129 @@ module RuVim
|
|
|
90
116
|
}
|
|
91
117
|
end
|
|
92
118
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
def render_hit_enter_overlay(editor, lines, text_rows:, cols:)
|
|
120
|
+
msg_lines = editor.hit_enter_lines
|
|
121
|
+
prompt = "Press ENTER or type command to continue"
|
|
122
|
+
# Total rows available: text_rows (text area) + 1 (status) + 1 (command) = text_rows + 2
|
|
123
|
+
total_rows = text_rows + 2
|
|
124
|
+
# We need msg_lines.length rows for messages + 1 row for the prompt
|
|
125
|
+
overlay_count = msg_lines.length + 1
|
|
126
|
+
start_row = [total_rows - overlay_count + 1, 1].max
|
|
127
|
+
msg_lines.each_with_index do |line, i|
|
|
128
|
+
row_no = start_row + i
|
|
129
|
+
break if row_no > total_rows
|
|
130
|
+
lines[row_no] = truncate(line.to_s, cols)
|
|
98
131
|
end
|
|
132
|
+
prompt_row = start_row + msg_lines.length
|
|
133
|
+
prompt_row = [prompt_row, total_rows].min
|
|
134
|
+
lines[prompt_row] = "\e[7m#{truncate(prompt, cols)}\e[m"
|
|
99
135
|
end
|
|
100
136
|
|
|
101
|
-
def
|
|
102
|
-
|
|
137
|
+
def render_window_area(editor, lines, rects, text_rows:, text_cols:)
|
|
138
|
+
render_tree_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
139
|
+
end
|
|
103
140
|
|
|
141
|
+
def render_tree_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
142
|
+
# Pre-render each window's rows
|
|
143
|
+
window_rows_cache = {}
|
|
104
144
|
editor.window_order.each do |win_id|
|
|
105
145
|
rect = rects[win_id]
|
|
106
146
|
next unless rect
|
|
107
|
-
|
|
108
147
|
window = editor.windows.fetch(win_id)
|
|
109
148
|
buffer = editor.buffers.fetch(window.buffer_id)
|
|
110
149
|
gutter_w = number_column_width(editor, window, buffer)
|
|
111
150
|
content_w = [rect[:width] - gutter_w, 1].max
|
|
112
|
-
|
|
113
|
-
rect[:height].times do |dy|
|
|
114
|
-
row_no = rect[:top] + dy
|
|
115
|
-
lines[row_no] = rows[dy] || (" " * rect[:width])
|
|
116
|
-
end
|
|
151
|
+
window_rows_cache[win_id] = window_render_rows(editor, window, buffer, height: rect[:height], gutter_w:, content_w:)
|
|
117
152
|
end
|
|
118
153
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
lines[rect[:sep_row]] = ("-" * text_cols)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
154
|
+
# Build a row-plan: for each screen row, collect the pieces to concatenate
|
|
155
|
+
row_plans = build_row_plans(editor.layout_tree, rects, text_rows, text_cols)
|
|
124
156
|
|
|
125
|
-
def render_vertical_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
126
157
|
1.upto(text_rows) do |row_no|
|
|
158
|
+
plan = row_plans[row_no]
|
|
159
|
+
unless plan
|
|
160
|
+
lines[row_no] = " " * text_cols
|
|
161
|
+
next
|
|
162
|
+
end
|
|
163
|
+
|
|
127
164
|
pieces = +""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@__window_rows_cache[key][dy] || (" " * rect[:width])
|
|
144
|
-
else
|
|
145
|
-
" " * rect[:width]
|
|
146
|
-
end
|
|
147
|
-
pieces << text
|
|
148
|
-
pieces << "|" if idx < editor.window_order.length - 1
|
|
165
|
+
plan.each do |piece|
|
|
166
|
+
case piece[:type]
|
|
167
|
+
when :window
|
|
168
|
+
rect = rects[piece[:id]]
|
|
169
|
+
dy = row_no - rect[:top]
|
|
170
|
+
rows = window_rows_cache[piece[:id]]
|
|
171
|
+
text = (rows && dy >= 0 && dy < rect[:height]) ? (rows[dy] || " " * rect[:width]) : " " * rect[:width]
|
|
172
|
+
pieces << text
|
|
173
|
+
when :vsep
|
|
174
|
+
pieces << "|"
|
|
175
|
+
when :hsep
|
|
176
|
+
pieces << "-" * piece[:width]
|
|
177
|
+
when :blank
|
|
178
|
+
pieces << " " * piece[:width]
|
|
179
|
+
end
|
|
149
180
|
end
|
|
150
181
|
lines[row_no] = pieces
|
|
151
182
|
end
|
|
152
|
-
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Build a row-by-row plan for compositing. Each row's plan is an array of
|
|
186
|
+
# pieces to concatenate left-to-right.
|
|
187
|
+
def build_row_plans(node, rects, text_rows, text_cols)
|
|
188
|
+
plans = {}
|
|
189
|
+
fill_row_plans(node, rects, plans, 1, text_rows)
|
|
190
|
+
plans
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def fill_row_plans(node, rects, plans, row_start, row_end)
|
|
194
|
+
return unless node
|
|
195
|
+
|
|
196
|
+
if node[:type] == :window
|
|
197
|
+
rect = rects[node[:id]]
|
|
198
|
+
return unless rect
|
|
199
|
+
rect[:height].times do |dy|
|
|
200
|
+
row_no = rect[:top] + dy
|
|
201
|
+
next if row_no < row_start || row_no > row_end
|
|
202
|
+
plans[row_no] ||= []
|
|
203
|
+
plans[row_no] << { type: :window, id: node[:id] }
|
|
204
|
+
end
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
children = node[:children]
|
|
209
|
+
if node[:type] == :vsplit
|
|
210
|
+
children.each_with_index do |child, i|
|
|
211
|
+
fill_row_plans(child, rects, plans, row_start, row_end)
|
|
212
|
+
if i < children.length - 1
|
|
213
|
+
# Insert vsep marker for the rows spanned by these children
|
|
214
|
+
child_leaves = tree_leaves_for_rects(child)
|
|
215
|
+
child_rects_list = child_leaves.filter_map { |id| rects[id] }
|
|
216
|
+
next if child_rects_list.empty?
|
|
217
|
+
top = child_rects_list.map { |r| r[:top] }.min
|
|
218
|
+
bottom = child_rects_list.map { |r| r[:top] + r[:height] - 1 }.max
|
|
219
|
+
top.upto(bottom) do |row_no|
|
|
220
|
+
next if row_no < row_start || row_no > row_end
|
|
221
|
+
plans[row_no] ||= []
|
|
222
|
+
plans[row_no] << { type: :vsep }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
elsif node[:type] == :hsplit
|
|
227
|
+
children.each_with_index do |child, i|
|
|
228
|
+
fill_row_plans(child, rects, plans, row_start, row_end)
|
|
229
|
+
if i < children.length - 1
|
|
230
|
+
child_leaves = tree_leaves_for_rects(child)
|
|
231
|
+
child_rects_list = child_leaves.filter_map { |id| rects[id] }
|
|
232
|
+
next if child_rects_list.empty?
|
|
233
|
+
sep_row = child_rects_list.map { |r| r[:top] + r[:height] }.max
|
|
234
|
+
left = child_rects_list.map { |r| r[:left] }.min
|
|
235
|
+
right = child_rects_list.map { |r| r[:left] + r[:width] - 1 }.max
|
|
236
|
+
next unless sep_row >= row_start && sep_row <= row_end
|
|
237
|
+
plans[sep_row] ||= []
|
|
238
|
+
plans[sep_row] << { type: :hsep, width: right - left + 1 }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
153
242
|
end
|
|
154
243
|
|
|
155
244
|
def can_diff_render?(frame)
|
|
@@ -301,22 +390,144 @@ module RuVim
|
|
|
301
390
|
end
|
|
302
391
|
|
|
303
392
|
def plain_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
393
|
+
if RuVim::RichView.active?(editor)
|
|
394
|
+
return rich_view_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
395
|
+
end
|
|
396
|
+
|
|
304
397
|
Array.new(height) do |dy|
|
|
305
398
|
buffer_row = window.row_offset + dy
|
|
306
399
|
if buffer_row < buffer.line_count
|
|
307
400
|
render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
308
401
|
else
|
|
309
|
-
|
|
402
|
+
render_gutter_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
|
|
310
403
|
end
|
|
311
404
|
end
|
|
312
405
|
end
|
|
313
406
|
|
|
407
|
+
def rich_view_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
408
|
+
raw_lines = []
|
|
409
|
+
height.times do |dy|
|
|
410
|
+
row = window.row_offset + dy
|
|
411
|
+
raw_lines << (row < buffer.line_count ? buffer.line_at(row) : nil)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
non_nil = raw_lines.compact
|
|
415
|
+
context = rich_view_context(editor, window, buffer)
|
|
416
|
+
formatted = RuVim::RichView.render_visible_lines(editor, non_nil, context: context)
|
|
417
|
+
fmt_idx = 0
|
|
418
|
+
col_offset_sc = @rich_render_info ? @rich_render_info[:col_offset_sc] : 0
|
|
419
|
+
|
|
420
|
+
Array.new(height) do |dy|
|
|
421
|
+
buffer_row = window.row_offset + dy
|
|
422
|
+
prefix = render_gutter_prefix(editor, window, buffer, buffer_row < buffer.line_count ? buffer_row : nil, gutter_w)
|
|
423
|
+
if buffer_row < buffer.line_count
|
|
424
|
+
line = formatted[fmt_idx] || ""
|
|
425
|
+
fmt_idx += 1
|
|
426
|
+
body = render_rich_view_line_sc(line, width: content_w, skip_sc: col_offset_sc)
|
|
427
|
+
prefix + body
|
|
428
|
+
else
|
|
429
|
+
prefix + pad_plain_display("~", content_w)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def rich_view_context(editor, window, buffer)
|
|
435
|
+
state = editor.rich_state
|
|
436
|
+
return {} unless state
|
|
437
|
+
|
|
438
|
+
format = state[:format]
|
|
439
|
+
renderer = RuVim::RichView.renderer_for(format)
|
|
440
|
+
return {} unless renderer && renderer.respond_to?(:needs_pre_context?) && renderer.needs_pre_context?
|
|
441
|
+
|
|
442
|
+
# Collect lines before the visible area for state tracking (e.g., code fences)
|
|
443
|
+
pre_lines = []
|
|
444
|
+
(0...window.row_offset).each do |row|
|
|
445
|
+
pre_lines << buffer.line_at(row) if row < buffer.line_count
|
|
446
|
+
end
|
|
447
|
+
{ pre_context_lines: pre_lines }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Render a formatted rich-view line by skipping `skip_sc` display columns
|
|
451
|
+
# then showing the next `width` display columns. Using screen columns
|
|
452
|
+
# instead of character indices keeps alignment correct when lines mix
|
|
453
|
+
# CJK and ASCII characters with different char-to-display-width ratios.
|
|
454
|
+
# ANSI escape sequences (\e[...m) are treated as zero-width and passed
|
|
455
|
+
# through to the output unchanged.
|
|
456
|
+
def render_rich_view_line_sc(text, width:, skip_sc:)
|
|
457
|
+
# Phase 1: skip `skip_sc` display columns
|
|
458
|
+
# Collect ANSI sequences encountered during skip so active styles carry over.
|
|
459
|
+
chars = text.to_s
|
|
460
|
+
pos = 0
|
|
461
|
+
skipped = 0
|
|
462
|
+
len = chars.length
|
|
463
|
+
pending_ansi = +""
|
|
464
|
+
while pos < len
|
|
465
|
+
if chars[pos] == "\e"
|
|
466
|
+
end_pos = find_ansi_end(chars, pos)
|
|
467
|
+
pending_ansi << chars[pos...end_pos]
|
|
468
|
+
pos = end_pos
|
|
469
|
+
next
|
|
470
|
+
end
|
|
471
|
+
ch = chars[pos]
|
|
472
|
+
cw = RuVim::DisplayWidth.cell_width(ch, col: skipped, tabstop: 8)
|
|
473
|
+
break if skipped + cw > skip_sc
|
|
474
|
+
skipped += cw
|
|
475
|
+
pos += 1
|
|
476
|
+
end
|
|
477
|
+
# If a wide char straddles the skip boundary, pad with a space
|
|
478
|
+
leading_pad = skip_sc - skipped
|
|
479
|
+
|
|
480
|
+
# Phase 2: collect `width` display columns
|
|
481
|
+
out = +""
|
|
482
|
+
out << pending_ansi unless pending_ansi.empty?
|
|
483
|
+
col = 0
|
|
484
|
+
if leading_pad > 0
|
|
485
|
+
out << " " * leading_pad
|
|
486
|
+
col += leading_pad
|
|
487
|
+
pos += 1 if pos < len && chars[pos] != "\e"
|
|
488
|
+
end
|
|
489
|
+
while pos < len
|
|
490
|
+
if chars[pos] == "\e"
|
|
491
|
+
end_pos = find_ansi_end(chars, pos)
|
|
492
|
+
out << chars[pos...end_pos]
|
|
493
|
+
pos = end_pos
|
|
494
|
+
next
|
|
495
|
+
end
|
|
496
|
+
ch = chars[pos]
|
|
497
|
+
cw = RuVim::DisplayWidth.cell_width(ch, col: skipped + col, tabstop: 8)
|
|
498
|
+
break if col + cw > width
|
|
499
|
+
out << ch
|
|
500
|
+
col += cw
|
|
501
|
+
pos += 1
|
|
502
|
+
end
|
|
503
|
+
out << " " * [width - col, 0].max
|
|
504
|
+
out << "\e[m"
|
|
505
|
+
out
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Find the end position of an ANSI escape sequence starting at `pos`.
|
|
509
|
+
# Handles CSI sequences (\e[...X) where X is a letter.
|
|
510
|
+
def find_ansi_end(str, pos)
|
|
511
|
+
i = pos + 1 # skip \e
|
|
512
|
+
return i if i >= str.length
|
|
513
|
+
if str[i] == "["
|
|
514
|
+
i += 1
|
|
515
|
+
# Skip parameter bytes and intermediate bytes
|
|
516
|
+
i += 1 while i < str.length && str[i].ord >= 0x20 && str[i].ord <= 0x3F
|
|
517
|
+
# Final byte
|
|
518
|
+
i += 1 if i < str.length && str[i].ord >= 0x40 && str[i].ord <= 0x7E
|
|
519
|
+
else
|
|
520
|
+
i += 1
|
|
521
|
+
end
|
|
522
|
+
i
|
|
523
|
+
end
|
|
524
|
+
|
|
314
525
|
def wrapped_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
315
526
|
rows = []
|
|
316
527
|
row_idx = window.row_offset
|
|
317
528
|
while rows.length < height
|
|
318
529
|
if row_idx >= buffer.line_count
|
|
319
|
-
rows << (
|
|
530
|
+
rows << (render_gutter_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w))
|
|
320
531
|
next
|
|
321
532
|
end
|
|
322
533
|
|
|
@@ -325,7 +536,7 @@ module RuVim
|
|
|
325
536
|
segments.each_with_index do |seg, seg_i|
|
|
326
537
|
break if rows.length >= height
|
|
327
538
|
|
|
328
|
-
gutter =
|
|
539
|
+
gutter = render_gutter_prefix(editor, window, buffer, seg_i.zero? ? row_idx : nil, gutter_w)
|
|
329
540
|
rows << gutter + render_text_segment(line, editor, buffer_row: row_idx, window:, buffer:, width: content_w,
|
|
330
541
|
source_col_start: seg[:source_col_start], display_prefix: seg[:display_prefix])
|
|
331
542
|
end
|
|
@@ -334,7 +545,92 @@ module RuVim
|
|
|
334
545
|
rows
|
|
335
546
|
end
|
|
336
547
|
|
|
548
|
+
def ensure_visible_under_wrap(editor, window, buffer, height:, content_w:)
|
|
549
|
+
return if height.to_i <= 0 || buffer.line_count <= 0
|
|
550
|
+
|
|
551
|
+
window.row_offset = [[window.row_offset.to_i, 0].max, buffer.line_count - 1].min
|
|
552
|
+
return if window.cursor_y < window.row_offset
|
|
553
|
+
|
|
554
|
+
cursor_line = buffer.line_at(window.cursor_y)
|
|
555
|
+
cursor_segs = wrapped_segments_for_line(editor, window, buffer, cursor_line, width: content_w)
|
|
556
|
+
cursor_seg_index = wrapped_segment_index(cursor_segs, window.cursor_x)
|
|
557
|
+
|
|
558
|
+
visual_rows_before = 0
|
|
559
|
+
row = window.row_offset
|
|
560
|
+
while row < window.cursor_y
|
|
561
|
+
visual_rows_before += wrapped_segments_for_line(editor, window, buffer, buffer.line_at(row), width: content_w).length
|
|
562
|
+
row += 1
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
cursor_visual_row = visual_rows_before + cursor_seg_index
|
|
566
|
+
while cursor_visual_row >= height && window.row_offset < window.cursor_y
|
|
567
|
+
dropped = wrapped_segments_for_line(editor, window, buffer, buffer.line_at(window.row_offset), width: content_w).length
|
|
568
|
+
window.row_offset += 1
|
|
569
|
+
cursor_visual_row -= dropped
|
|
570
|
+
end
|
|
571
|
+
rescue StandardError
|
|
572
|
+
nil
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Compute a screen-column-based horizontal scroll offset for rich mode.
|
|
576
|
+
# Unlike normal mode (which stores a char index in window.col_offset),
|
|
577
|
+
# rich mode must scroll by display columns because CJK-padded formatted
|
|
578
|
+
# lines have different character counts for the same display width.
|
|
579
|
+
def ensure_visible_rich(editor, win, buf, rect, content_w)
|
|
580
|
+
state = editor.rich_state
|
|
581
|
+
unless state
|
|
582
|
+
@rich_render_info = nil
|
|
583
|
+
@rich_col_offset_sc = 0
|
|
584
|
+
return
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
format = state[:format]
|
|
588
|
+
delimiter = state[:delimiter]
|
|
589
|
+
renderer = RuVim::RichView.renderer_for(format)
|
|
590
|
+
height = [rect[:height], 1].max
|
|
591
|
+
|
|
592
|
+
raw_lines = height.times.map { |dy|
|
|
593
|
+
row = win.row_offset + dy
|
|
594
|
+
row < buf.line_count ? buf.line_at(row) : nil
|
|
595
|
+
}.compact
|
|
596
|
+
|
|
597
|
+
cursor_raw_line = buf.line_at(win.cursor_y)
|
|
598
|
+
|
|
599
|
+
if renderer && renderer.respond_to?(:cursor_display_col)
|
|
600
|
+
cursor_sc = renderer.cursor_display_col(
|
|
601
|
+
cursor_raw_line, win.cursor_x, visible_lines: raw_lines, delimiter: delimiter
|
|
602
|
+
)
|
|
603
|
+
else
|
|
604
|
+
cursor_sc = RuVim::TextMetrics.screen_col_for_char_index(cursor_raw_line, win.cursor_x)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Use persisted screen-column offset from previous frame
|
|
608
|
+
offset_sc = @rich_col_offset_sc || 0
|
|
609
|
+
|
|
610
|
+
sso = editor.effective_option("sidescrolloff", window: win, buffer: buf).to_i
|
|
611
|
+
sso = [[sso, 0].max, [content_w - 1, 0].max].min
|
|
612
|
+
|
|
613
|
+
if cursor_sc < offset_sc + sso
|
|
614
|
+
offset_sc = [cursor_sc - sso, 0].max
|
|
615
|
+
elsif cursor_sc >= offset_sc + content_w - sso
|
|
616
|
+
offset_sc = cursor_sc - content_w + sso + 1
|
|
617
|
+
end
|
|
618
|
+
offset_sc = [offset_sc, 0].max
|
|
619
|
+
|
|
620
|
+
@rich_col_offset_sc = offset_sc
|
|
621
|
+
|
|
622
|
+
if win == editor.current_window
|
|
623
|
+
@rich_render_info = {
|
|
624
|
+
col_offset_sc: offset_sc,
|
|
625
|
+
cursor_sc: cursor_sc,
|
|
626
|
+
delimiter: delimiter
|
|
627
|
+
}
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
337
631
|
def wrap_enabled?(editor, window, buffer)
|
|
632
|
+
return false if editor.rich_state
|
|
633
|
+
|
|
338
634
|
!!editor.effective_option("wrap", window:, buffer:)
|
|
339
635
|
end
|
|
340
636
|
|
|
@@ -345,15 +641,25 @@ module RuVim
|
|
|
345
641
|
linebreak = !!editor.effective_option("linebreak", window:, buffer:)
|
|
346
642
|
showbreak = editor.effective_option("showbreak", window:, buffer:).to_s
|
|
347
643
|
breakindent = !!editor.effective_option("breakindent", window:, buffer:)
|
|
644
|
+
line = line.to_s
|
|
645
|
+
return [{ source_col_start: 0, display_prefix: "" }] if line.empty?
|
|
646
|
+
|
|
647
|
+
cache_key = [line.object_id, line.length, line.hash, width, tabstop, linebreak, showbreak, breakindent]
|
|
648
|
+
if (cached = @wrapped_segments_cache[cache_key])
|
|
649
|
+
return cached
|
|
650
|
+
end
|
|
651
|
+
|
|
348
652
|
indent_prefix = breakindent ? wrapped_indent_prefix(line, tabstop:, max_width: [width - RuVim::DisplayWidth.display_width(showbreak, tabstop:), 0].max) : ""
|
|
653
|
+
segs = compute_wrapped_segments(line, width:, tabstop:, linebreak:, showbreak:, indent_prefix:)
|
|
654
|
+
@wrapped_segments_cache[cache_key] = segs
|
|
655
|
+
@wrapped_segments_cache.shift while @wrapped_segments_cache.length > WRAP_SEGMENTS_CACHE_LIMIT
|
|
656
|
+
segs
|
|
657
|
+
end
|
|
349
658
|
|
|
659
|
+
def compute_wrapped_segments(line, width:, tabstop:, linebreak:, showbreak:, indent_prefix:)
|
|
350
660
|
segs = []
|
|
351
661
|
start_col = 0
|
|
352
662
|
first = true
|
|
353
|
-
line = line.to_s
|
|
354
|
-
if line.empty?
|
|
355
|
-
return [{ source_col_start: 0, display_prefix: "" }]
|
|
356
|
-
end
|
|
357
663
|
|
|
358
664
|
while start_col < line.length
|
|
359
665
|
display_prefix = first ? "" : "#{showbreak}#{indent_prefix}"
|
|
@@ -361,7 +667,7 @@ module RuVim
|
|
|
361
667
|
avail = [width - prefix_w, 1].max
|
|
362
668
|
cells, = RuVim::TextMetrics.clip_cells_for_width(line[start_col..].to_s, avail, source_col_start: start_col, tabstop:)
|
|
363
669
|
if cells.empty?
|
|
364
|
-
segs << { source_col_start: start_col, display_prefix: display_prefix }
|
|
670
|
+
segs << { source_col_start: start_col, display_prefix: display_prefix }.freeze
|
|
365
671
|
break
|
|
366
672
|
end
|
|
367
673
|
|
|
@@ -372,7 +678,7 @@ module RuVim
|
|
|
372
678
|
end
|
|
373
679
|
end
|
|
374
680
|
|
|
375
|
-
segs << { source_col_start: start_col, display_prefix: display_prefix }
|
|
681
|
+
segs << { source_col_start: start_col, display_prefix: display_prefix }.freeze
|
|
376
682
|
next_start = cells.last.source_col.to_i + 1
|
|
377
683
|
if linebreak
|
|
378
684
|
next_start += 1 while next_start < line.length && line[next_start] == " "
|
|
@@ -383,7 +689,20 @@ module RuVim
|
|
|
383
689
|
first = false
|
|
384
690
|
end
|
|
385
691
|
|
|
386
|
-
segs
|
|
692
|
+
segs.freeze
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def wrapped_segment_index(segs, cursor_x)
|
|
696
|
+
x = cursor_x.to_i
|
|
697
|
+
seg_index = 0
|
|
698
|
+
segs.each_with_index do |seg, i|
|
|
699
|
+
nxt = segs[i + 1]
|
|
700
|
+
if nxt.nil? || x < nxt[:source_col_start]
|
|
701
|
+
seg_index = i
|
|
702
|
+
break
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
seg_index
|
|
387
706
|
end
|
|
388
707
|
|
|
389
708
|
def linebreak_break_index(cells, line)
|
|
@@ -453,7 +772,7 @@ module RuVim
|
|
|
453
772
|
def render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
454
773
|
line = buffer.line_at(buffer_row)
|
|
455
774
|
line = line[window.col_offset..] || ""
|
|
456
|
-
prefix =
|
|
775
|
+
prefix = render_gutter_prefix(editor, window, buffer, buffer_row, gutter_w)
|
|
457
776
|
body = render_text_line(line, editor, buffer_row:, window:, buffer:, width: content_w)
|
|
458
777
|
prefix + body
|
|
459
778
|
end
|
|
@@ -497,6 +816,15 @@ module RuVim
|
|
|
497
816
|
sign + num.rjust([num_width - 1, 0].max) + (num_width.positive? ? " " : "")
|
|
498
817
|
end
|
|
499
818
|
|
|
819
|
+
def render_gutter_prefix(editor, window, buffer, buffer_row, width)
|
|
820
|
+
prefix = line_number_prefix(editor, window, buffer, buffer_row, width)
|
|
821
|
+
return prefix if prefix.empty?
|
|
822
|
+
return prefix if buffer_row.nil?
|
|
823
|
+
|
|
824
|
+
current_line = (buffer_row == window.cursor_y)
|
|
825
|
+
"#{line_number_fg_seq(editor, current_line: current_line)}#{prefix}\e[m"
|
|
826
|
+
end
|
|
827
|
+
|
|
500
828
|
def sign_column_width(editor, window, buffer)
|
|
501
829
|
raw = editor.effective_option("signcolumn", window:, buffer:).to_s
|
|
502
830
|
case raw
|
|
@@ -554,6 +882,14 @@ module RuVim
|
|
|
554
882
|
truecolor_enabled?(editor) ? "\e[48;2;58;58;58m" : "\e[48;5;236m"
|
|
555
883
|
end
|
|
556
884
|
|
|
885
|
+
def line_number_fg_seq(editor, current_line: false)
|
|
886
|
+
if truecolor_enabled?(editor)
|
|
887
|
+
current_line ? "\e[38;2;190;190;190m" : "\e[38;2;120;120;120m"
|
|
888
|
+
else
|
|
889
|
+
current_line ? "\e[37m" : "\e[90m"
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
557
893
|
def truecolor_enabled?(editor)
|
|
558
894
|
!!editor.effective_option("termguicolors")
|
|
559
895
|
rescue StandardError
|
|
@@ -569,18 +905,47 @@ module RuVim
|
|
|
569
905
|
when :visual_char then "-- VISUAL --"
|
|
570
906
|
when :visual_line then "-- VISUAL LINE --"
|
|
571
907
|
when :visual_block then "-- VISUAL BLOCK --"
|
|
908
|
+
when :rich then "-- RICH --"
|
|
572
909
|
else "-- NORMAL --"
|
|
573
910
|
end
|
|
574
911
|
|
|
575
912
|
path = buffer.display_name
|
|
576
913
|
mod = buffer.modified? ? " [+]" : ""
|
|
914
|
+
stream = stream_status_token(buffer)
|
|
915
|
+
loading = file_loading_status_token(buffer)
|
|
916
|
+
tab = tab_status_token(editor)
|
|
577
917
|
msg = editor.message_error? ? "" : editor.message.to_s
|
|
578
|
-
left = "#{mode} #{path}#{mod}"
|
|
579
|
-
right = " #{window.cursor_y + 1}:#{window.cursor_x + 1} "
|
|
918
|
+
left = "#{mode} #{path}#{mod}#{stream}#{loading}"
|
|
919
|
+
right = " #{window.cursor_y + 1}:#{window.cursor_x + 1}#{tab} "
|
|
580
920
|
body_width = [width - right.length, 0].max
|
|
581
921
|
"#{compose_status_body(left, msg, body_width)}#{right}"
|
|
582
922
|
end
|
|
583
923
|
|
|
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
|
+
def file_loading_status_token(buffer)
|
|
933
|
+
return "" unless buffer.respond_to?(:loading_state)
|
|
934
|
+
return "" unless buffer.file_buffer?
|
|
935
|
+
|
|
936
|
+
state = buffer.loading_state
|
|
937
|
+
return "" unless state
|
|
938
|
+
return "" if state.to_sym == :closed
|
|
939
|
+
|
|
940
|
+
" [load/#{state}]"
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def tab_status_token(editor)
|
|
944
|
+
return "" if editor.tabpage_count <= 1
|
|
945
|
+
|
|
946
|
+
" tab:#{editor.current_tabpage_number}/#{editor.tabpage_count}"
|
|
947
|
+
end
|
|
948
|
+
|
|
584
949
|
def compose_status_body(left, msg, width)
|
|
585
950
|
w = [width.to_i, 0].max
|
|
586
951
|
return "" if w.zero?
|
|
@@ -596,7 +961,8 @@ module RuVim
|
|
|
596
961
|
end
|
|
597
962
|
|
|
598
963
|
def truncate(str, width)
|
|
599
|
-
RuVim::TextMetrics.terminal_safe_text(str)
|
|
964
|
+
safe = RuVim::TextMetrics.terminal_safe_text(str)
|
|
965
|
+
RuVim::TextMetrics.pad_plain_to_screen_width(safe, width)
|
|
600
966
|
end
|
|
601
967
|
|
|
602
968
|
def error_message_line(msg, cols)
|
|
@@ -614,6 +980,16 @@ module RuVim
|
|
|
614
980
|
def cursor_screen_position(editor, text_rows, rects)
|
|
615
981
|
window = editor.current_window
|
|
616
982
|
|
|
983
|
+
if editor.hit_enter_active? && editor.hit_enter_lines
|
|
984
|
+
total_rows = text_rows + 2
|
|
985
|
+
msg_count = editor.hit_enter_lines.length
|
|
986
|
+
prompt_row = [total_rows - msg_count, 1].max + msg_count
|
|
987
|
+
prompt_row = [prompt_row, total_rows].min
|
|
988
|
+
prompt_text = "Press ENTER or type command to continue"
|
|
989
|
+
col = [prompt_text.length + 1, total_rows].min
|
|
990
|
+
return [prompt_row, col]
|
|
991
|
+
end
|
|
992
|
+
|
|
617
993
|
if editor.command_line_active?
|
|
618
994
|
row = text_rows + 2
|
|
619
995
|
col = 1 + editor.command_line.prefix.length + editor.command_line.cursor
|
|
@@ -634,14 +1010,7 @@ module RuVim
|
|
|
634
1010
|
row += 1
|
|
635
1011
|
end
|
|
636
1012
|
segs = wrapped_segments_for_line(editor, window, buffer, line, width: content_w)
|
|
637
|
-
seg_index =
|
|
638
|
-
segs.each_with_index do |seg, i|
|
|
639
|
-
nxt = segs[i + 1]
|
|
640
|
-
if nxt.nil? || window.cursor_x < nxt[:source_col_start]
|
|
641
|
-
seg_index = i
|
|
642
|
-
break
|
|
643
|
-
end
|
|
644
|
-
end
|
|
1013
|
+
seg_index = wrapped_segment_index(segs, window.cursor_x)
|
|
645
1014
|
seg = segs[seg_index] || { source_col_start: 0, display_prefix: "" }
|
|
646
1015
|
row = rect[:top] + visual_rows_before + seg_index
|
|
647
1016
|
seg_prefix_w = RuVim::DisplayWidth.display_width(seg[:display_prefix].to_s, tabstop:)
|
|
@@ -649,6 +1018,9 @@ module RuVim
|
|
|
649
1018
|
cursor_sc = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) + extra_virtual
|
|
650
1019
|
seg_sc = RuVim::TextMetrics.screen_col_for_char_index(line, seg[:source_col_start], tabstop:)
|
|
651
1020
|
col = rect[:left] + gutter_w + seg_prefix_w + [cursor_sc - seg_sc, 0].max
|
|
1021
|
+
elsif @rich_render_info
|
|
1022
|
+
row = rect[:top] + (window.cursor_y - window.row_offset)
|
|
1023
|
+
col = rect[:left] + gutter_w + [@rich_render_info[:cursor_sc] - @rich_render_info[:col_offset_sc], 0].max
|
|
652
1024
|
else
|
|
653
1025
|
row = rect[:top] + (window.cursor_y - window.row_offset)
|
|
654
1026
|
extra_virtual = [window.cursor_x - line.length, 0].max
|
|
@@ -666,39 +1038,64 @@ module RuVim
|
|
|
666
1038
|
end
|
|
667
1039
|
|
|
668
1040
|
def window_rects(editor, text_rows:, text_cols:)
|
|
1041
|
+
tree = editor.layout_tree
|
|
1042
|
+
return {} if tree.nil?
|
|
669
1043
|
ids = editor.window_order
|
|
670
1044
|
return {} if ids.empty?
|
|
671
|
-
return { ids.first => { top: 1, left: 1, height: text_rows, width: text_cols } } if ids.length == 1
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1045
|
+
return { ids.first => { top: 1, left: 1, height: text_rows, width: text_cols } } if ids.length == 1
|
|
1046
|
+
|
|
1047
|
+
compute_tree_rects(tree, top: 1, left: 1, height: text_rows, width: text_cols)
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
def compute_tree_rects(node, top:, left:, height:, width:)
|
|
1051
|
+
if node[:type] == :window
|
|
1052
|
+
return { node[:id] => { top: top, left: left, height: height, width: width } }
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
children = node[:children]
|
|
1056
|
+
n = children.length
|
|
1057
|
+
rects = {}
|
|
1058
|
+
|
|
1059
|
+
case node[:type]
|
|
1060
|
+
when :vsplit
|
|
1061
|
+
sep_count = n - 1
|
|
1062
|
+
usable = [width - sep_count, n].max
|
|
1063
|
+
widths = split_sizes(usable, n)
|
|
1064
|
+
cur_left = left
|
|
1065
|
+
children.each_with_index do |child, i|
|
|
680
1066
|
w = widths[i]
|
|
681
|
-
|
|
682
|
-
|
|
1067
|
+
child_rects = compute_tree_rects(child, top: top, left: cur_left, height: height, width: w)
|
|
1068
|
+
child_rects.each_value { |r| r[:separator] = :vertical }
|
|
1069
|
+
rects.merge!(child_rects)
|
|
1070
|
+
cur_left += w + 1
|
|
683
1071
|
end
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
rects = {}
|
|
691
|
-
ids.each_with_index do |id, i|
|
|
1072
|
+
when :hsplit
|
|
1073
|
+
sep_count = n - 1
|
|
1074
|
+
usable = [height - sep_count, n].max
|
|
1075
|
+
heights = split_sizes(usable, n)
|
|
1076
|
+
cur_top = top
|
|
1077
|
+
children.each_with_index do |child, i|
|
|
692
1078
|
h = heights[i]
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1079
|
+
child_rects = compute_tree_rects(child, top: cur_top, left: left, height: h, width: width)
|
|
1080
|
+
child_rects.each_value { |r| r[:separator] = :horizontal }
|
|
1081
|
+
rects.merge!(child_rects)
|
|
1082
|
+
if i < n - 1
|
|
1083
|
+
# Mark separator row for the last window in this child
|
|
1084
|
+
child_leaves = tree_leaves_for_rects(child)
|
|
1085
|
+
last_leaf = child_leaves.last
|
|
1086
|
+
rects[last_leaf][:sep_row] = cur_top + h if last_leaf && rects[last_leaf]
|
|
698
1087
|
end
|
|
1088
|
+
cur_top += h + 1
|
|
699
1089
|
end
|
|
700
|
-
rects
|
|
701
1090
|
end
|
|
1091
|
+
|
|
1092
|
+
rects
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
def tree_leaves_for_rects(node)
|
|
1096
|
+
return [node[:id]] if node[:type] == :window
|
|
1097
|
+
|
|
1098
|
+
node[:children].flat_map { |c| tree_leaves_for_rects(c) }
|
|
702
1099
|
end
|
|
703
1100
|
|
|
704
1101
|
def split_sizes(total, n)
|