ruvim 0.3.0 → 0.4.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +18 -6
  3. data/README.md +15 -1
  4. data/docs/binding.md +16 -0
  5. data/docs/command.md +78 -4
  6. data/docs/config.md +10 -2
  7. data/docs/spec.md +60 -9
  8. data/docs/tutorial.md +24 -0
  9. data/docs/vim_diff.md +18 -8
  10. data/lib/ruvim/app.rb +290 -8
  11. data/lib/ruvim/buffer.rb +14 -2
  12. data/lib/ruvim/cli.rb +6 -0
  13. data/lib/ruvim/editor.rb +12 -1
  14. data/lib/ruvim/file_watcher.rb +243 -0
  15. data/lib/ruvim/git/blame.rb +245 -0
  16. data/lib/ruvim/git/branch.rb +97 -0
  17. data/lib/ruvim/git/commit.rb +102 -0
  18. data/lib/ruvim/git/diff.rb +129 -0
  19. data/lib/ruvim/git/handler.rb +84 -0
  20. data/lib/ruvim/git/log.rb +41 -0
  21. data/lib/ruvim/git/status.rb +103 -0
  22. data/lib/ruvim/global_commands.rb +176 -42
  23. data/lib/ruvim/highlighter.rb +3 -1
  24. data/lib/ruvim/input.rb +1 -0
  25. data/lib/ruvim/lang/diff.rb +41 -0
  26. data/lib/ruvim/lang/json.rb +34 -0
  27. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  28. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  29. data/lib/ruvim/rich_view.rb +16 -0
  30. data/lib/ruvim/screen.rb +9 -12
  31. data/lib/ruvim/version.rb +1 -1
  32. data/lib/ruvim.rb +10 -0
  33. data/test/app_completion_test.rb +25 -0
  34. data/test/app_scenario_test.rb +169 -0
  35. data/test/cli_test.rb +14 -0
  36. data/test/clipboard_test.rb +67 -0
  37. data/test/command_line_test.rb +118 -0
  38. data/test/config_dsl_test.rb +87 -0
  39. data/test/display_width_test.rb +41 -0
  40. data/test/file_watcher_test.rb +197 -0
  41. data/test/follow_test.rb +199 -0
  42. data/test/git_blame_test.rb +713 -0
  43. data/test/highlighter_test.rb +44 -0
  44. data/test/indent_test.rb +86 -0
  45. data/test/rich_view_test.rb +256 -0
  46. data/test/search_option_test.rb +19 -0
  47. data/test/test_helper.rb +9 -0
  48. metadata +17 -1
data/lib/ruvim/app.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "fileutils"
5
+ require_relative "file_watcher"
5
6
 
6
7
  module RuVim
7
8
  class App
@@ -10,7 +11,7 @@ module RuVim
10
11
  ASYNC_FILE_READ_CHUNK_BYTES = 1 * 1024 * 1024
11
12
  ASYNC_FILE_EVENT_FLUSH_BYTES = 4 * 1024 * 1024
12
13
 
13
- def initialize(path: nil, paths: nil, stdin: STDIN, ui_stdin: nil, stdin_stream_mode: false, stdout: STDOUT, pre_config_actions: [], startup_actions: [], clean: false, skip_user_config: false, config_path: nil, readonly: false, diff_mode: false, quickfix_errorfile: nil, session_file: nil, nomodifiable: false, restricted: false, verbose_level: 0, verbose_io: STDERR, startup_time_path: nil, startup_open_layout: nil, startup_open_count: nil)
14
+ def initialize(path: nil, paths: nil, stdin: STDIN, ui_stdin: nil, stdin_stream_mode: false, stdout: STDOUT, pre_config_actions: [], startup_actions: [], clean: false, skip_user_config: false, config_path: nil, readonly: false, diff_mode: false, quickfix_errorfile: nil, session_file: nil, nomodifiable: false, follow: false, restricted: false, verbose_level: 0, verbose_io: STDERR, startup_time_path: nil, startup_open_layout: nil, startup_open_count: nil)
14
15
  startup_paths = Array(paths || path).compact
