ruvim 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +68 -7
- data/README.md +30 -7
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +18 -1
- data/docs/command.md +156 -10
- data/docs/config.md +10 -2
- data/docs/done.md +23 -0
- data/docs/spec.md +162 -25
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +33 -1
- data/docs/vim_diff.md +31 -8
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +246 -2525
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +43 -20
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +74 -80
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +255 -0
- data/lib/ruvim/git/branch.rb +112 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +125 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +351 -77
- data/lib/ruvim/highlighter.rb +4 -11
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +43 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +40 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +30 -7
- data/lib/ruvim/screen.rb +135 -84
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +31 -4
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +65 -16
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +182 -8
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +77 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +134 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/display_width_test.rb +41 -0
- data/test/ex_command_registry_test.rb +106 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +198 -0
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +792 -0
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +169 -0
- data/test/indent_test.rb +223 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +279 -23
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/search_option_test.rb +19 -0
- data/test/stream_test.rb +165 -0
- data/test/test_helper.rb +9 -0
- data/test/window_test.rb +59 -0
- metadata +68 -2
data/lib/ruvim/screen.rb
CHANGED
|
@@ -26,7 +26,7 @@ module RuVim
|
|
|
26
26
|
|
|
27
27
|
rects = window_rects(editor, text_rows:, text_cols:)
|
|
28
28
|
if (current_rect = rects[editor.current_window_id])
|
|
29
|
-
editor.current_window_view_height_hint = [current_rect[:height]
|
|
29
|
+
editor.current_window_view_height_hint = [current_rect[:height], 1].max
|
|
30
30
|
end
|
|
31
31
|
editor.window_order.each do |win_id|
|
|
32
32
|
win = editor.windows.fetch(win_id)
|
|
@@ -69,7 +69,12 @@ module RuVim
|
|
|
69
69
|
end
|
|
70
70
|
cursor_row, cursor_col = cursor_screen_position(editor, text_rows, rects)
|
|
71
71
|
out << "\e[#{cursor_row};#{cursor_col}H"
|
|
72
|
-
|
|
72
|
+
if cursor_use_terminal?(editor)
|
|
73
|
+
out << "\e[6 q"
|
|
74
|
+
out << "\e[?25h"
|
|
75
|
+
else
|
|
76
|
+
out << "\e[?25l"
|
|
77
|
+
end
|
|
73
78
|
@last_frame = frame.merge(cursor_row:, cursor_col:)
|
|
74
79
|
@terminal.write(out)
|
|
75
80
|
end
|
|
@@ -80,7 +85,7 @@ module RuVim
|
|
|
80
85
|
text_rows = [text_rows, 1].max
|
|
81
86
|
text_cols = [text_cols, 1].max
|
|
82
87
|
rect = window_rects(editor, text_rows:, text_cols:)[editor.current_window_id]
|
|
83
|
-
height = [rect ? rect[:height]
|
|
88
|
+
height = [rect ? rect[:height] : text_rows, 1].max
|
|
84
89
|
editor.current_window_view_height_hint = height if editor.respond_to?(:current_window_view_height_hint=)
|
|
85
90
|
height
|
|
86
91
|
rescue StandardError
|
|
@@ -105,6 +110,8 @@ module RuVim
|
|
|
105
110
|
lines[status_row + 1] = truncate("#{cmd.prefix}#{cmd.text}", cols)
|
|
106
111
|
elsif editor.message_error?
|
|
107
112
|
lines[status_row + 1] = error_message_line(editor.message.to_s, cols)
|
|
113
|
+
elsif !editor.message.to_s.empty?
|
|
114
|
+
lines[status_row + 1] = truncate(editor.message.to_s, cols)
|
|
108
115
|
end
|
|
109
116
|
end
|
|
110
117
|
|
|
@@ -275,15 +282,48 @@ module RuVim
|
|
|
275
282
|
end
|
|
276
283
|
|
|
277
284
|
def render_text_line(text, editor, buffer_row:, window:, buffer:, width:)
|
|
285
|
+
# Ultra-fast path: plain ASCII line with no highlighting — skip Cell creation entirely
|
|
286
|
+
if can_bulk_render_line?(text, editor, buffer_row:, window:, buffer:)
|
|
287
|
+
return bulk_render_line(text, width, col_offset: window.col_offset)
|
|
288
|
+
end
|
|
289
|
+
|
|
278
290
|
tabstop = tabstop_for(editor, window, buffer)
|
|
279
291
|
cells, display_col = RuVim::TextMetrics.clip_cells_for_width(text, width, source_col_start: window.col_offset, tabstop:)
|
|
280
292
|
render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width:, source_line: buffer.line_at(buffer_row),
|
|
281
293
|
source_col_offset: window.col_offset, leading_display_prefix: "")
|
|
282
294
|
end
|
|
283
295
|
|
|
296
|
+
def can_bulk_render_line?(text, editor, buffer_row:, window:, buffer:)
|
|
297
|
+
return false if editor.current_window_id == window.id && window.cursor_y == buffer_row
|
|
298
|
+
return false if editor.current_window_id == window.id && editor.visual_active?
|
|
299
|
+
return false if !!editor.effective_option("cursorline", window:, buffer:)
|
|
300
|
+
return false if !!editor.effective_option("list", window:, buffer:)
|
|
301
|
+
return false unless colorcolumn_display_cols(editor, window, buffer).empty?
|
|
302
|
+
return false if text.include?("\t")
|
|
303
|
+
return false unless text.ascii_only?
|
|
304
|
+
return false if text.match?(/[\x00-\x1f\x7f]/) # control chars need sanitizing
|
|
305
|
+
|
|
306
|
+
source_text = text[window.col_offset..].to_s
|
|
307
|
+
return false unless search_highlight_source_cols(editor, source_text, source_col_offset: window.col_offset).empty?
|
|
308
|
+
return false unless syntax_highlight_source_cols(editor, window, buffer, source_text, source_col_offset: window.col_offset).empty?
|
|
309
|
+
|
|
310
|
+
true
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def bulk_render_line(text, width, col_offset:)
|
|
314
|
+
clipped = text[col_offset, width].to_s
|
|
315
|
+
clipped + (" " * [width - clipped.length, 0].max)
|
|
316
|
+
end
|
|
317
|
+
|
|
284
318
|
def render_text_segment(source_line, editor, buffer_row:, window:, buffer:, width:, source_col_start:, display_prefix: "")
|
|
319
|
+
prefix = display_prefix
|
|
320
|
+
|
|
321
|
+
# Bulk path: no prefix, printable ASCII, no highlighting
|
|
322
|
+
if prefix.empty? && can_bulk_render_line?(source_line, editor, buffer_row:, window:, buffer:)
|
|
323
|
+
return bulk_render_line(source_line, width, col_offset: source_col_start)
|
|
324
|
+
end
|
|
325
|
+
|
|
285
326
|
tabstop = tabstop_for(editor, window, buffer)
|
|
286
|
-
prefix = display_prefix.to_s
|
|
287
327
|
prefix_w = RuVim::DisplayWidth.display_width(prefix, tabstop:)
|
|
288
328
|
avail = [width - prefix_w, 0].max
|
|
289
329
|
cells, display_col = RuVim::TextMetrics.clip_cells_for_width(source_line[source_col_start..].to_s, avail, source_col_start:, tabstop:)
|
|
@@ -295,7 +335,6 @@ module RuVim
|
|
|
295
335
|
body
|
|
296
336
|
else
|
|
297
337
|
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
338
|
out = prefix_render + body
|
|
300
339
|
out
|
|
301
340
|
end
|
|
@@ -319,28 +358,38 @@ module RuVim
|
|
|
319
358
|
leading_prefix_width = RuVim::DisplayWidth.display_width(leading_display_prefix.to_s, tabstop:)
|
|
320
359
|
display_pos = leading_prefix_width
|
|
321
360
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
361
|
+
# Fast path: no highlighting needed — bulk output glyphs
|
|
362
|
+
if !current_line && !visual && !cursorline_enabled &&
|
|
363
|
+
!list_enabled && search_cols.empty? &&
|
|
364
|
+
syntax_cols.empty? && colorcolumns.empty?
|
|
365
|
+
cells.each do |cell|
|
|
366
|
+
highlighted << cell.glyph
|
|
367
|
+
display_pos += [cell.display_width, 1].max
|
|
368
|
+
end
|
|
369
|
+
else
|
|
370
|
+
cells.each do |cell|
|
|
371
|
+
ch = display_glyph_for_cell(cell, source_line, list_enabled, listchars, tab_seen, trail_from)
|
|
372
|
+
buffer_col = cell.source_col
|
|
373
|
+
selected = selected_in_visual?(visual, buffer_row, buffer_col)
|
|
374
|
+
cursor_here = (current_line && window.cursor_x == buffer_col)
|
|
375
|
+
colorcolumn_here = colorcolumns[display_pos]
|
|
376
|
+
if cursor_here
|
|
377
|
+
highlighted << cursor_cell_render(editor, ch)
|
|
378
|
+
elsif selected
|
|
379
|
+
highlighted << "\e[7m#{ch}\e[m"
|
|
380
|
+
elsif search_cols[buffer_col]
|
|
381
|
+
highlighted << "#{search_bg_seq(editor)}#{ch}\e[m"
|
|
382
|
+
elsif colorcolumn_here
|
|
383
|
+
highlighted << "#{colorcolumn_bg_seq(editor)}#{ch}\e[m"
|
|
384
|
+
elsif cursorline_enabled
|
|
385
|
+
highlighted << "#{cursorline_bg_seq(editor)}#{ch}\e[m"
|
|
386
|
+
elsif (syntax_color = syntax_cols[buffer_col])
|
|
387
|
+
highlighted << "#{syntax_color}#{ch}\e[m"
|
|
388
|
+
else
|
|
389
|
+
highlighted << ch
|
|
390
|
+
end
|
|
391
|
+
display_pos += [cell.display_width, 1].max
|
|
342
392
|
end
|
|
343
|
-
display_pos += [cell.display_width.to_i, 1].max
|
|
344
393
|
end
|
|
345
394
|
|
|
346
395
|
if editor.current_window_id == window.id && window.cursor_y == buffer_row
|
|
@@ -379,7 +428,7 @@ module RuVim
|
|
|
379
428
|
|
|
380
429
|
base = RuVim::TextMetrics.screen_col_for_char_index(source_line, cursor_x, tabstop:) -
|
|
381
430
|
RuVim::TextMetrics.screen_col_for_char_index(source_line, source_col_offset, tabstop:)
|
|
382
|
-
extra = [cursor_x
|
|
431
|
+
extra = [cursor_x - source_line.length, 0].max
|
|
383
432
|
leading_prefix_width + [base, 0].max + extra
|
|
384
433
|
end
|
|
385
434
|
|
|
@@ -411,7 +460,7 @@ module RuVim
|
|
|
411
460
|
raw_lines << (row < buffer.line_count ? buffer.line_at(row) : nil)
|
|
412
461
|
end
|
|
413
462
|
|
|
414
|
-
non_nil = raw_lines.compact
|
|
463
|
+
non_nil = raw_lines.compact.map { |l| RuVim::TextMetrics.terminal_safe_text(l) }
|
|
415
464
|
context = rich_view_context(editor, window, buffer)
|
|
416
465
|
formatted = RuVim::RichView.render_visible_lines(editor, non_nil, context: context)
|
|
417
466
|
fmt_idx = 0
|
|
@@ -456,7 +505,7 @@ module RuVim
|
|
|
456
505
|
def render_rich_view_line_sc(text, width:, skip_sc:)
|
|
457
506
|
# Phase 1: skip `skip_sc` display columns
|
|
458
507
|
# Collect ANSI sequences encountered during skip so active styles carry over.
|
|
459
|
-
chars = text
|
|
508
|
+
chars = text
|
|
460
509
|
pos = 0
|
|
461
510
|
skipped = 0
|
|
462
511
|
len = chars.length
|
|
@@ -546,9 +595,9 @@ module RuVim
|
|
|
546
595
|
end
|
|
547
596
|
|
|
548
597
|
def ensure_visible_under_wrap(editor, window, buffer, height:, content_w:)
|
|
549
|
-
return if height
|
|
598
|
+
return if height <= 0 || buffer.line_count <= 0
|
|
550
599
|
|
|
551
|
-
window.row_offset = [[window.row_offset
|
|
600
|
+
window.row_offset = [[window.row_offset, 0].max, buffer.line_count - 1].min
|
|
552
601
|
return if window.cursor_y < window.row_offset
|
|
553
602
|
|
|
554
603
|
cursor_line = buffer.line_at(window.cursor_y)
|
|
@@ -641,7 +690,6 @@ module RuVim
|
|
|
641
690
|
linebreak = !!editor.effective_option("linebreak", window:, buffer:)
|
|
642
691
|
showbreak = editor.effective_option("showbreak", window:, buffer:).to_s
|
|
643
692
|
breakindent = !!editor.effective_option("breakindent", window:, buffer:)
|
|
644
|
-
line = line.to_s
|
|
645
693
|
return [{ source_col_start: 0, display_prefix: "" }] if line.empty?
|
|
646
694
|
|
|
647
695
|
cache_key = [line.object_id, line.length, line.hash, width, tabstop, linebreak, showbreak, breakindent]
|
|
@@ -652,7 +700,10 @@ module RuVim
|
|
|
652
700
|
indent_prefix = breakindent ? wrapped_indent_prefix(line, tabstop:, max_width: [width - RuVim::DisplayWidth.display_width(showbreak, tabstop:), 0].max) : ""
|
|
653
701
|
segs = compute_wrapped_segments(line, width:, tabstop:, linebreak:, showbreak:, indent_prefix:)
|
|
654
702
|
@wrapped_segments_cache[cache_key] = segs
|
|
655
|
-
|
|
703
|
+
if @wrapped_segments_cache.length > WRAP_SEGMENTS_CACHE_LIMIT
|
|
704
|
+
trim = @wrapped_segments_cache.length - WRAP_SEGMENTS_CACHE_LIMIT / 2
|
|
705
|
+
trim.times { @wrapped_segments_cache.shift }
|
|
706
|
+
end
|
|
656
707
|
segs
|
|
657
708
|
end
|
|
658
709
|
|
|
@@ -679,7 +730,7 @@ module RuVim
|
|
|
679
730
|
end
|
|
680
731
|
|
|
681
732
|
segs << { source_col_start: start_col, display_prefix: display_prefix }.freeze
|
|
682
|
-
next_start = cells.last.source_col
|
|
733
|
+
next_start = cells.last.source_col + 1
|
|
683
734
|
if linebreak
|
|
684
735
|
next_start += 1 while next_start < line.length && line[next_start] == " "
|
|
685
736
|
end
|
|
@@ -693,7 +744,7 @@ module RuVim
|
|
|
693
744
|
end
|
|
694
745
|
|
|
695
746
|
def wrapped_segment_index(segs, cursor_x)
|
|
696
|
-
x = cursor_x
|
|
747
|
+
x = cursor_x
|
|
697
748
|
seg_index = 0
|
|
698
749
|
segs.each_with_index do |seg, i|
|
|
699
750
|
nxt = segs[i + 1]
|
|
@@ -723,7 +774,7 @@ module RuVim
|
|
|
723
774
|
""
|
|
724
775
|
end
|
|
725
776
|
|
|
726
|
-
def display_glyph_for_cell(cell, source_line, list_enabled
|
|
777
|
+
def display_glyph_for_cell(cell, source_line, list_enabled, listchars, tab_seen, trail_from)
|
|
727
778
|
return cell.glyph unless list_enabled
|
|
728
779
|
|
|
729
780
|
src = source_line[cell.source_col]
|
|
@@ -742,7 +793,7 @@ module RuVim
|
|
|
742
793
|
end
|
|
743
794
|
|
|
744
795
|
def parse_listchars(raw)
|
|
745
|
-
raw_key = raw
|
|
796
|
+
raw_key = raw
|
|
746
797
|
@listchars_cache ||= {}
|
|
747
798
|
return @listchars_cache[raw_key] if @listchars_cache.key?(raw_key)
|
|
748
799
|
|
|
@@ -786,6 +837,12 @@ module RuVim
|
|
|
786
837
|
end
|
|
787
838
|
|
|
788
839
|
def number_column_width(editor, window, buffer)
|
|
840
|
+
labels = buffer.options["gutter_labels"]
|
|
841
|
+
if labels && !labels.empty?
|
|
842
|
+
max_w = labels.map { |l| RuVim::DisplayWidth.display_width(l.to_s) }.max || 0
|
|
843
|
+
return max_w
|
|
844
|
+
end
|
|
845
|
+
|
|
789
846
|
sign_w = sign_column_width(editor, window, buffer)
|
|
790
847
|
enabled = editor.effective_option("number", window:, buffer:) || editor.effective_option("relativenumber", window:, buffer:)
|
|
791
848
|
return sign_w unless enabled
|
|
@@ -817,6 +874,15 @@ module RuVim
|
|
|
817
874
|
end
|
|
818
875
|
|
|
819
876
|
def render_gutter_prefix(editor, window, buffer, buffer_row, width)
|
|
877
|
+
labels = buffer.options["gutter_labels"]
|
|
878
|
+
if labels
|
|
879
|
+
return " " * width if buffer_row.nil?
|
|
880
|
+
|
|
881
|
+
label = (labels[buffer_row] || "").ljust(width)[0, width]
|
|
882
|
+
color = line_number_fg_seq(editor, current_line: false)
|
|
883
|
+
return "#{color}#{label}\e[m"
|
|
884
|
+
end
|
|
885
|
+
|
|
820
886
|
prefix = line_number_prefix(editor, window, buffer, buffer_row, width)
|
|
821
887
|
return prefix if prefix.empty?
|
|
822
888
|
return prefix if buffer_row.nil?
|
|
@@ -871,15 +937,19 @@ module RuVim
|
|
|
871
937
|
end
|
|
872
938
|
|
|
873
939
|
def search_bg_seq(editor)
|
|
874
|
-
|
|
940
|
+
term_color(editor, "\e[48;2;255;215;0m", "\e[43m")
|
|
875
941
|
end
|
|
876
942
|
|
|
877
943
|
def colorcolumn_bg_seq(editor)
|
|
878
|
-
|
|
944
|
+
term_color(editor, "\e[48;2;72;72;72m", "\e[48;5;238m")
|
|
879
945
|
end
|
|
880
946
|
|
|
881
947
|
def cursorline_bg_seq(editor)
|
|
882
|
-
|
|
948
|
+
term_color(editor, "\e[48;2;58;58;58m", "\e[48;5;236m")
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def term_color(editor, truecolor_seq, fallback_seq)
|
|
952
|
+
truecolor_enabled?(editor) ? truecolor_seq : fallback_seq
|
|
883
953
|
end
|
|
884
954
|
|
|
885
955
|
def line_number_fg_seq(editor, current_line: false)
|
|
@@ -911,34 +981,15 @@ module RuVim
|
|
|
911
981
|
|
|
912
982
|
path = buffer.display_name
|
|
913
983
|
mod = buffer.modified? ? " [+]" : ""
|
|
914
|
-
stream =
|
|
915
|
-
|
|
984
|
+
stream = buffer.stream_status ? " [#{buffer.stream_status}]" : ""
|
|
985
|
+
cmd = buffer.stream_command ? " #{buffer.stream_command}" : ""
|
|
916
986
|
tab = tab_status_token(editor)
|
|
917
|
-
|
|
918
|
-
left = "#{mode} #{path}#{mod}#{stream}#{loading}"
|
|
987
|
+
left = "#{mode} #{path}#{mod}#{stream}#{cmd}"
|
|
919
988
|
right = " #{window.cursor_y + 1}:#{window.cursor_x + 1}#{tab} "
|
|
920
989
|
body_width = [width - right.length, 0].max
|
|
921
|
-
"#{
|
|
990
|
+
"#{left.ljust(body_width)[0, body_width]}#{right}"
|
|
922
991
|
end
|
|
923
992
|
|
|
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
993
|
|
|
943
994
|
def tab_status_token(editor)
|
|
944
995
|
return "" if editor.tabpage_count <= 1
|
|
@@ -946,20 +997,6 @@ module RuVim
|
|
|
946
997
|
" tab:#{editor.current_tabpage_number}/#{editor.tabpage_count}"
|
|
947
998
|
end
|
|
948
999
|
|
|
949
|
-
def compose_status_body(left, msg, width)
|
|
950
|
-
w = [width.to_i, 0].max
|
|
951
|
-
return "" if w.zero?
|
|
952
|
-
return left.ljust(w)[0, w] if msg.to_s.empty?
|
|
953
|
-
|
|
954
|
-
msg_part = " | #{msg}"
|
|
955
|
-
if msg_part.length >= w
|
|
956
|
-
return msg_part[0, w]
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
left_budget = w - msg_part.length
|
|
960
|
-
"#{left.ljust(left_budget)[0, left_budget]}#{msg_part}"
|
|
961
|
-
end
|
|
962
|
-
|
|
963
1000
|
def truncate(str, width)
|
|
964
1001
|
safe = RuVim::TextMetrics.terminal_safe_text(str)
|
|
965
1002
|
RuVim::TextMetrics.pad_plain_to_screen_width(safe, width)
|
|
@@ -977,6 +1014,16 @@ module RuVim
|
|
|
977
1014
|
"\e[7m"
|
|
978
1015
|
end
|
|
979
1016
|
|
|
1017
|
+
# Insert/command-line: show terminal bar cursor; otherwise: hide terminal cursor, use cell rendering
|
|
1018
|
+
def cursor_use_terminal?(editor)
|
|
1019
|
+
case editor.mode
|
|
1020
|
+
when :insert, :command_line
|
|
1021
|
+
true
|
|
1022
|
+
else
|
|
1023
|
+
false
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
980
1027
|
def cursor_screen_position(editor, text_rows, rects)
|
|
981
1028
|
window = editor.current_window
|
|
982
1029
|
|
|
@@ -1028,12 +1075,12 @@ module RuVim
|
|
|
1028
1075
|
RuVim::TextMetrics.screen_col_for_char_index(line, window.col_offset, tabstop:)
|
|
1029
1076
|
col = rect[:left] + gutter_w + [prefix_screen_col, 0].max + extra_virtual
|
|
1030
1077
|
end
|
|
1031
|
-
min_row = [rect[:top]
|
|
1032
|
-
max_row = [rect[:top]
|
|
1033
|
-
min_col = [rect[:left]
|
|
1034
|
-
max_col = [rect[:left]
|
|
1035
|
-
row = [[row
|
|
1036
|
-
col = [[col
|
|
1078
|
+
min_row = [rect[:top], 1].max
|
|
1079
|
+
max_row = [rect[:top] + [rect[:height], 1].max - 1, min_row].max
|
|
1080
|
+
min_col = [rect[:left], 1].max
|
|
1081
|
+
max_col = [rect[:left] + [rect[:width], 1].max - 1, min_col].max
|
|
1082
|
+
row = [[row, min_row].max, max_row].min
|
|
1083
|
+
col = [[col, min_col].max, max_col].min
|
|
1037
1084
|
[row, col]
|
|
1038
1085
|
end
|
|
1039
1086
|
|
|
@@ -1130,6 +1177,7 @@ module RuVim
|
|
|
1130
1177
|
search = editor.last_search
|
|
1131
1178
|
return {} unless search && search[:pattern]
|
|
1132
1179
|
return {} unless editor.effective_option("hlsearch")
|
|
1180
|
+
return {} if editor.hlsearch_suppressed?
|
|
1133
1181
|
|
|
1134
1182
|
regex = build_screen_search_regex(editor, search[:pattern])
|
|
1135
1183
|
cols = {}
|
|
@@ -1177,7 +1225,10 @@ module RuVim
|
|
|
1177
1225
|
|
|
1178
1226
|
cols = RuVim::Highlighter.color_columns(filetype, source_line_text)
|
|
1179
1227
|
@syntax_color_cache[key] = cols
|
|
1180
|
-
|
|
1228
|
+
if @syntax_color_cache.length > SYNTAX_CACHE_LIMIT
|
|
1229
|
+
trim = @syntax_color_cache.length - SYNTAX_CACHE_LIMIT / 2
|
|
1230
|
+
trim.times { @syntax_color_cache.shift }
|
|
1231
|
+
end
|
|
1181
1232
|
cols
|
|
1182
1233
|
end
|
|
1183
1234
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class Stream::FileLoad < Stream
|
|
5
|
+
CHUNK_BYTES = 1 * 1024 * 1024
|
|
6
|
+
FLUSH_BYTES = 32 * 1024 * 1024
|
|
7
|
+
|
|
8
|
+
attr_accessor :thread, :io
|
|
9
|
+
|
|
10
|
+
def initialize(io:, file_size:, buffer_id:, queue:, stop_handler: nil, ¬ify)
|
|
11
|
+
super(stop_handler: stop_handler)
|
|
12
|
+
@io = io
|
|
13
|
+
@state = :live
|
|
14
|
+
@thread = Thread.new do
|
|
15
|
+
pending_bytes = "".b
|
|
16
|
+
ended_with_newline = false
|
|
17
|
+
loaded_bytes = io.pos
|
|
18
|
+
loop do
|
|
19
|
+
chunk = io.readpartial(CHUNK_BYTES)
|
|
20
|
+
next if chunk.nil? || chunk.empty?
|
|
21
|
+
|
|
22
|
+
loaded_bytes += chunk.bytesize
|
|
23
|
+
ended_with_newline = chunk.end_with?("\n")
|
|
24
|
+
pending_bytes << chunk
|
|
25
|
+
next if pending_bytes.bytesize < FLUSH_BYTES
|
|
26
|
+
|
|
27
|
+
last_nl = pending_bytes.rindex("\n".b)
|
|
28
|
+
if last_nl
|
|
29
|
+
send_bytes = pending_bytes[0..last_nl]
|
|
30
|
+
pending_bytes = pending_bytes[(last_nl + 1)..] || "".b
|
|
31
|
+
else
|
|
32
|
+
send_bytes = pending_bytes
|
|
33
|
+
pending_bytes = "".b
|
|
34
|
+
end
|
|
35
|
+
decoded = Buffer.decode_text(send_bytes)
|
|
36
|
+
parts = decoded.split("\n", -1)
|
|
37
|
+
head = parts.shift || ""
|
|
38
|
+
queue << { type: :file_lines, buffer_id: buffer_id, head: head, lines: parts, loaded_bytes: loaded_bytes, file_size: file_size }
|
|
39
|
+
notify.call
|
|
40
|
+
end
|
|
41
|
+
rescue EOFError
|
|
42
|
+
unless pending_bytes.empty?
|
|
43
|
+
decoded = Buffer.decode_text(pending_bytes)
|
|
44
|
+
parts = decoded.split("\n", -1)
|
|
45
|
+
head = parts.shift || ""
|
|
46
|
+
queue << { type: :file_lines, buffer_id: buffer_id, head: head, lines: parts }
|
|
47
|
+
notify.call
|
|
48
|
+
end
|
|
49
|
+
queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: ended_with_newline }
|
|
50
|
+
notify.call
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
queue << { type: :file_error, buffer_id: buffer_id, error: e.message.to_s }
|
|
53
|
+
notify.call
|
|
54
|
+
ensure
|
|
55
|
+
begin
|
|
56
|
+
io.close unless io.closed?
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def status
|
|
64
|
+
case @state
|
|
65
|
+
when :live then "load"
|
|
66
|
+
when :error then "load/error"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stop!
|
|
71
|
+
io = @io; @io = nil
|
|
72
|
+
begin
|
|
73
|
+
io&.close unless io&.closed?
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
thread = @thread; @thread = nil
|
|
78
|
+
if thread&.alive?
|
|
79
|
+
thread.kill
|
|
80
|
+
thread.join(0.05)
|
|
81
|
+
end
|
|
82
|
+
@state = :closed
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class Stream::Follow < Stream
|
|
5
|
+
attr_accessor :watcher, :backend
|
|
6
|
+
|
|
7
|
+
def initialize(path:, buffer_id:, queue:, stop_handler: nil, ¬ify)
|
|
8
|
+
super(stop_handler: stop_handler)
|
|
9
|
+
@state = :live
|
|
10
|
+
@watcher = FileWatcher.create(path) do |type, data|
|
|
11
|
+
case type
|
|
12
|
+
when :data
|
|
13
|
+
queue << { type: :follow_data, buffer_id: buffer_id, data: data }
|
|
14
|
+
when :truncated
|
|
15
|
+
queue << { type: :follow_truncated, buffer_id: buffer_id }
|
|
16
|
+
when :deleted
|
|
17
|
+
queue << { type: :follow_deleted, buffer_id: buffer_id }
|
|
18
|
+
end
|
|
19
|
+
notify.call
|
|
20
|
+
end
|
|
21
|
+
@backend = @watcher.backend
|
|
22
|
+
@watcher.start
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def status
|
|
26
|
+
case @state
|
|
27
|
+
when :live
|
|
28
|
+
@backend == :inotify ? "follow/i" : "follow"
|
|
29
|
+
when :error then "follow/error"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop!
|
|
34
|
+
watcher = @watcher; @watcher = nil
|
|
35
|
+
watcher&.stop
|
|
36
|
+
@backend = nil
|
|
37
|
+
@state = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class Stream::Git < Stream
|
|
5
|
+
attr_accessor :io, :thread
|
|
6
|
+
|
|
7
|
+
def initialize(cmd:, root:, buffer_id:, queue:, stop_handler: nil, ¬ify)
|
|
8
|
+
super(stop_handler: stop_handler)
|
|
9
|
+
@io = nil
|
|
10
|
+
stream = self
|
|
11
|
+
@thread = Thread.new do
|
|
12
|
+
IO.popen(cmd, chdir: root, err: [:child, :out]) do |io|
|
|
13
|
+
stream.io = io
|
|
14
|
+
while (chunk = io.read(4096))
|
|
15
|
+
queue << { type: :stream_data, buffer_id: buffer_id, data: Buffer.decode_text(chunk) }
|
|
16
|
+
notify.call
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
stream.io = nil
|
|
20
|
+
queue << { type: :stream_eof, buffer_id: buffer_id }
|
|
21
|
+
notify.call
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
stream.io = nil
|
|
24
|
+
queue << { type: :stream_error, buffer_id: buffer_id, error: e.message.to_s }
|
|
25
|
+
notify.call
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop!
|
|
30
|
+
io = @io; @io = nil
|
|
31
|
+
begin
|
|
32
|
+
io&.close unless io&.closed?
|
|
33
|
+
rescue IOError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
thread = @thread; @thread = nil
|
|
37
|
+
if thread&.alive?
|
|
38
|
+
thread.kill
|
|
39
|
+
thread.join(0.05)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pty"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
class Stream::Run < Stream
|
|
7
|
+
attr_accessor :io, :pid, :thread, :command, :exit_status
|
|
8
|
+
|
|
9
|
+
def initialize(command:, buffer_id:, queue:, stop_handler: nil, ¬ify)
|
|
10
|
+
super(stop_handler: stop_handler)
|
|
11
|
+
@command = command
|
|
12
|
+
@io = nil
|
|
13
|
+
@pid = nil
|
|
14
|
+
@exit_status = nil
|
|
15
|
+
@state = :live
|
|
16
|
+
stream = self
|
|
17
|
+
@thread = Thread.new do
|
|
18
|
+
shell = ENV["SHELL"].to_s
|
|
19
|
+
shell = "/bin/sh" if shell.empty?
|
|
20
|
+
PTY.spawn(shell, "-c", command) do |r, _w, pid|
|
|
21
|
+
stream.io = r
|
|
22
|
+
stream.pid = pid
|
|
23
|
+
begin
|
|
24
|
+
while (chunk = r.readpartial(4096))
|
|
25
|
+
text = Buffer.decode_text(chunk).delete("\r")
|
|
26
|
+
queue << { type: :stream_data, buffer_id: buffer_id, data: text }
|
|
27
|
+
notify.call
|
|
28
|
+
end
|
|
29
|
+
rescue EOFError, Errno::EIO
|
|
30
|
+
# expected: PTY raises EIO when child process exits
|
|
31
|
+
end
|
|
32
|
+
_status = Process.waitpid2(pid)[1] rescue nil
|
|
33
|
+
stream.io = nil
|
|
34
|
+
queue << { type: :stream_eof, buffer_id: buffer_id, status: _status }
|
|
35
|
+
notify.call
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
stream.io = nil
|
|
39
|
+
queue << { type: :stream_error, buffer_id: buffer_id, error: e.message.to_s }
|
|
40
|
+
notify.call
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def status
|
|
45
|
+
case @state
|
|
46
|
+
when :live then "run"
|
|
47
|
+
when :closed
|
|
48
|
+
code = @exit_status&.exitstatus
|
|
49
|
+
code ? "run/exit #{code}" : "run/EOF"
|
|
50
|
+
when :error then "run/error"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def stop!
|
|
55
|
+
pid = @pid; @pid = nil
|
|
56
|
+
if pid
|
|
57
|
+
Process.kill(:TERM, pid) rescue nil
|
|
58
|
+
Process.waitpid(pid) rescue nil
|
|
59
|
+
end
|
|
60
|
+
io = @io; @io = nil
|
|
61
|
+
begin
|
|
62
|
+
io&.close unless io&.closed?
|
|
63
|
+
rescue IOError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
thread = @thread; @thread = nil
|
|
67
|
+
if thread&.alive?
|
|
68
|
+
thread.kill
|
|
69
|
+
thread.join(0.05)
|
|
70
|
+
end
|
|
71
|
+
@state = :closed
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|