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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +68 -7
  3. data/README.md +30 -7
  4. data/Rakefile +7 -0
  5. data/benchmark/cext_compare.rb +165 -0
  6. data/benchmark/chunked_load.rb +256 -0
  7. data/benchmark/file_load.rb +140 -0
  8. data/benchmark/hotspots.rb +178 -0
  9. data/docs/binding.md +18 -1
  10. data/docs/command.md +156 -10
  11. data/docs/config.md +10 -2
  12. data/docs/done.md +23 -0
  13. data/docs/spec.md +162 -25
  14. data/docs/todo.md +9 -0
  15. data/docs/tutorial.md +33 -1
  16. data/docs/vim_diff.md +31 -8
  17. data/ext/ruvim/extconf.rb +5 -0
  18. data/ext/ruvim/ruvim_ext.c +519 -0
  19. data/lib/ruvim/app.rb +246 -2525
  20. data/lib/ruvim/browser.rb +104 -0
  21. data/lib/ruvim/buffer.rb +43 -20
  22. data/lib/ruvim/cli.rb +6 -0
  23. data/lib/ruvim/command_invocation.rb +2 -2
  24. data/lib/ruvim/completion_manager.rb +708 -0
  25. data/lib/ruvim/dispatcher.rb +14 -8
  26. data/lib/ruvim/display_width.rb +91 -45
  27. data/lib/ruvim/editor.rb +74 -80
  28. data/lib/ruvim/ex_command_registry.rb +3 -1
  29. data/lib/ruvim/file_watcher.rb +243 -0
  30. data/lib/ruvim/gh/link.rb +207 -0
  31. data/lib/ruvim/git/blame.rb +255 -0
  32. data/lib/ruvim/git/branch.rb +112 -0
  33. data/lib/ruvim/git/commit.rb +102 -0
  34. data/lib/ruvim/git/diff.rb +129 -0
  35. data/lib/ruvim/git/grep.rb +107 -0
  36. data/lib/ruvim/git/handler.rb +125 -0
  37. data/lib/ruvim/git/log.rb +41 -0
  38. data/lib/ruvim/git/status.rb +103 -0
  39. data/lib/ruvim/global_commands.rb +351 -77
  40. data/lib/ruvim/highlighter.rb +4 -11
  41. data/lib/ruvim/input.rb +1 -0
  42. data/lib/ruvim/key_handler.rb +1510 -0
  43. data/lib/ruvim/keymap_manager.rb +7 -7
  44. data/lib/ruvim/lang/base.rb +5 -0
  45. data/lib/ruvim/lang/c.rb +116 -0
  46. data/lib/ruvim/lang/cpp.rb +107 -0
  47. data/lib/ruvim/lang/csv.rb +4 -1
  48. data/lib/ruvim/lang/diff.rb +43 -0
  49. data/lib/ruvim/lang/dockerfile.rb +36 -0
  50. data/lib/ruvim/lang/elixir.rb +85 -0
  51. data/lib/ruvim/lang/erb.rb +30 -0
  52. data/lib/ruvim/lang/go.rb +83 -0
  53. data/lib/ruvim/lang/html.rb +34 -0
  54. data/lib/ruvim/lang/javascript.rb +83 -0
  55. data/lib/ruvim/lang/json.rb +40 -0
  56. data/lib/ruvim/lang/lua.rb +76 -0
  57. data/lib/ruvim/lang/makefile.rb +36 -0
  58. data/lib/ruvim/lang/markdown.rb +3 -4
  59. data/lib/ruvim/lang/ocaml.rb +77 -0
  60. data/lib/ruvim/lang/perl.rb +91 -0
  61. data/lib/ruvim/lang/python.rb +85 -0
  62. data/lib/ruvim/lang/registry.rb +102 -0
  63. data/lib/ruvim/lang/ruby.rb +7 -0
  64. data/lib/ruvim/lang/rust.rb +95 -0
  65. data/lib/ruvim/lang/scheme.rb +5 -0
  66. data/lib/ruvim/lang/sh.rb +76 -0
  67. data/lib/ruvim/lang/sql.rb +52 -0
  68. data/lib/ruvim/lang/toml.rb +36 -0
  69. data/lib/ruvim/lang/tsv.rb +4 -1
  70. data/lib/ruvim/lang/typescript.rb +53 -0
  71. data/lib/ruvim/lang/yaml.rb +62 -0
  72. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  73. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  74. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  75. data/lib/ruvim/rich_view.rb +30 -7
  76. data/lib/ruvim/screen.rb +135 -84
  77. data/lib/ruvim/stream/file_load.rb +85 -0
  78. data/lib/ruvim/stream/follow.rb +40 -0
  79. data/lib/ruvim/stream/git.rb +43 -0
  80. data/lib/ruvim/stream/run.rb +74 -0
  81. data/lib/ruvim/stream/stdin.rb +55 -0
  82. data/lib/ruvim/stream.rb +35 -0
  83. data/lib/ruvim/stream_mixer.rb +394 -0
  84. data/lib/ruvim/terminal.rb +18 -4
  85. data/lib/ruvim/text_metrics.rb +84 -65
  86. data/lib/ruvim/version.rb +1 -1
  87. data/lib/ruvim/window.rb +5 -5
  88. data/lib/ruvim.rb +31 -4
  89. data/test/app_command_test.rb +382 -0
  90. data/test/app_completion_test.rb +65 -16
  91. data/test/app_dot_repeat_test.rb +27 -3
  92. data/test/app_ex_command_test.rb +154 -0
  93. data/test/app_motion_test.rb +13 -12
  94. data/test/app_register_test.rb +2 -1
  95. data/test/app_scenario_test.rb +182 -8
  96. data/test/app_startup_test.rb +70 -27
  97. data/test/app_text_object_test.rb +2 -1
  98. data/test/app_unicode_behavior_test.rb +3 -2
  99. data/test/browser_test.rb +88 -0
  100. data/test/buffer_test.rb +24 -0
  101. data/test/cli_test.rb +77 -0
  102. data/test/clipboard_test.rb +67 -0
  103. data/test/command_invocation_test.rb +33 -0
  104. data/test/command_line_test.rb +118 -0
  105. data/test/config_dsl_test.rb +134 -0
  106. data/test/dispatcher_test.rb +74 -4
  107. data/test/display_width_test.rb +41 -0
  108. data/test/ex_command_registry_test.rb +106 -0
  109. data/test/file_watcher_test.rb +197 -0
  110. data/test/follow_test.rb +198 -0
  111. data/test/gh_link_test.rb +141 -0
  112. data/test/git_blame_test.rb +792 -0
  113. data/test/git_grep_test.rb +64 -0
  114. data/test/highlighter_test.rb +169 -0
  115. data/test/indent_test.rb +223 -0
  116. data/test/input_screen_integration_test.rb +1 -1
  117. data/test/keyword_chars_test.rb +85 -0
  118. data/test/lang_test.rb +634 -0
  119. data/test/markdown_renderer_test.rb +5 -5
  120. data/test/on_save_hook_test.rb +12 -8
  121. data/test/render_snapshot_test.rb +78 -0
  122. data/test/rich_view_test.rb +279 -23
  123. data/test/run_command_test.rb +307 -0
  124. data/test/screen_test.rb +68 -5
  125. data/test/search_option_test.rb +19 -0
  126. data/test/stream_test.rb +165 -0
  127. data/test/test_helper.rb +9 -0
  128. data/test/window_test.rb +59 -0
  129. 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].to_i, 1].max
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
- out << "\e[?25h"
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].to_i : text_rows, 1].max
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
- cells.each do |cell|
323
- ch = display_glyph_for_cell(cell, source_line, list_enabled:, listchars:, tab_seen:, trail_from:)
324
- buffer_col = cell.source_col
325
- selected = selected_in_visual?(visual, buffer_row, buffer_col)
326
- cursor_here = (editor.current_window_id == window.id && window.cursor_y == buffer_row && window.cursor_x == buffer_col)
327
- colorcolumn_here = colorcolumns[display_pos]
328
- if cursor_here
329
- highlighted << cursor_cell_render(editor, ch)
330
- elsif selected
331
- highlighted << "\e[7m#{ch}\e[m"
332
- elsif search_cols[buffer_col]
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"
338
- elsif (syntax_color = syntax_cols[buffer_col])
339
- highlighted << "#{syntax_color}#{ch}\e[m"
340
- else
341
- highlighted << ch
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.to_i - source_line.to_s.length, 0].max
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.to_s
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.to_i <= 0 || buffer.line_count <= 0
598
+ return if height <= 0 || buffer.line_count <= 0
550
599
 