15
16
  @ui_stdin = ui_stdin || stdin
16
17
  @stdin_stream_mode = !!stdin_stream_mode
@@ -27,6 +28,7 @@ module RuVim
27
28
  @stream_buffer_id = nil
28
29
  @stream_stop_requested = false
29
30
  @async_file_loads = {}
31
+ @follow_watchers = {}
30
32
  @cmdline_history = Hash.new { |h, k| h[k] = [] }
31
33
  @cmdline_history_index = nil
32
34
  @cmdline_completion = nil
@@ -43,6 +45,7 @@ module RuVim
43
45
  @startup_quickfix_errorfile = quickfix_errorfile
44
46
  @startup_session_file = session_file
45
47
  @startup_nomodifiable = nomodifiable
48
+ @startup_follow = follow
46
49
  @restricted_mode = restricted
47
50
  @verbose_level = verbose_level.to_i
48
51
  @verbose_io = verbose_io
@@ -56,6 +59,8 @@ module RuVim
56
59
  @editor.open_path_handler = method(:open_path_with_large_file_support)
57
60
  @editor.keymap_manager = @keymaps
58
61
  @editor.app_action_handler = method(:handle_editor_app_action)
62
+ @editor.git_stream_handler = method(:start_git_stream_command)
63
+ @editor.git_stream_stop_handler = method(:stop_git_stream!)
59
64
  load_command_line_history!
60
65
 
61
66
  startup_mark("init.start")
@@ -119,10 +124,17 @@ module RuVim
119
124
  @needs_redraw = true
120
125
 
121
126
  # Batch insert-mode keystrokes to avoid per-char rendering during paste
122
- while @editor.mode == :insert && @input.has_pending_input?
123
- batch_key = @input.read_key(timeout: 0, esc_timeout: 0)
124
- break unless batch_key
125
- handle_key(batch_key)
127
+ if @editor.mode == :insert && @input.has_pending_input?
128
+ @paste_batch = true
129
+ begin
130
+ while @editor.mode == :insert && @input.has_pending_input?
131
+ batch_key = @input.read_key(timeout: 0, esc_timeout: 0)
132
+ break unless batch_key
133
+ handle_key(batch_key)
134
+ end
135
+ ensure
136
+ @paste_batch = false
137
+ end
126
138
  end
127
139
  end
128
140
  end
@@ -352,10 +364,27 @@ module RuVim
352
364
  register_ex_unless(ex, "d", call: :ex_delete_lines, aliases: %w[delete], desc: "Delete lines", nargs: :any)
353
365
  register_ex_unless(ex, "y", call: :ex_yank_lines, aliases: %w[yank], desc: "Yank lines", nargs: :any)
354
366
  register_ex_unless(ex, "rich", call: :ex_rich, desc: "Open/close Rich View", nargs: :maybe_one)
367
+ register_ex_unless(ex, "follow", call: ->(ctx, **) { ctx.editor.invoke_app_action(:follow_toggle) }, desc: "Toggle file follow mode", nargs: 0)
368
+ register_ex_unless(ex, "nohlsearch", call: ->(ctx, **) { ctx.editor.suppress_hlsearch! }, aliases: %w[noh nohl nohlsearc nohlsear nohlsea nohlse nohls], desc: "Temporarily clear search highlight", nargs: 0)
369
+ register_ex_unless(ex, "filter", call: :ex_filter, desc: "Filter lines matching search pattern", nargs: :any)
370
+ register_internal_unless(cmd, "search.filter", call: :search_filter, desc: "Filter lines matching search pattern")
355
371
  register_internal_unless(cmd, "rich.toggle", call: :rich_toggle, desc: "Toggle Rich View")
372
+ register_internal_unless(cmd, "rich.close_buffer", call: :rich_view_close_buffer, desc: "Close rich view buffer")
356
373
  register_internal_unless(cmd, "quickfix.next", call: :ex_cnext, desc: "Next quickfix item")
