ruvim 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +29 -0
- data/docs/command.md +101 -0
- data/docs/config.md +203 -84
- data/docs/done.md +21 -0
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +195 -33
- data/docs/todo.md +183 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +94 -171
- data/lib/ruvim/app.rb +1543 -172
- 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 +21 -5
- data/lib/ruvim/context.rb +2 -7
- data/lib/ruvim/dispatcher.rb +153 -13
- data/lib/ruvim/display_width.rb +28 -2
- data/lib/ruvim/editor.rb +622 -69
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +1386 -114
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +52 -29
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +48 -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 +851 -119
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +28 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +37 -10
- data/lib/ruvim.rb +15 -0
- data/test/app_completion_test.rb +174 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +110 -2
- data/test/app_scenario_test.rb +998 -0
- data/test/app_startup_test.rb +197 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +438 -0
- data/test/display_width_test.rb +18 -0
- data/test/editor_register_test.rb +23 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +65 -14
- 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 +470 -0
- data/test/window_test.rb +26 -0
- metadata +37 -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,24 +17,48 @@ 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
|
|
19
25
|
text_cols = [text_cols, 1].max
|
|
20
26
|
|
|
21
27
|
rects = window_rects(editor, text_rows:, text_cols:)
|
|
28
|
+
if (current_rect = rects[editor.current_window_id])
|
|
29
|
+
editor.current_window_view_height_hint = [current_rect[:height].to_i, 1].max
|
|
30
|
+
end
|
|
22
31
|
editor.window_order.each do |win_id|
|
|
23
32
|
win = editor.windows.fetch(win_id)
|
|
24
33
|
buf = editor.buffers.fetch(win.buffer_id)
|
|
25
34
|
rect = rects[win_id]
|
|
26
35
|
next unless rect
|
|
27
36
|
content_width = [rect[:width] - number_column_width(editor, win, buf), 1].max
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
62
|
end
|
|
35
63
|
|
|
36
64
|
frame = build_frame(editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
@@ -52,7 +80,9 @@ module RuVim
|
|
|
52
80
|
text_rows = [text_rows, 1].max
|
|
53
81
|
text_cols = [text_cols, 1].max
|
|
54
82
|
rect = window_rects(editor, text_rows:, text_cols:)[editor.current_window_id]
|
|
55
|
-
[rect ? rect[:height].to_i : text_rows, 1].max
|
|
83
|
+
height = [rect ? rect[:height].to_i : text_rows, 1].max
|
|
84
|
+
editor.current_window_view_height_hint = height if editor.respond_to?(:current_window_view_height_hint=)
|
|
85
|
+
height
|
|
56
86
|
rescue StandardError
|
|
57
87
|
1
|
|
58
88
|
end
|
|
@@ -63,14 +93,19 @@ module RuVim
|
|
|
63
93
|
lines = {}
|
|
64
94
|
render_window_area(editor, lines, rects, text_rows:, text_cols:)
|
|
65
95
|
|
|
66
|
-
|
|
67
|
-
|
|
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] = ""
|
|
68
102
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
74
109
|
end
|
|
75
110
|
|
|
76
111
|
{
|
|
@@ -81,70 +116,128 @@ module RuVim
|
|
|
81
116
|
}
|
|
82
117
|
end
|
|
83
118
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)
|
|
89
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"
|
|
90
135
|
end
|
|
91
136
|
|
|
92
|
-
def
|
|
93
|
-
|
|
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
|
|
94
140
|
|
|
141
|
+
def render_tree_windows(editor, lines, rects, text_rows:, text_cols:)
|
|
142
|
+
# Pre-render each window's rows
|
|
143
|
+
window_rows_cache = {}
|
|
95
144
|
editor.window_order.each do |win_id|
|
|
96
145
|
rect = rects[win_id]
|
|
97
146
|
next unless rect
|
|
147
|
+
window = editor.windows.fetch(win_id)
|
|
148
|
+
buffer = editor.buffers.fetch(window.buffer_id)
|
|
149
|
+
gutter_w = number_column_width(editor, window, buffer)
|
|
150
|
+
content_w = [rect[:width] - gutter_w, 1].max
|
|
151
|
+
window_rows_cache[win_id] = window_render_rows(editor, window, buffer, height: rect[:height], gutter_w:, content_w:)
|
|
152
|
+
end
|
|
98
153
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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)
|
|
156
|
+
|
|
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
|
+
|
|
164
|
+
pieces = +""
|
|
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]
|
|
113
179
|
end
|
|
180
|
+
end
|
|
181
|
+
lines[row_no] = pieces
|
|
114
182
|
end
|
|
183
|
+
end
|
|
115
184
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
120
191
|
end
|
|
121
192
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 }
|
|
143
223
|
end
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
240
|
end
|
|
147
|
-
lines[row_no] = pieces
|
|
148
241
|
end
|
|
149
242
|
end
|
|
150
243
|
|
|
@@ -184,43 +277,502 @@ module RuVim
|
|
|
184
277
|
def render_text_line(text, editor, buffer_row:, window:, buffer:, width:)
|
|
185
278
|
tabstop = tabstop_for(editor, window, buffer)
|
|
186
279
|
cells, display_col = RuVim::TextMetrics.clip_cells_for_width(text, width, source_col_start: window.col_offset, tabstop:)
|
|
280
|
+
render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width:, source_line: buffer.line_at(buffer_row),
|
|
281
|
+
source_col_offset: window.col_offset, leading_display_prefix: "")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def render_text_segment(source_line, editor, buffer_row:, window:, buffer:, width:, source_col_start:, display_prefix: "")
|
|
285
|
+
tabstop = tabstop_for(editor, window, buffer)
|
|
286
|
+
prefix = display_prefix.to_s
|
|
287
|
+
prefix_w = RuVim::DisplayWidth.display_width(prefix, tabstop:)
|
|
288
|
+
avail = [width - prefix_w, 0].max
|
|
289
|
+
cells, display_col = RuVim::TextMetrics.clip_cells_for_width(source_line[source_col_start..].to_s, avail, source_col_start:, tabstop:)
|
|
290
|
+
body = render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width: avail, source_line: source_line,
|
|
291
|
+
source_col_offset: source_col_start, leading_display_prefix: prefix)
|
|
292
|
+
if width <= 0
|
|
293
|
+
""
|
|
294
|
+
elsif prefix_w <= 0
|
|
295
|
+
body
|
|
296
|
+
else
|
|
297
|
+
prefix_render = RuVim::TextMetrics.pad_plain_to_screen_width(prefix, [width, 0].max, tabstop:)[0...prefix.length].to_s
|
|
298
|
+
# body already includes padding for avail; prepend the visible prefix and trim to width.
|
|
299
|
+
out = prefix_render + body
|
|
300
|
+
out
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width:, source_line:, source_col_offset:, leading_display_prefix:)
|
|
187
305
|
highlighted = +""
|
|
306
|
+
tabstop = tabstop_for(editor, window, buffer)
|
|
188
307
|
visual = (editor.current_window_id == window.id && editor.visual_active?) ? editor.visual_selection(window) : nil
|
|
189
|
-
|
|
190
|
-
|
|
308
|
+
text_for_highlight = source_line[source_col_offset..].to_s
|
|
309
|
+
search_cols = search_highlight_source_cols(editor, text_for_highlight, source_col_offset: source_col_offset)
|
|
310
|
+
syntax_cols = syntax_highlight_source_cols(editor, window, buffer, text_for_highlight, source_col_offset: source_col_offset)
|
|
311
|
+
list_enabled = !!editor.effective_option("list", window:, buffer:)
|
|
312
|
+
listchars = parse_listchars(editor.effective_option("listchars", window:, buffer:))
|
|
313
|
+
tab_seen = {}
|
|
314
|
+
trail_from = source_line.rstrip.length
|
|
315
|
+
cursorline = !!editor.effective_option("cursorline", window:, buffer:)
|
|
316
|
+
current_line = (editor.current_window_id == window.id && window.cursor_y == buffer_row)
|
|
317
|
+
cursorline_enabled = cursorline && current_line
|
|
318
|
+
colorcolumns = colorcolumn_display_cols(editor, window, buffer)
|
|
319
|
+
leading_prefix_width = RuVim::DisplayWidth.display_width(leading_display_prefix.to_s, tabstop:)
|
|
320
|
+
display_pos = leading_prefix_width
|
|
191
321
|
|
|
192
|
-
cells.
|
|
193
|
-
ch = cell
|
|
322
|
+
cells.each do |cell|
|
|
323
|
+
ch = display_glyph_for_cell(cell, source_line, list_enabled:, listchars:, tab_seen:, trail_from:)
|
|
194
324
|
buffer_col = cell.source_col
|
|
195
325
|
selected = selected_in_visual?(visual, buffer_row, buffer_col)
|
|
196
326
|
cursor_here = (editor.current_window_id == window.id && window.cursor_y == buffer_row && window.cursor_x == buffer_col)
|
|
197
|
-
|
|
327
|
+
colorcolumn_here = colorcolumns[display_pos]
|
|
328
|
+
if cursor_here
|
|
329
|
+
highlighted << cursor_cell_render(editor, ch)
|
|
330
|
+
elsif selected
|
|
198
331
|
highlighted << "\e[7m#{ch}\e[m"
|
|
199
332
|
elsif search_cols[buffer_col]
|
|
200
|
-
highlighted << "
|
|
333
|
+
highlighted << "#{search_bg_seq(editor)}#{ch}\e[m"
|
|
334
|
+
elsif colorcolumn_here
|
|
335
|
+
highlighted << "#{colorcolumn_bg_seq(editor)}#{ch}\e[m"
|
|
336
|
+
elsif cursorline_enabled
|
|
337
|
+
highlighted << "#{cursorline_bg_seq(editor)}#{ch}\e[m"
|
|
201
338
|
elsif (syntax_color = syntax_cols[buffer_col])
|
|
202
339
|
highlighted << "#{syntax_color}#{ch}\e[m"
|
|
203
340
|
else
|
|
204
341
|
highlighted << ch
|
|
205
342
|
end
|
|
343
|
+
display_pos += [cell.display_width.to_i, 1].max
|
|
206
344
|
end
|
|
207
345
|
|
|
208
346
|
if editor.current_window_id == window.id && window.cursor_y == buffer_row
|
|
209
|
-
|
|
210
|
-
if
|
|
211
|
-
|
|
347
|
+
cursor_target = virtual_cursor_display_pos(source_line, window.cursor_x, source_col_offset:, tabstop:, leading_prefix_width:)
|
|
348
|
+
if cursor_target && cursor_target >= display_pos && cursor_target < width
|
|
349
|
+
gap = cursor_target - display_pos
|
|
350
|
+
if gap.positive?
|
|
351
|
+
highlighted << (" " * gap)
|
|
352
|
+
display_col += gap
|
|
353
|
+
display_pos += gap
|
|
354
|
+
end
|
|
355
|
+
highlighted << cursor_cell_render(editor, " ")
|
|
212
356
|
display_col += 1
|
|
357
|
+
display_pos += 1
|
|
213
358
|
end
|
|
214
359
|
end
|
|
215
360
|
|
|
216
|
-
|
|
361
|
+
trailing = [width - display_col, 0].max
|
|
362
|
+
if trailing.positive? && cursorline_enabled
|
|
363
|
+
trailing.times do
|
|
364
|
+
if colorcolumns[display_pos]
|
|
365
|
+
highlighted << "#{colorcolumn_bg_seq(editor)} \e[m"
|
|
366
|
+
else
|
|
367
|
+
highlighted << "#{cursorline_bg_seq(editor)} \e[m"
|
|
368
|
+
end
|
|
369
|
+
display_pos += 1
|
|
370
|
+
end
|
|
371
|
+
else
|
|
372
|
+
highlighted << (" " * trailing)
|
|
373
|
+
end
|
|
217
374
|
highlighted
|
|
218
375
|
end
|
|
219
376
|
|
|
377
|
+
def virtual_cursor_display_pos(source_line, cursor_x, source_col_offset:, tabstop:, leading_prefix_width:)
|
|
378
|
+
return nil if cursor_x < source_col_offset
|
|
379
|
+
|
|
380
|
+
base = RuVim::TextMetrics.screen_col_for_char_index(source_line, cursor_x, tabstop:) -
|
|
381
|
+
RuVim::TextMetrics.screen_col_for_char_index(source_line, source_col_offset, tabstop:)
|
|
382
|
+
extra = [cursor_x.to_i - source_line.to_s.length, 0].max
|
|
383
|
+
leading_prefix_width + [base, 0].max + extra
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
387
|
+
return plain_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:) unless wrap_enabled?(editor, window, buffer)
|
|
388
|
+
|
|
389
|
+
wrapped_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
390
|
+
end
|
|
391
|
+
|
|
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
|
+
|
|
397
|
+
Array.new(height) do |dy|
|
|
398
|
+
buffer_row = window.row_offset + dy
|
|
399
|
+
if buffer_row < buffer.line_count
|
|
400
|
+
render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
401
|
+
else
|
|
402
|
+
render_gutter_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
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
|
+
|
|
525
|
+
def wrapped_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
|
|
526
|
+
rows = []
|
|
527
|
+
row_idx = window.row_offset
|
|
528
|
+
while rows.length < height
|
|
529
|
+
if row_idx >= buffer.line_count
|
|
530
|
+
rows << (render_gutter_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w))
|
|
531
|
+
next
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
line = buffer.line_at(row_idx)
|
|
535
|
+
segments = wrapped_segments_for_line(editor, window, buffer, line, width: content_w)
|
|
536
|
+
segments.each_with_index do |seg, seg_i|
|
|
537
|
+
break if rows.length >= height
|
|
538
|
+
|
|
539
|
+
gutter = render_gutter_prefix(editor, window, buffer, seg_i.zero? ? row_idx : nil, gutter_w)
|
|
540
|
+
rows << gutter + render_text_segment(line, editor, buffer_row: row_idx, window:, buffer:, width: content_w,
|
|
541
|
+
source_col_start: seg[:source_col_start], display_prefix: seg[:display_prefix])
|
|
542
|
+
end
|
|
543
|
+
row_idx += 1
|
|
544
|
+
end
|
|
545
|
+
rows
|
|
546
|
+
end
|
|
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
|
+
|
|
631
|
+
def wrap_enabled?(editor, window, buffer)
|
|
632
|
+
return false if editor.rich_state
|
|
633
|
+
|
|
634
|
+
!!editor.effective_option("wrap", window:, buffer:)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def wrapped_segments_for_line(editor, window, buffer, line, width:)
|
|
638
|
+
return [{ source_col_start: 0, display_prefix: "" }] if width <= 0
|
|
639
|
+
|
|
640
|
+
tabstop = tabstop_for(editor, window, buffer)
|
|
641
|
+
linebreak = !!editor.effective_option("linebreak", window:, buffer:)
|
|
642
|
+
showbreak = editor.effective_option("showbreak", window:, buffer:).to_s
|
|
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
|
+
|
|
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
|
|
658
|
+
|
|
659
|
+
def compute_wrapped_segments(line, width:, tabstop:, linebreak:, showbreak:, indent_prefix:)
|
|
660
|
+
segs = []
|
|
661
|
+
start_col = 0
|
|
662
|
+
first = true
|
|
663
|
+
|
|
664
|
+
while start_col < line.length
|
|
665
|
+
display_prefix = first ? "" : "#{showbreak}#{indent_prefix}"
|
|
666
|
+
prefix_w = RuVim::DisplayWidth.display_width(display_prefix, tabstop:)
|
|
667
|
+
avail = [width - prefix_w, 1].max
|
|
668
|
+
cells, = RuVim::TextMetrics.clip_cells_for_width(line[start_col..].to_s, avail, source_col_start: start_col, tabstop:)
|
|
669
|
+
if cells.empty?
|
|
670
|
+
segs << { source_col_start: start_col, display_prefix: display_prefix }.freeze
|
|
671
|
+
break
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
if linebreak && cells.length > 1
|
|
675
|
+
break_idx = linebreak_break_index(cells, line)
|
|
676
|
+
if break_idx && break_idx < cells.length - 1
|
|
677
|
+
cells = cells[0..break_idx]
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
segs << { source_col_start: start_col, display_prefix: display_prefix }.freeze
|
|
682
|
+
next_start = cells.last.source_col.to_i + 1
|
|
683
|
+
if linebreak
|
|
684
|
+
next_start += 1 while next_start < line.length && line[next_start] == " "
|
|
685
|
+
end
|
|
686
|
+
break if next_start <= start_col
|
|
687
|
+
|
|
688
|
+
start_col = next_start
|
|
689
|
+
first = false
|
|
690
|
+
end
|
|
691
|
+
|
|
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
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def linebreak_break_index(cells, line)
|
|
709
|
+
idx = nil
|
|
710
|
+
cells.each_with_index do |cell, i|
|
|
711
|
+
ch = line[cell.source_col]
|
|
712
|
+
idx = i if ch =~ /\s/
|
|
713
|
+
end
|
|
714
|
+
idx
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def wrapped_indent_prefix(line, tabstop:, max_width:)
|
|
718
|
+
indent = line.to_s[/\A[ \t]*/].to_s
|
|
719
|
+
return "" if indent.empty? || max_width <= 0
|
|
720
|
+
|
|
721
|
+
RuVim::TextMetrics.pad_plain_to_screen_width(indent, max_width, tabstop:)[0...indent.length].to_s
|
|
722
|
+
rescue StandardError
|
|
723
|
+
""
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def display_glyph_for_cell(cell, source_line, list_enabled:, listchars:, tab_seen:, trail_from:)
|
|
727
|
+
return cell.glyph unless list_enabled
|
|
728
|
+
|
|
729
|
+
src = source_line[cell.source_col]
|
|
730
|
+
case src
|
|
731
|
+
when "\t"
|
|
732
|
+
first = !tab_seen[cell.source_col]
|
|
733
|
+
tab_seen[cell.source_col] = true
|
|
734
|
+
first ? listchars[:tab_head] : listchars[:tab_fill]
|
|
735
|
+
when " "
|
|
736
|
+
cell.source_col >= trail_from ? listchars[:trail] : cell.glyph
|
|
737
|
+
when "\u00A0"
|
|
738
|
+
listchars[:nbsp]
|
|
739
|
+
else
|
|
740
|
+
cell.glyph
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
def parse_listchars(raw)
|
|
745
|
+
raw_key = raw.to_s
|
|
746
|
+
@listchars_cache ||= {}
|
|
747
|
+
return @listchars_cache[raw_key] if @listchars_cache.key?(raw_key)
|
|
748
|
+
|
|
749
|
+
cfg = { tab_head: ">", tab_fill: "-", trail: "-", nbsp: "+" }
|
|
750
|
+
raw_key.split(",").each do |entry|
|
|
751
|
+
entry_key, val = entry.split(":", 2)
|
|
752
|
+
next unless entry_key && val
|
|
753
|
+
|
|
754
|
+
case entry_key.strip
|
|
755
|
+
when "tab"
|
|
756
|
+
chars = val.to_s.each_char.to_a
|
|
757
|
+
cfg[:tab_head] = chars[0] if chars[0]
|
|
758
|
+
cfg[:tab_fill] = chars[1] if chars[1]
|
|
759
|
+
when "trail"
|
|
760
|
+
ch = val.to_s.each_char.first
|
|
761
|
+
cfg[:trail] = ch if ch
|
|
762
|
+
when "nbsp"
|
|
763
|
+
ch = val.to_s.each_char.first
|
|
764
|
+
cfg[:nbsp] = ch if ch
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
@listchars_cache[raw_key] = cfg.freeze
|
|
768
|
+
rescue StandardError
|
|
769
|
+
{ tab_head: ">", tab_fill: "-", trail: "-", nbsp: "+" }
|
|
770
|
+
end
|
|
771
|
+
|
|
220
772
|
def render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
|
|
221
773
|
line = buffer.line_at(buffer_row)
|
|
222
774
|
line = line[window.col_offset..] || ""
|
|
223
|
-
prefix =
|
|
775
|
+
prefix = render_gutter_prefix(editor, window, buffer, buffer_row, gutter_w)
|
|
224
776
|
body = render_text_line(line, editor, buffer_row:, window:, buffer:, width: content_w)
|
|
225
777
|
prefix + body
|
|
226
778
|
end
|
|
@@ -234,18 +786,24 @@ module RuVim
|
|
|
234
786
|
end
|
|
235
787
|
|
|
236
788
|
def number_column_width(editor, window, buffer)
|
|
789
|
+
sign_w = sign_column_width(editor, window, buffer)
|
|
237
790
|
enabled = editor.effective_option("number", window:, buffer:) || editor.effective_option("relativenumber", window:, buffer:)
|
|
238
|
-
return
|
|
791
|
+
return sign_w unless enabled
|
|
239
792
|
|
|
240
|
-
[buffer.line_count.to_s.length, 1].max
|
|
793
|
+
base = [buffer.line_count.to_s.length, 1].max
|
|
794
|
+
minw = editor.effective_option("numberwidth", window:, buffer:).to_i
|
|
795
|
+
sign_w + ([[base, minw].max, 1].max + 1)
|
|
241
796
|
end
|
|
242
797
|
|
|
243
798
|
def line_number_prefix(editor, window, buffer, buffer_row, width)
|
|
244
799
|
return "" if width <= 0
|
|
800
|
+
sign_w = sign_column_width(editor, window, buffer)
|
|
801
|
+
sign = " " * sign_w
|
|
802
|
+
num_width = [width - sign_w, 0].max
|
|
245
803
|
show_abs = editor.effective_option("number", window:, buffer:)
|
|
246
804
|
show_rel = editor.effective_option("relativenumber", window:, buffer:)
|
|
247
|
-
return " " *
|
|
248
|
-
return " " *
|
|
805
|
+
return sign + (" " * num_width) unless show_abs || show_rel
|
|
806
|
+
return sign + (" " * num_width) if buffer_row.nil?
|
|
249
807
|
|
|
250
808
|
num =
|
|
251
809
|
if show_rel && buffer_row != window.cursor_y
|
|
@@ -255,13 +813,89 @@ module RuVim
|
|
|
255
813
|
else
|
|
256
814
|
"0"
|
|
257
815
|
end
|
|
258
|
-
num.rjust(
|
|
816
|
+
sign + num.rjust([num_width - 1, 0].max) + (num_width.positive? ? " " : "")
|
|
817
|
+
end
|
|
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
|
+
|
|
828
|
+
def sign_column_width(editor, window, buffer)
|
|
829
|
+
raw = editor.effective_option("signcolumn", window:, buffer:).to_s
|
|
830
|
+
case raw
|
|
831
|
+
when "", "auto", "number"
|
|
832
|
+
0
|
|
833
|
+
when "no"
|
|
834
|
+
0
|
|
835
|
+
else
|
|
836
|
+
if (m = /\Ayes(?::(\d+))?\z/.match(raw))
|
|
837
|
+
n = m[1].to_i
|
|
838
|
+
n = 1 if n <= 0
|
|
839
|
+
n
|
|
840
|
+
else
|
|
841
|
+
1
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
rescue StandardError
|
|
845
|
+
0
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def colorcolumn_display_cols(editor, window, buffer)
|
|
849
|
+
raw = editor.effective_option("colorcolumn", window:, buffer:).to_s
|
|
850
|
+
return {} if raw.empty?
|
|
851
|
+
|
|
852
|
+
@colorcolumn_cache ||= {}
|
|
853
|
+
return @colorcolumn_cache[raw] if @colorcolumn_cache.key?(raw)
|
|
854
|
+
|
|
855
|
+
cols = {}
|
|
856
|
+
raw.split(",").each do |tok|
|
|
857
|
+
t = tok.strip
|
|
858
|
+
next if t.empty?
|
|
859
|
+
next unless t.match?(/\A\d+\z/)
|
|
860
|
+
n = t.to_i
|
|
861
|
+
next if n <= 0
|
|
862
|
+
cols[n - 1] = true
|
|
863
|
+
end
|
|
864
|
+
@colorcolumn_cache[raw] = cols.freeze
|
|
865
|
+
rescue StandardError
|
|
866
|
+
{}
|
|
259
867
|
end
|
|
260
868
|
|
|
261
869
|
def pad_plain_display(text, width)
|
|
262
870
|
RuVim::TextMetrics.pad_plain_to_screen_width(text, width, tabstop: DEFAULT_TABSTOP)
|
|
263
871
|
end
|
|
264
872
|
|
|
873
|
+
def search_bg_seq(editor)
|
|
874
|
+
truecolor_enabled?(editor) ? "\e[48;2;255;215;0m" : "\e[43m"
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
def colorcolumn_bg_seq(editor)
|
|
878
|
+
truecolor_enabled?(editor) ? "\e[48;2;72;72;72m" : "\e[48;5;238m"
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def cursorline_bg_seq(editor)
|
|
882
|
+
truecolor_enabled?(editor) ? "\e[48;2;58;58;58m" : "\e[48;5;236m"
|
|
883
|
+
end
|
|
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
|
+
|
|
893
|
+
def truecolor_enabled?(editor)
|
|
894
|
+
!!editor.effective_option("termguicolors")
|
|
895
|
+
rescue StandardError
|
|
896
|
+
false
|
|
897
|
+
end
|
|
898
|
+
|
|
265
899
|
def status_line(editor, width)
|
|
266
900
|
buffer = editor.current_buffer
|
|
267
901
|
window = editor.current_window
|
|
@@ -271,23 +905,47 @@ module RuVim
|
|
|
271
905
|
when :visual_char then "-- VISUAL --"
|
|
272
906
|
when :visual_line then "-- VISUAL LINE --"
|
|
273
907
|
when :visual_block then "-- VISUAL BLOCK --"
|
|
908
|
+
when :rich then "-- RICH --"
|
|
274
909
|
else "-- NORMAL --"
|
|
275
910
|
end
|
|
276
911
|
|
|
277
912
|
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
913
|
mod = buffer.modified? ? " [+]" : ""
|
|
914
|
+
stream = stream_status_token(buffer)
|
|
915
|
+
loading = file_loading_status_token(buffer)
|
|
916
|
+
tab = tab_status_token(editor)
|
|
281
917
|
msg = editor.message_error? ? "" : editor.message.to_s
|
|
282
|
-
|
|
283
|
-
|
|
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} "
|
|
918
|
+
left = "#{mode} #{path}#{mod}#{stream}#{loading}"
|
|
919
|
+
right = " #{window.cursor_y + 1}:#{window.cursor_x + 1}#{tab} "
|
|
287
920
|
body_width = [width - right.length, 0].max
|
|
288
921
|
"#{compose_status_body(left, msg, body_width)}#{right}"
|
|
289
922
|
end
|
|
290
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
|
+
|
|
291
949
|
def compose_status_body(left, msg, width)
|
|
292
950
|
w = [width.to_i, 0].max
|
|
293
951
|
return "" if w.zero?
|
|
@@ -303,16 +961,35 @@ module RuVim
|
|
|
303
961
|
end
|
|
304
962
|
|
|
305
963
|
def truncate(str, width)
|
|
306
|
-
|
|
964
|
+
safe = RuVim::TextMetrics.terminal_safe_text(str)
|
|
965
|
+
RuVim::TextMetrics.pad_plain_to_screen_width(safe, width)
|
|
307
966
|
end
|
|
308
967
|
|
|
309
968
|
def error_message_line(msg, cols)
|
|
310
969
|
"\e[97;41m#{truncate(msg, cols)}\e[m"
|
|
311
970
|
end
|
|
312
971
|
|
|
972
|
+
def cursor_cell_render(editor, ch)
|
|
973
|
+
"#{cursor_cell_seq(editor)}#{ch}\e[m"
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def cursor_cell_seq(editor)
|
|
977
|
+
"\e[7m"
|
|
978
|
+
end
|
|
979
|
+
|
|
313
980
|
def cursor_screen_position(editor, text_rows, rects)
|
|
314
981
|
window = editor.current_window
|
|
315
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
|
+
|
|
316
993
|
if editor.command_line_active?
|
|
317
994
|
row = text_rows + 2
|
|
318
995
|
col = 1 + editor.command_line.prefix.length + editor.command_line.cursor
|
|
@@ -320,50 +997,105 @@ module RuVim
|
|
|
320
997
|
end
|
|
321
998
|
|
|
322
999
|
rect = rects[window.id] || { top: 1, left: 1 }
|
|
323
|
-
|
|
324
|
-
line =
|
|
325
|
-
gutter_w = number_column_width(editor, window,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1000
|
+
buffer = editor.current_buffer
|
|
1001
|
+
line = buffer.line_at(window.cursor_y)
|
|
1002
|
+
gutter_w = number_column_width(editor, window, buffer)
|
|
1003
|
+
content_w = [rect[:width] - gutter_w, 1].max
|
|
1004
|
+
tabstop = tabstop_for(editor, window, buffer)
|
|
1005
|
+
if wrap_enabled?(editor, window, buffer)
|
|
1006
|
+
visual_rows_before = 0
|
|
1007
|
+
row = window.row_offset
|
|
1008
|
+
while row < window.cursor_y
|
|
1009
|
+
visual_rows_before += wrapped_segments_for_line(editor, window, buffer, buffer.line_at(row), width: content_w).length
|
|
1010
|
+
row += 1
|
|
1011
|
+
end
|
|
1012
|
+
segs = wrapped_segments_for_line(editor, window, buffer, line, width: content_w)
|
|
1013
|
+
seg_index = wrapped_segment_index(segs, window.cursor_x)
|
|
1014
|
+
seg = segs[seg_index] || { source_col_start: 0, display_prefix: "" }
|
|
1015
|
+
row = rect[:top] + visual_rows_before + seg_index
|
|
1016
|
+
seg_prefix_w = RuVim::DisplayWidth.display_width(seg[:display_prefix].to_s, tabstop:)
|
|
1017
|
+
extra_virtual = [window.cursor_x - line.length, 0].max
|
|
1018
|
+
cursor_sc = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) + extra_virtual
|
|
1019
|
+
seg_sc = RuVim::TextMetrics.screen_col_for_char_index(line, seg[:source_col_start], tabstop:)
|
|
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
|
|
1024
|
+
else
|
|
1025
|
+
row = rect[:top] + (window.cursor_y - window.row_offset)
|
|
1026
|
+
extra_virtual = [window.cursor_x - line.length, 0].max
|
|
1027
|
+
prefix_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) -
|
|
1028
|
+
RuVim::TextMetrics.screen_col_for_char_index(line, window.col_offset, tabstop:)
|
|
1029
|
+
col = rect[:left] + gutter_w + [prefix_screen_col, 0].max + extra_virtual
|
|
1030
|
+
end
|
|
1031
|
+
min_row = [rect[:top].to_i, 1].max
|
|
1032
|
+
max_row = [rect[:top].to_i + [rect[:height].to_i, 1].max - 1, min_row].max
|
|
1033
|
+
min_col = [rect[:left].to_i, 1].max
|
|
1034
|
+
max_col = [rect[:left].to_i + [rect[:width].to_i, 1].max - 1, min_col].max
|
|
1035
|
+
row = [[row.to_i, min_row].max, max_row].min
|
|
1036
|
+
col = [[col.to_i, min_col].max, max_col].min
|
|
330
1037
|
[row, col]
|
|
331
1038
|
end
|
|
332
1039
|
|
|
333
1040
|
def window_rects(editor, text_rows:, text_cols:)
|
|
1041
|
+
tree = editor.layout_tree
|
|
1042
|
+
return {} if tree.nil?
|
|
334
1043
|
ids = editor.window_order
|
|
335
1044
|
return {} if ids.empty?
|
|
336
|
-
return { ids.first => { top: 1, left: 1, height: text_rows, width: text_cols } } if ids.length == 1
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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|
|
|
345
1066
|
w = widths[i]
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
348
1071
|
end
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
rects = {}
|
|
356
|
-
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|
|
|
357
1078
|
h = heights[i]
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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]
|
|
363
1087
|
end
|
|
1088
|
+
cur_top += h + 1
|
|
364
1089
|
end
|
|
365
|
-
rects
|
|
366
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) }
|
|
367
1099
|
end
|
|
368
1100
|
|
|
369
1101
|
def split_sizes(total, n)
|