551
- window.row_offset = [[window.row_offset.to_i, 0].max, buffer.line_count - 1].min
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
- @wrapped_segments_cache.shift while @wrapped_segments_cache.length > WRAP_SEGMENTS_CACHE_LIMIT
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.to_i + 1
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.to_i
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:, listchars:, tab_seen:, trail_from:)
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.to_s
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
- truecolor_enabled?(editor) ? "\e[48;2;255;215;0m" : "\e[43m"
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
- truecolor_enabled?(editor) ? "\e[48;2;72;72;72m" : "\e[48;5;238m"
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
- truecolor_enabled?(editor) ? "\e[48;2;58;58;58m" : "\e[48;5;236m"
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 = stream_status_token(buffer)
915
- loading = file_loading_status_token(buffer)
984
+ stream = buffer.stream_status ? " [#{buffer.stream_status}]" : ""
985
+ cmd = buffer.stream_command ? " #{buffer.stream_command}" : ""
916
986
  tab = tab_status_token(editor)
917
- msg = editor.message_error? ? "" : editor.message.to_s
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
- "#{compose_status_body(left, msg, body_width)}#{right}"
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].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
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
- @syntax_color_cache.shift while @syntax_color_cache.length > SYNTAX_CACHE_LIMIT
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, &notify)
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, &notify)
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, &notify)
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, &notify)
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