357
374
  register_internal_unless(cmd, "quickfix.prev", call: :ex_cprev, desc: "Prev quickfix item")
358
375
  register_internal_unless(cmd, "quickfix.open", call: :ex_copen, desc: "Open quickfix list")
376
+
377
+ register_internal_unless(cmd, "git.blame", call: :git_blame, desc: "Open git blame buffer")
378
+ register_internal_unless(cmd, "git.blame.prev", call: :git_blame_prev, desc: "Blame at parent commit")
379
+ register_internal_unless(cmd, "git.blame.back", call: :git_blame_back, desc: "Restore previous blame")
380
+ register_internal_unless(cmd, "git.blame.commit", call: :git_blame_commit, desc: "Show commit details")
381
+ register_internal_unless(cmd, "git.command_mode", call: :enter_git_command_mode, desc: "Enter Git command-line mode")
382
+ register_internal_unless(cmd, "git.close_buffer", call: :git_close_buffer, desc: "Close git buffer")
383
+ register_internal_unless(cmd, "git.status.open_file", call: :git_status_open_file, desc: "Open file from git status")
384
+ register_internal_unless(cmd, "git.diff.open_file", call: :git_diff_open_file, desc: "Open file from git diff")
385
+ register_internal_unless(cmd, "git.branch.checkout", call: :git_branch_checkout, desc: "Checkout branch under cursor")
386
+ register_internal_unless(cmd, "git.commit.execute", call: :git_commit_execute, desc: "Execute git commit")
387
+ register_ex_unless(ex, "git", call: :ex_git, desc: "Git subcommand dispatcher", nargs: :any)
359
388
  end
360
389
 
361
390
  def bind_default_keys!
@@ -448,6 +477,8 @@ module RuVim
448
477
  @keymaps.bind(:normal, "g#", "search.word_backward_partial")
449
478
  @keymaps.bind(:normal, "gf", "file.goto_under_cursor")
450
479
  @keymaps.bind(:normal, "gr", "rich.toggle")
480
+ @keymaps.bind(:normal, "g/", "search.filter")
481
+ @keymaps.bind(:normal, ["<C-g>"], "git.command_mode")
451
482
  @keymaps.bind(:normal, "Q", "quickfix.open")
452
483
  @keymaps.bind(:normal, ["]", "q"], "quickfix.next")
453
484
  @keymaps.bind(:normal, ["[", "q"], "quickfix.prev")
@@ -541,6 +572,8 @@ module RuVim
541
572
  linewise: !!(kwargs[:linewise] || kwargs["linewise"]),
542
573
  repeat_token: (kwargs[:repeat_token] || kwargs["repeat_token"]).to_s
543
574
  )
575
+ when :follow_toggle
576
+ ex_follow_toggle
544
577
  else
545
578
  raise RuVim::CommandError, "Unknown app action: #{name}"
546
579
  end
@@ -844,6 +877,10 @@ module RuVim
844
877
 
845
878
  def handle_list_window_enter
846
879
  buffer = @editor.current_buffer
880
+ return handle_filter_buffer_enter if buffer.kind == :filter
881
+ return handle_git_status_enter if buffer.kind == :git_status
882
+ return handle_git_diff_enter if buffer.kind == :git_diff || buffer.kind == :git_log
883
+ return handle_git_branch_enter if buffer.kind == :git_branch
847
884
  return false unless buffer.kind == :quickfix || buffer.kind == :location_list
848
885
 
849
886
  item_index = @editor.current_window.cursor_y - 2
@@ -885,6 +922,47 @@ module RuVim
885
922
  true
886
923
  end
887
924
 
