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.
- checksums.yaml +4 -4
- data/AGENTS.md +18 -6
- data/README.md +15 -1
- data/docs/binding.md +16 -0
- data/docs/command.md +78 -4
- data/docs/config.md +10 -2
- data/docs/spec.md +60 -9
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +18 -8
- data/lib/ruvim/app.rb +290 -8
- data/lib/ruvim/buffer.rb +14 -2
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/editor.rb +12 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +176 -42
- data/lib/ruvim/highlighter.rb +3 -1
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +34 -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.rb +16 -0
- data/lib/ruvim/screen.rb +9 -12
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim.rb +10 -0
- data/test/app_completion_test.rb +25 -0
- data/test/app_scenario_test.rb +169 -0
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/display_width_test.rb +41 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +44 -0
- data/test/indent_test.rb +86 -0
- data/test/rich_view_test.rb +256 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- 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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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
|
|
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",
|