925
+ def handle_filter_buffer_enter
926
+ buffer = @editor.current_buffer
927
+ origins = buffer.options["filter_origins"]
928
+ return false unless origins
929
+
930
+ row = @editor.current_window.cursor_y
931
+ origin = origins[row]
932
+ unless origin
933
+ @editor.echo_error("No filter item on this line")
934
+ return true
935
+ end
936
+
937
+ target_buffer_id = origin[:buffer_id]
938
+ target_row = origin[:row]
939
+ filter_buf_id = buffer.id
940
+
941
+ @editor.delete_buffer(filter_buf_id)
942
+ target_buf = @editor.buffers[target_buffer_id]
943
+ if target_buf
944
+ @editor.switch_to_buffer(target_buffer_id) unless @editor.current_buffer.id == target_buffer_id
945
+ @editor.current_window.cursor_y = [target_row, target_buf.lines.length - 1].min
946
+ @editor.current_window.cursor_x = 0
947
+ end
948
+ true
949
+ end
950
+
951
+ def handle_git_status_enter
952
+ @dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.status.open_file"))
953
+ true
954
+ end
955
+
956
+ def handle_git_diff_enter
957
+ @dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.diff.open_file"))
958
+ true
959
+ end
960
+
961
+ def handle_git_branch_enter
962
+ @dispatcher.dispatch(@editor, CommandInvocation.new(id: "git.branch.checkout"))
963
+ true
964
+ end
965
+
888
966
  def arrow_key?(key)
889
967
  %i[left right up down].include?(key)
890
968
  end
@@ -946,6 +1024,7 @@ module RuVim
946
1024
  when :ctrl_w then "<C-w>"
947
1025
  when :ctrl_l then "<C-l>"
948
1026
  when :ctrl_c then "<C-c>"
1027
+ when :ctrl_g then "<C-g>"
949
1028
  when :left then "<Left>"
950
1029
  when :right then "<Right>"
951
1030
  when :up then "<Up>"
@@ -1072,7 +1151,12 @@ module RuVim
1072
1151
  @jump_pending = nil
1073
1152
  @macro_record_pending = false
1074
1153
  @macro_play_pending = false
1075
- @editor.clear_message
1154
+ buf = @editor.current_buffer
1155
+ if buf && @follow_watchers[buf.id]
1156
+ stop_follow!(buf)
1157
+ else
1158
+ @editor.clear_message
1159
+ end
1076
1160
  end
1077
1161
 
1078
1162
  def suspend_to_shell
@@ -1290,7 +1374,7 @@ module RuVim
1290
1374
 
1291
1375
  def handle_operator_pending_key(token)
1292
1376
  op = @operator_pending
1293
- if %w[i a].include?(token) && !op[:motion_prefix]
1377
+ if %w[i a g].include?(token) && !op[:motion_prefix]
1294
1378
  @operator_pending[:motion_prefix] = token
1295
1379
  @editor.echo("#{op[:name].to_s[0]}#{token}")
1296
1380
  return
@@ -1846,6 +1930,7 @@ module RuVim
1846
1930
  end
1847
1931
 
1848
1932
  def apply_insert_autoindent(row, x, previous_row:)
1933
+ return x if @paste_batch
1849
1934
  buf = @editor.current_buffer
1850
1935
  win = @editor.current_window
1851
1936
  return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
@@ -2307,6 +2392,10 @@ module RuVim
2307
2392
  return option_completion_candidates(prefix)
2308
2393
  end
2309
2394
 
2395
+ if cmd == "git"
2396
+ return Git::Handler::GIT_SUBCOMMANDS.keys.sort.select { |s| s.start_with?(prefix) }
2397
+ end
2398
+
2310
2399
  []
2311
2400
  end
2312
2401
 
@@ -2492,6 +2581,17 @@ module RuVim
2492
2581
  @editor.echo("readonly: #{buf.display_name}")
2493
2582
  end
2494
2583
 
2584
+ def apply_startup_follow!
2585
+ buf = @editor.current_buffer
2586
+ return unless buf&.file_buffer?
2587
+ return if @follow_watchers[buf.id]
2588
+
2589
+ win = @editor.current_window
2590
+ win.cursor_y = buf.line_count - 1
2591
+ win.clamp_to_buffer(buf)
2592
+ start_follow!(buf)
2593
+ end
2594
+
2495
2595
  def apply_startup_nomodifiable!
2496
2596
  buf = @editor.current_buffer
2497
2597
  return unless buf&.file_buffer?
@@ -2533,6 +2633,7 @@ module RuVim
2533
2633
  @editor.open_path(first)
2534
2634
  apply_startup_readonly! if @startup_readonly
2535
2635
  apply_startup_nomodifiable! if @startup_nomodifiable
2636
+ apply_startup_follow! if @startup_follow
2536
2637
 
2537
2638
  case @startup_open_layout
2538
2639
  when :horizontal
@@ -2548,7 +2649,10 @@ module RuVim
2548
2649
  @editor.tabnext(-(@editor.tabpage_count - 1))
2549
2650
  else
2550
2651
  # Load remaining files as buffers (Vim-like behavior).
2551
- rest.each { |p| @editor.add_buffer_from_file(p) }
2652
+ rest.each do |p|
2653
+ buf = @editor.add_buffer_from_file(p)
2654
+ start_follow!(buf) if @startup_follow
2655
+ end
2552
2656
  end
2553
2657
  end
2554
2658
 
@@ -2570,12 +2674,14 @@ module RuVim
2570
2674
  @editor.open_path(path)
2571
2675
  apply_startup_readonly! if @startup_readonly
2572
2676
  apply_startup_nomodifiable! if @startup_nomodifiable
2677
+ apply_startup_follow! if @startup_follow
2573
2678
  end
2574
2679
 
2575
2680
  def open_path_in_tab!(path)
2576
2681
  @editor.tabnew(path:)
2577
2682
  apply_startup_readonly! if @startup_readonly
2578
2683
  apply_startup_nomodifiable! if @startup_nomodifiable
2684
+ apply_startup_follow! if @startup_follow
2579
2685
  end
2580
2686
 
2581
2687
  def open_path_with_large_file_support(path)
@@ -2808,12 +2914,30 @@ module RuVim
2808
2914
  end
2809
2915
  @editor.echo_error("[stdin] stream error: #{event[:error]}")
2810
2916
  changed = true
2917
+ when :follow_data
2918
+ changed = apply_follow_chunk!(event[:buffer_id], event[:data]) || changed
2919
+ when :follow_truncated
2920
+ if (buf = @editor.buffers[event[:buffer_id]])
2921
+ @editor.echo("[follow] file truncated: #{buf.display_name}")
2922
+ changed = true
2923
+ end
2924
+ when :follow_deleted
2925
+ if (buf = @editor.buffers[event[:buffer_id]])
2926
+ @editor.echo("[follow] file deleted, waiting for re-creation: #{buf.display_name}")
2927
+ changed = true
2928
+ end
2811
2929
  when :file_data
2812
2930
  changed = apply_async_file_chunk!(event[:buffer_id], event[:data]) || changed
2813
2931
  when :file_eof
2814
2932
  changed = finish_async_file_load!(event[:buffer_id], ended_with_newline: event[:ended_with_newline]) || changed
2815
2933
  when :file_error
2816
2934
  changed = fail_async_file_load!(event[:buffer_id], event[:error]) || changed
2935
+ when :git_cmd_data
2936
+ changed = apply_git_stream_chunk!(event[:buffer_id], event[:data]) || changed
2937
+ when :git_cmd_eof
2938
+ changed = finish_git_stream!(event[:buffer_id]) || changed
2939
+ when :git_cmd_error
2940
+ changed = fail_git_stream!(event[:buffer_id], event[:error]) || changed
2817
2941
  end
2818
2942
  end
2819
2943
  rescue ThreadError
@@ -2902,6 +3026,99 @@ module RuVim
2902
3026
  nil
2903
3027
  end
2904
3028
 
3029
+ def ex_follow_toggle
3030
+ buf = @editor.current_buffer
3031
+ raise RuVim::CommandError, "No file associated with buffer" unless buf.path
3032
+
3033
+ if @follow_watchers[buf.id]
3034
+ stop_follow!(buf)
3035
+ else
3036
+ raise RuVim::CommandError, "Buffer has unsaved changes" if buf.modified?
3037
+ start_follow!(buf)
3038
+ end
3039
+ end
3040
+
3041
+ def start_follow!(buf)
3042
+ ensure_stream_event_queue!
3043
+ if buf.path && File.exist?(buf.path)
3044
+ data = File.binread(buf.path)
3045
+ if data.end_with?("\n") && buf.lines.last.to_s != ""
3046
+ following_wins = @editor.windows.values.select do |w|
3047
+ w.buffer_id == buf.id && stream_window_following_end?(w, buf)
3048
+ end
3049
+ buf.append_stream_text!("\n")
3050
+ following_wins.each { |w| move_window_to_stream_end!(w, buf) }
3051
+ end
3052
+ end
3053
+ buffer_id = buf.id
3054
+ watcher = FileWatcher.create(buf.path) do |type, data|
3055
+ case type
3056
+ when :data
3057
+ @stream_event_queue << { type: :follow_data, buffer_id: buffer_id, data: data }
3058
+ when :truncated
3059
+ @stream_event_queue << { type: :follow_truncated, buffer_id: buffer_id }
3060
+ when :deleted
3061
+ @stream_event_queue << { type: :follow_deleted, buffer_id: buffer_id }
3062
+ end
3063
+ notify_signal_wakeup
3064
+ end
3065
+ watcher.start
3066
+ @follow_watchers[buf.id] = watcher
3067
+ buf.stream_state = :live
3068
+ buf.follow_backend = watcher.backend
3069
+ @editor.echo("[follow] #{buf.display_name}")
3070
+ end
3071
+
3072
+ def stop_follow!(buf)
3073
+ watcher = @follow_watchers.delete(buf.id)
3074
+ watcher&.stop
3075
+ # Remove trailing empty line added as sentinel by start_follow!
3076
+ if buf.line_count > 1 && buf.lines.last.to_s == ""
3077
+ buf.lines.pop
3078
+ last = buf.line_count - 1
3079
+ @editor.windows.each_value do |win|
3080
+ next unless win.buffer_id == buf.id
3081
+ win.cursor_y = last if win.cursor_y > last
3082
+ end
3083
+ end
3084
+ buf.stream_state = nil
3085
+ buf.follow_backend = nil
3086
+ @editor.echo("[follow] stopped")
3087
+ end
3088
+
3089
+ def apply_follow_chunk!(buffer_id, text)
3090
+ return false if text.to_s.empty?
3091
+
3092
+ buf = @editor.buffers[buffer_id]
3093
+ return false unless buf
3094
+
3095
+ follow_window_ids = @editor.windows.values.filter_map do |win|
3096
+ next unless win.buffer_id == buf.id
3097
+ next unless stream_window_following_end?(win, buf)
3098
+
3099
+ win.id
3100
+ end
3101
+
3102
+ buf.append_stream_text!(text)
3103
+
3104
+ follow_window_ids.each do |win_id|
3105
+ win = @editor.windows[win_id]
3106
+ move_window_to_stream_end!(win, buf) if win
3107
+ end
3108
+
3109
+ true
3110
+ end
3111
+
3112
+ def shutdown_follow_watchers!
3113
+ watchers = @follow_watchers
3114
+ @follow_watchers = {}
3115
+ watchers.each_value do |watcher|
3116
+ watcher.stop
3117
+ rescue StandardError
3118
+ nil
3119
+ end
3120
+ end
3121
+
2905
3122
  def shutdown_async_file_loaders!
2906
3123
  loaders = @async_file_loads
2907
3124
  @async_file_loads = {}
@@ -2924,6 +3141,7 @@ module RuVim
2924
3141
 
2925
3142
  def shutdown_background_readers!
2926
3143
  shutdown_stream_reader!
3144
+ shutdown_follow_watchers!
2927
3145
  shutdown_async_file_loaders!
2928
3146
  end
2929
3147
 
@@ -2940,6 +3158,70 @@ module RuVim
2940
3158
  @stream_event_queue ||= Queue.new
2941
3159
  end
2942
3160
 
3161
+ def apply_git_stream_chunk!(buffer_id, text)
3162
+ return false if text.to_s.empty?
3163
+
3164
+ buf = @editor.buffers[buffer_id]
3165
+ return false unless buf
3166
+
3167
+ buf.append_stream_text!(text)
3168
+ true
3169
+ end
3170
+
3171
+ def finish_git_stream!(buffer_id)
3172
+ @git_stream_ios&.delete(buffer_id)
3173
+ @git_stream_threads&.delete(buffer_id)
3174
+ buf = @editor.buffers[buffer_id]
3175
+ return false unless buf
3176
+
3177
+ # Remove trailing empty line if present
3178
+ if buf.lines.length > 1 && buf.lines[-1] == ""
3179
+ buf.lines.pop
3180
+ end
3181
+ line_count = buf.line_count
3182
+ @editor.echo("#{buf.name} #{line_count} lines")
3183
+ true
3184
+ end
3185
+
3186
+ def fail_git_stream!(buffer_id, error)
3187
+ @git_stream_ios&.delete(buffer_id)
3188
+ @git_stream_threads&.delete(buffer_id)
3189
+ buf = @editor.buffers[buffer_id]
3190
+ @editor.echo_error("git stream error: #{error}") if buf
3191
+ true
3192
+ end
3193
+
3194
+ def start_git_stream_command(buffer_id, cmd, root)
3195
+ ensure_stream_event_queue!
3196
+ @git_stream_ios ||= {}
3197
+ @git_stream_threads ||= {}
3198
+ queue = @stream_event_queue
3199
+ ios = @git_stream_ios
3200
+ @git_stream_threads[buffer_id] = Thread.new do
3201
+ IO.popen(cmd, chdir: root, err: [:child, :out]) do |io|
3202
+ ios[buffer_id] = io
3203
+ while (chunk = io.read(4096))
3204
+ queue << { type: :git_cmd_data, buffer_id: buffer_id, data: Buffer.decode_text(chunk) }
3205
+ notify_signal_wakeup
3206
+ end
3207
+ end
3208
+ ios.delete(buffer_id)
3209
+ queue << { type: :git_cmd_eof, buffer_id: buffer_id }
3210
+ notify_signal_wakeup
3211
+ rescue StandardError => e
3212
+ ios.delete(buffer_id)
3213
+ queue << { type: :git_cmd_error, buffer_id: buffer_id, error: e.message.to_s }
3214
+ notify_signal_wakeup
3215
+ end
3216
+ end
3217
+
3218
+ def stop_git_stream!(buffer_id)
3219
+ io = @git_stream_ios&.delete(buffer_id)
3220
+ io&.close
3221
+ rescue IOError
3222
+ # already closed
3223
+ end
3224
+
2943
3225
  def move_cursor_to_line(line_number)
2944
3226
  win = @editor.current_window
2945
3227
  buf = @editor.current_buffer
data/lib/ruvim/buffer.rb CHANGED
@@ -6,7 +6,19 @@ module RuVim
6
6
  attr_accessor :path
7
7
  attr_reader :options
8
8
  attr_writer :modified
9
- attr_accessor :stream_state, :loading_state
9
+ attr_accessor :stream_state, :loading_state, :follow_backend
10
+
11
+ def stream_status
12
+ return nil unless @stream_state
13
+
14
+ if @kind == :stream
15
+ "stdin/#{@stream_state}"
16
+ elsif @follow_backend == :inotify
17
+ "follow/i"
18
+ else
19
+ "follow"
20
+ end
21
+ end
10
22
  attr_accessor :lang_module
11
23
 
12
24
  def self.from_file(id:, path:)
@@ -87,7 +99,7 @@ module RuVim
87
99
  end
88
100
 
89
101
  def modifiable?
90
- @modifiable && @loading_state != :live
102
+ @modifiable && @loading_state != :live && @stream_state != :live
91
103
  end
92
104
 
93
105
  def modifiable=(value)
data/lib/ruvim/cli.rb CHANGED
@@ -16,6 +16,7 @@ module RuVim
16
16
  :no_swap,
17
17
  :nomodifiable,
18
18
  :restricted_mode,
19
+ :follow,
19
20
  :verbose_level,
20
21
  :startup_time_path,
21
22
  :startup_open_layout,
@@ -70,6 +71,7 @@ module RuVim
70
71
  quickfix_errorfile: opts.quickfix_errorfile,
71
72
  session_file: opts.session_file,
72
73
  nomodifiable: opts.nomodifiable,
74
+ follow: opts.follow,
73
75
  restricted: opts.restricted_mode,
74
76
  verbose_level: opts.verbose_level,
75
77
  verbose_io: stderr,
@@ -101,6 +103,7 @@ module RuVim
101
103
  no_swap: false,
102
104
  nomodifiable: false,
103
105
  restricted_mode: false,
106
+ follow: false,
104
107
  verbose_level: 0,
105
108
  startup_time_path: nil,
106
109
  startup_open_layout: nil,
@@ -159,6 +162,8 @@ module RuVim
159
162
  end
160
163
  when "-n"
161
164
  opts.no_swap = true
165
+ when "-f"
166
+ opts.follow = true
162
167
  when "-M"
163
168
  opts.nomodifiable = true
164
169
  when "-Z"
@@ -240,6 +245,7 @@ module RuVim
240
245
  -h, --help Show this help
241
246
  -v, --version Show version
242
247
  --clean Start without user config and ftplugin
248
+ -f Open file in follow mode (tail -f style)
243
249
  -R Open file readonly (disallow :w on current buffer)
244
250
  -d Diff mode requested (compat placeholder; not implemented yet)
245
251
  -q {errorfile} Quickfix startup placeholder (not implemented yet)
data/lib/ruvim/editor.rb CHANGED
@@ -71,7 +71,7 @@ module RuVim
71
71
  ].freeze
72
72
 
73
73
  attr_reader :buffers, :windows, :layout_tree
74
- attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :restricted_mode, :current_window_view_height_hint, :stdin_stream_stop_handler, :open_path_handler, :keymap_manager, :app_action_handler
74
+ attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :restricted_mode, :current_window_view_height_hint, :stdin_stream_stop_handler, :open_path_handler, :keymap_manager, :app_action_handler, :git_stream_handler, :git_stream_stop_handler
75
75
 
76
76
  def initialize
77
77
  @buffers = {}
@@ -100,6 +100,7 @@ module RuVim
100
100
  @global_options = default_global_options
101
101
  @command_line = CommandLine.new
102
102
  @last_search = nil
103
+ @hlsearch_suppressed = false
103
104
  @last_find = nil
104
105
  @registers = {}
105
106
  @active_register_name = nil
@@ -152,6 +153,15 @@ module RuVim
152
153
 
153
154
  def set_last_search(pattern:, direction:)
154
155
  @last_search = { pattern: pattern.to_s, direction: direction.to_sym }
156
+ @hlsearch_suppressed = false
157
+ end
158
+
159
+ def suppress_hlsearch!
160
+ @hlsearch_suppressed = true
161
+ end
162
+
163
+ def hlsearch_suppressed?
164
+ @hlsearch_suppressed
155
165
  end
156
166
 
157
167
  def set_last_find(char:, direction:, till:)
@@ -266,6 +276,7 @@ module RuVim
266
276
  ".tsx" => "typescriptreact",
267
277
  ".jsx" => "javascriptreact",
268
278
  ".json" => "json",
279
+ ".jsonl" => "jsonl",
269
280
  ".yml" => "yaml",
270
281
  ".yaml" => "yaml",
271
282
  ".md" => "markdown",