ruvim 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +23 -0
- data/docs/command.md +85 -0
- data/docs/config.md +2 -2
- data/docs/done.md +21 -0
- data/docs/spec.md +157 -12
- data/docs/todo.md +1 -5
- data/docs/vim_diff.md +94 -172
- data/lib/ruvim/app.rb +882 -69
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +455 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +890 -63
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +39 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/json.rb +18 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +503 -106
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +14 -0
- data/test/app_completion_test.rb +73 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +729 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/dispatcher_test.rb +322 -0
- data/test/editor_register_test.rb +23 -0
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +478 -0
- data/test/screen_test.rb +304 -0
- metadata +33 -2
data/lib/ruvim/editor.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class Editor
|
|
3
5
|
OPTION_DEFS = {
|
|
@@ -45,20 +47,36 @@ module RuVim
|
|
|
45
47
|
"expandtab" => { default_scope: :buffer, type: :bool, default: false },
|
|
46
48
|
"shiftwidth" => { default_scope: :buffer, type: :int, default: 2 },
|
|
47
49
|
"softtabstop" => { default_scope: :buffer, type: :int, default: 0 },
|
|
48
|
-
"autoindent" => { default_scope: :buffer, type: :bool, default:
|
|
49
|
-
"smartindent" => { default_scope: :buffer, type: :bool, default:
|
|
50
|
+
"autoindent" => { default_scope: :buffer, type: :bool, default: true },
|
|
51
|
+
"smartindent" => { default_scope: :buffer, type: :bool, default: true },
|
|
50
52
|
"iskeyword" => { default_scope: :buffer, type: :string, default: nil },
|
|
51
53
|
"tabstop" => { default_scope: :buffer, type: :int, default: 2 },
|
|
52
|
-
"filetype" => { default_scope: :buffer, type: :string, default: nil }
|
|
54
|
+
"filetype" => { default_scope: :buffer, type: :string, default: nil },
|
|
55
|
+
"onsavehook" => { default_scope: :buffer, type: :bool, default: true },
|
|
56
|
+
"grepprg" => { default_scope: :global, type: :string, default: "grep -nH" },
|
|
57
|
+
"grepformat" => { default_scope: :global, type: :string, default: "%f:%l:%m" }
|
|
53
58
|
}.freeze
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
SHEBANG_FILETYPE_RULES = [
|
|
60
|
+
[/\Aruby(?:\d+(?:\.\d+)*)?\z/, "ruby"],
|
|
61
|
+
[/\Apython(?:\d+(?:\.\d+)*)?\z/, "python"],
|
|
62
|
+
["node", "javascript"],
|
|
63
|
+
["nodejs", "javascript"],
|
|
64
|
+
["deno", "javascript"],
|
|
65
|
+
["bash", "sh"],
|
|
66
|
+
["sh", "sh"],
|
|
67
|
+
["zsh", "sh"],
|
|
68
|
+
["ksh", "sh"],
|
|
69
|
+
["dash", "sh"],
|
|
70
|
+
[/\Aperl(?:\d+(?:\.\d+)*)?\z/, "perl"]
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
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
|
|
57
75
|
|
|
58
76
|
def initialize
|
|
59
77
|
@buffers = {}
|
|
60
78
|
@windows = {}
|
|
61
|
-
@
|
|
79
|
+
@layout_tree = nil
|
|
62
80
|
@tabpages = []
|
|
63
81
|
@current_tabpage_index = nil
|
|
64
82
|
@next_tabpage_id = 1
|
|
@@ -68,7 +86,6 @@ module RuVim
|
|
|
68
86
|
@current_window_id = nil
|
|
69
87
|
@alternate_buffer_id = nil
|
|
70
88
|
@mode = :normal
|
|
71
|
-
@window_layout = :single
|
|
72
89
|
@message = ""
|
|
73
90
|
@message_kind = :info
|
|
74
91
|
@message_deadline = nil
|
|
@@ -76,6 +93,10 @@ module RuVim
|
|
|
76
93
|
@restricted_mode = false
|
|
77
94
|
@current_window_view_height_hint = 1
|
|
78
95
|
@running = true
|
|
96
|
+
@stdin_stream_stop_handler = nil
|
|
97
|
+
@open_path_handler = nil
|
|
98
|
+
@keymap_manager = nil
|
|
99
|
+
@app_action_handler = nil
|
|
79
100
|
@global_options = default_global_options
|
|
80
101
|
@command_line = CommandLine.new
|
|
81
102
|
@last_search = nil
|
|
@@ -89,8 +110,12 @@ module RuVim
|
|
|
89
110
|
@macros = {}
|
|
90
111
|
@macro_recording = nil
|
|
91
112
|
@visual_state = nil
|
|
113
|
+
@rich_state = nil
|
|
92
114
|
@quickfix_list = { items: [], index: nil }
|
|
93
115
|
@location_lists = Hash.new { |h, k| h[k] = { items: [], index: nil } }
|
|
116
|
+
@arglist = []
|
|
117
|
+
@arglist_index = 0
|
|
118
|
+
@hit_enter_lines = nil
|
|
94
119
|
end
|
|
95
120
|
|
|
96
121
|
def running?
|
|
@@ -141,6 +166,22 @@ module RuVim
|
|
|
141
166
|
@buffers.fetch(current_window.buffer_id)
|
|
142
167
|
end
|
|
143
168
|
|
|
169
|
+
def stdin_stream_stop_or_cancel!
|
|
170
|
+
handler = @stdin_stream_stop_handler
|
|
171
|
+
return false unless handler
|
|
172
|
+
|
|
173
|
+
handler.call
|
|
174
|
+
true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def invoke_app_action(name, **kwargs)
|
|
178
|
+
handler = @app_action_handler
|
|
179
|
+
return false unless handler
|
|
180
|
+
|
|
181
|
+
handler.call(name.to_sym, **kwargs)
|
|
182
|
+
true
|
|
183
|
+
end
|
|
184
|
+
|
|
144
185
|
def option_def(name)
|
|
145
186
|
OPTION_DEFS[name.to_s]
|
|
146
187
|
end
|
|
@@ -213,7 +254,7 @@ module RuVim
|
|
|
213
254
|
base = File.basename(p)
|
|
214
255
|
return "ruby" if %w[Gemfile Rakefile Guardfile].include?(base)
|
|
215
256
|
|
|
216
|
-
{
|
|
257
|
+
ext_ft = {
|
|
217
258
|
".rb" => "ruby",
|
|
218
259
|
".rake" => "ruby",
|
|
219
260
|
".ru" => "ruby",
|
|
@@ -231,8 +272,16 @@ module RuVim
|
|
|
231
272
|
".txt" => "text",
|
|
232
273
|
".html" => "html",
|
|
233
274
|
".css" => "css",
|
|
234
|
-
".sh" => "sh"
|
|
275
|
+
".sh" => "sh",
|
|
276
|
+
".tsv" => "tsv",
|
|
277
|
+
".csv" => "csv",
|
|
278
|
+
".scm" => "scheme",
|
|
279
|
+
".ss" => "scheme",
|
|
280
|
+
".sld" => "scheme"
|
|
235
281
|
}[File.extname(base).downcase]
|
|
282
|
+
return ext_ft if ext_ft
|
|
283
|
+
|
|
284
|
+
detect_filetype_from_shebang(p)
|
|
236
285
|
end
|
|
237
286
|
|
|
238
287
|
def registers
|
|
@@ -481,6 +530,53 @@ module RuVim
|
|
|
481
530
|
end
|
|
482
531
|
end
|
|
483
532
|
|
|
533
|
+
def rich_state
|
|
534
|
+
@rich_state
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def rich_mode?
|
|
538
|
+
@mode == :rich
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def enter_rich_mode(format:, delimiter:)
|
|
542
|
+
@mode = :rich
|
|
543
|
+
@pending_count = nil
|
|
544
|
+
@rich_state = { format: format, delimiter: delimiter }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def exit_rich_mode
|
|
548
|
+
@rich_state = nil
|
|
549
|
+
enter_normal_mode
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def hit_enter_active?
|
|
553
|
+
@mode == :hit_enter
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def hit_enter_lines
|
|
557
|
+
@hit_enter_lines
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def enter_hit_enter_mode(lines)
|
|
561
|
+
@mode = :hit_enter
|
|
562
|
+
@hit_enter_lines = Array(lines)
|
|
563
|
+
@pending_count = nil
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def exit_hit_enter_mode
|
|
567
|
+
@hit_enter_lines = nil
|
|
568
|
+
enter_normal_mode
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def echo_multiline(lines)
|
|
572
|
+
lines = Array(lines)
|
|
573
|
+
if lines.length <= 1
|
|
574
|
+
echo(lines.first.to_s)
|
|
575
|
+
else
|
|
576
|
+
enter_hit_enter_mode(lines)
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
484
580
|
def add_empty_buffer(path: nil)
|
|
485
581
|
id = next_buffer_id
|
|
486
582
|
buffer = Buffer.new(id:, path:)
|
|
@@ -492,7 +588,7 @@ module RuVim
|
|
|
492
588
|
def add_virtual_buffer(kind:, name:, lines:, filetype: nil, readonly: true, modifiable: false)
|
|
493
589
|
id = next_buffer_id
|
|
494
590
|
buffer = Buffer.new(id:, lines:, kind:, name:, readonly:, modifiable:)
|
|
495
|
-
buffer
|
|
591
|
+
assign_filetype(buffer, filetype) if filetype
|
|
496
592
|
@buffers[id] = buffer
|
|
497
593
|
buffer
|
|
498
594
|
end
|
|
@@ -509,7 +605,17 @@ module RuVim
|
|
|
509
605
|
id = next_window_id
|
|
510
606
|
window = Window.new(id:, buffer_id:)
|
|
511
607
|
@windows[id] = window
|
|
512
|
-
|
|
608
|
+
leaf = { type: :window, id: id }
|
|
609
|
+
if @layout_tree.nil?
|
|
610
|
+
@layout_tree = leaf
|
|
611
|
+
else
|
|
612
|
+
# Append as sibling — used for initial bootstrap only
|
|
613
|
+
if @layout_tree[:type] == :window
|
|
614
|
+
@layout_tree = { type: :hsplit, children: [@layout_tree, leaf] }
|
|
615
|
+
else
|
|
616
|
+
@layout_tree[:children] << leaf
|
|
617
|
+
end
|
|
618
|
+
end
|
|
513
619
|
@current_window_id ||= id
|
|
514
620
|
ensure_initial_tabpage!
|
|
515
621
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
@@ -522,15 +628,17 @@ module RuVim
|
|
|
522
628
|
id = next_window_id
|
|
523
629
|
win = Window.new(id:, buffer_id: src.buffer_id)
|
|
524
630
|
@windows[id] = win
|
|
525
|
-
src_idx = @window_order.index(src.id) || (@window_order.length - 1)
|
|
526
|
-
insert_idx = (place.to_sym == :before ? src_idx : src_idx + 1)
|
|
527
|
-
@window_order.insert(insert_idx, win.id)
|
|
528
631
|
ensure_initial_tabpage!
|
|
529
632
|
win.cursor_x = src.cursor_x
|
|
530
633
|
win.cursor_y = src.cursor_y
|
|
531
634
|
win.row_offset = src.row_offset
|
|
532
635
|
win.col_offset = src.col_offset
|
|
533
|
-
|
|
636
|
+
|
|
637
|
+
split_type = (layout.to_sym == :vertical ? :vsplit : :hsplit)
|
|
638
|
+
new_leaf = { type: :window, id: win.id }
|
|
639
|
+
|
|
640
|
+
@layout_tree = tree_split_leaf(@layout_tree, src.id, split_type, new_leaf, place.to_sym)
|
|
641
|
+
|
|
534
642
|
@current_window_id = win.id
|
|
535
643
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
536
644
|
win
|
|
@@ -541,18 +649,21 @@ module RuVim
|
|
|
541
649
|
end
|
|
542
650
|
|
|
543
651
|
def close_window(id)
|
|
544
|
-
|
|
545
|
-
return nil if
|
|
546
|
-
return nil
|
|
652
|
+
leaves = tree_leaves(@layout_tree)
|
|
653
|
+
return nil if leaves.empty?
|
|
654
|
+
return nil if leaves.length <= 1
|
|
655
|
+
return nil unless leaves.include?(id)
|
|
547
656
|
|
|
548
657
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
549
|
-
idx =
|
|
658
|
+
idx = leaves.index(id) || 0
|
|
550
659
|
@windows.delete(id)
|
|
551
|
-
@window_order.delete(id)
|
|
552
660
|
@location_lists.delete(id)
|
|
553
|
-
|
|
554
|
-
@
|
|
555
|
-
|
|
661
|
+
|
|
662
|
+
@layout_tree = tree_remove(@layout_tree, id)
|
|
663
|
+
|
|
664
|
+
new_leaves = tree_leaves(@layout_tree)
|
|
665
|
+
@current_window_id = new_leaves[[idx, new_leaves.length - 1].min] if @current_window_id == id
|
|
666
|
+
@current_window_id ||= new_leaves.first
|
|
556
667
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
557
668
|
current_window
|
|
558
669
|
end
|
|
@@ -563,7 +674,8 @@ module RuVim
|
|
|
563
674
|
|
|
564
675
|
save_current_tabpage_state!
|
|
565
676
|
removed = @tabpages.delete_at(@current_tabpage_index)
|
|
566
|
-
|
|
677
|
+
removed_tree = removed && removed[:layout_tree]
|
|
678
|
+
tree_leaves(removed_tree).each do |wid|
|
|
567
679
|
@windows.delete(wid)
|
|
568
680
|
@location_lists.delete(wid)
|
|
569
681
|
end
|
|
@@ -581,42 +693,55 @@ module RuVim
|
|
|
581
693
|
end
|
|
582
694
|
|
|
583
695
|
def focus_next_window
|
|
584
|
-
|
|
696
|
+
order = window_order
|
|
697
|
+
return current_window if order.length <= 1
|
|
585
698
|
|
|
586
|
-
idx =
|
|
587
|
-
focus_window(
|
|
699
|
+
idx = order.index(@current_window_id) || 0
|
|
700
|
+
focus_window(order[(idx + 1) % order.length])
|
|
588
701
|
end
|
|
589
702
|
|
|
590
703
|
def focus_prev_window
|
|
591
|
-
|
|
704
|
+
order = window_order
|
|
705
|
+
return current_window if order.length <= 1
|
|
592
706
|
|
|
593
|
-
idx =
|
|
594
|
-
focus_window(
|
|
707
|
+
idx = order.index(@current_window_id) || 0
|
|
708
|
+
focus_window(order[(idx - 1) % order.length])
|
|
595
709
|
end
|
|
596
710
|
|
|
597
711
|
def focus_window_direction(dir)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
712
|
+
leaves = tree_leaves(@layout_tree)
|
|
713
|
+
return current_window if leaves.length <= 1
|
|
714
|
+
|
|
715
|
+
rects = tree_compute_rects(@layout_tree, top: 0.0, left: 0.0, height: 1.0, width: 1.0)
|
|
716
|
+
cur = rects[@current_window_id]
|
|
717
|
+
return current_window unless cur
|
|
718
|
+
|
|
719
|
+
best_id = nil
|
|
720
|
+
best_dist = Float::INFINITY
|
|
721
|
+
cur_cx = cur[:left] + cur[:width] / 2.0
|
|
722
|
+
cur_cy = cur[:top] + cur[:height] / 2.0
|
|
723
|
+
|
|
724
|
+
rects.each do |wid, r|
|
|
725
|
+
next if wid == @current_window_id
|
|
726
|
+
rcx = r[:left] + r[:width] / 2.0
|
|
727
|
+
rcy = r[:top] + r[:height] / 2.0
|
|
728
|
+
|
|
729
|
+
in_direction = case dir
|
|
730
|
+
when :left then rcx < cur_cx
|
|
731
|
+
when :right then rcx > cur_cx
|
|
732
|
+
when :up then rcy < cur_cy
|
|
733
|
+
when :down then rcy > cur_cy
|
|
734
|
+
end
|
|
735
|
+
next unless in_direction
|
|
736
|
+
|
|
737
|
+
dist = (rcx - cur_cx).abs + (rcy - cur_cy).abs
|
|
738
|
+
if dist < best_dist
|
|
739
|
+
best_dist = dist
|
|
740
|
+
best_id = wid
|
|
608
741
|
end
|
|
609
|
-
when :horizontal
|
|
610
|
-
if dir == :up
|
|
611
|
-
focus_prev_window
|
|
612
|
-
elsif dir == :down
|
|
613
|
-
focus_next_window
|
|
614
|
-
else
|
|
615
|
-
current_window
|
|
616
|
-
end
|
|
617
|
-
else
|
|
618
|
-
focus_next_window
|
|
619
742
|
end
|
|
743
|
+
|
|
744
|
+
best_id ? focus_window(best_id) : current_window
|
|
620
745
|
end
|
|
621
746
|
|
|
622
747
|
def switch_to_buffer(buffer_id)
|
|
@@ -634,9 +759,16 @@ module RuVim
|
|
|
634
759
|
end
|
|
635
760
|
|
|
636
761
|
def open_path(path)
|
|
762
|
+
handler = @open_path_handler
|
|
763
|
+
return handler.call(path) if handler
|
|
764
|
+
|
|
765
|
+
open_path_sync(path)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def open_path_sync(path)
|
|
637
769
|
buffer = add_buffer_from_file(path)
|
|
638
770
|
switch_to_buffer(buffer.id)
|
|
639
|
-
echo(path && File.exist?(path)
|
|
771
|
+
echo("[New File]") unless path && File.exist?(path)
|
|
640
772
|
buffer
|
|
641
773
|
end
|
|
642
774
|
|
|
@@ -672,7 +804,7 @@ module RuVim
|
|
|
672
804
|
current_buffer.replace_all_lines!(intro_lines)
|
|
673
805
|
current_buffer.configure_special!(kind: :intro, name: "[Intro]", readonly: true, modifiable: false)
|
|
674
806
|
current_buffer.modified = false
|
|
675
|
-
current_buffer
|
|
807
|
+
assign_filetype(current_buffer, "help")
|
|
676
808
|
current_window.cursor_x = 0
|
|
677
809
|
current_window.cursor_y = 0
|
|
678
810
|
current_window.row_offset = 0
|
|
@@ -685,7 +817,7 @@ module RuVim
|
|
|
685
817
|
return false unless current_buffer.intro_buffer?
|
|
686
818
|
|
|
687
819
|
current_buffer.become_normal_empty_buffer!
|
|
688
|
-
current_buffer
|
|
820
|
+
assign_filetype(current_buffer, nil)
|
|
689
821
|
current_window.cursor_x = 0
|
|
690
822
|
current_window.cursor_y = 0
|
|
691
823
|
current_window.row_offset = 0
|
|
@@ -745,7 +877,16 @@ module RuVim
|
|
|
745
877
|
end
|
|
746
878
|
|
|
747
879
|
def window_order
|
|
748
|
-
@
|
|
880
|
+
tree_leaves(@layout_tree)
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def window_layout
|
|
884
|
+
return :single if @layout_tree.nil? || @layout_tree[:type] == :window
|
|
885
|
+
case @layout_tree[:type]
|
|
886
|
+
when :vsplit then :vertical
|
|
887
|
+
when :hsplit then :horizontal
|
|
888
|
+
else :single
|
|
889
|
+
end
|
|
749
890
|
end
|
|
750
891
|
|
|
751
892
|
def tabpages
|
|
@@ -764,8 +905,12 @@ module RuVim
|
|
|
764
905
|
@tabpages.length
|
|
765
906
|
end
|
|
766
907
|
|
|
908
|
+
def tabpage_windows(tab)
|
|
909
|
+
tree_leaves(tab[:layout_tree])
|
|
910
|
+
end
|
|
911
|
+
|
|
767
912
|
def window_count
|
|
768
|
-
@
|
|
913
|
+
tree_leaves(@layout_tree).length
|
|
769
914
|
end
|
|
770
915
|
|
|
771
916
|
def quickfix_items
|
|
@@ -778,7 +923,7 @@ module RuVim
|
|
|
778
923
|
|
|
779
924
|
def set_quickfix_list(items)
|
|
780
925
|
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
781
|
-
@quickfix_list = { items: ary, index:
|
|
926
|
+
@quickfix_list = { items: ary, index: nil }
|
|
782
927
|
@quickfix_list
|
|
783
928
|
end
|
|
784
929
|
|
|
@@ -791,7 +936,12 @@ module RuVim
|
|
|
791
936
|
items = @quickfix_list[:items]
|
|
792
937
|
return nil if items.empty?
|
|
793
938
|
|
|
794
|
-
|
|
939
|
+
cur = @quickfix_list[:index]
|
|
940
|
+
@quickfix_list[:index] = if cur.nil?
|
|
941
|
+
step.to_i > 0 ? 0 : items.length - 1
|
|
942
|
+
else
|
|
943
|
+
(cur + step.to_i) % items.length
|
|
944
|
+
end
|
|
795
945
|
current_quickfix_item
|
|
796
946
|
end
|
|
797
947
|
|
|
@@ -814,7 +964,7 @@ module RuVim
|
|
|
814
964
|
|
|
815
965
|
def set_location_list(items, window_id: current_window_id)
|
|
816
966
|
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
817
|
-
@location_lists[window_id] = { items: ary, index:
|
|
967
|
+
@location_lists[window_id] = { items: ary, index: nil }
|
|
818
968
|
@location_lists[window_id]
|
|
819
969
|
end
|
|
820
970
|
|
|
@@ -829,7 +979,12 @@ module RuVim
|
|
|
829
979
|
items = list[:items]
|
|
830
980
|
return nil if items.empty?
|
|
831
981
|
|
|
832
|
-
|
|
982
|
+
cur = list[:index]
|
|
983
|
+
list[:index] = if cur.nil?
|
|
984
|
+
step.to_i > 0 ? 0 : items.length - 1
|
|
985
|
+
else
|
|
986
|
+
(cur + step.to_i) % items.length
|
|
987
|
+
end
|
|
833
988
|
current_location_list_item(window_id)
|
|
834
989
|
end
|
|
835
990
|
|
|
@@ -843,9 +998,22 @@ module RuVim
|
|
|
843
998
|
current_location_list_item(window_id)
|
|
844
999
|
end
|
|
845
1000
|
|
|
1001
|
+
# Check if the path from root to the current window passes through
|
|
1002
|
+
# a split node matching the direction's axis. Used by focus_or_split
|
|
1003
|
+
# to decide whether to split or stay put at edges.
|
|
1004
|
+
# left/right → check for :vsplit ancestor
|
|
1005
|
+
# up/down → check for :hsplit ancestor
|
|
1006
|
+
def has_split_ancestor_on_axis?(dir)
|
|
1007
|
+
target_type = case dir
|
|
1008
|
+
when :left, :right then :vsplit
|
|
1009
|
+
when :up, :down then :hsplit
|
|
1010
|
+
end
|
|
1011
|
+
tree_path_has_split_type?(@layout_tree, @current_window_id, target_type)
|
|
1012
|
+
end
|
|
1013
|
+
|
|
846
1014
|
def find_window_ids_by_buffer_kind(kind)
|
|
847
1015
|
sym = kind.to_sym
|
|
848
|
-
|
|
1016
|
+
window_order.select do |wid|
|
|
849
1017
|
win = @windows[wid]
|
|
850
1018
|
buf = win && @buffers[win.buffer_id]
|
|
851
1019
|
buf && buf.kind == sym
|
|
@@ -857,9 +1025,8 @@ module RuVim
|
|
|
857
1025
|
save_current_tabpage_state!
|
|
858
1026
|
|
|
859
1027
|
with_tab_autosave_suspended do
|
|
860
|
-
@
|
|
1028
|
+
@layout_tree = nil
|
|
861
1029
|
@current_window_id = nil
|
|
862
|
-
@window_layout = :single
|
|
863
1030
|
|
|
864
1031
|
buffer = path ? add_buffer_from_file(path) : add_empty_buffer
|
|
865
1032
|
add_window(buffer_id: buffer.id)
|
|
@@ -886,6 +1053,7 @@ module RuVim
|
|
|
886
1053
|
@mode = :normal
|
|
887
1054
|
@pending_count = nil
|
|
888
1055
|
clear_visual
|
|
1056
|
+
@rich_state = nil
|
|
889
1057
|
end
|
|
890
1058
|
|
|
891
1059
|
def enter_insert_mode
|
|
@@ -901,7 +1069,16 @@ module RuVim
|
|
|
901
1069
|
|
|
902
1070
|
def cancel_command_line
|
|
903
1071
|
@command_line.clear
|
|
904
|
-
|
|
1072
|
+
leave_command_line
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
def leave_command_line
|
|
1076
|
+
if @rich_state
|
|
1077
|
+
@mode = :rich
|
|
1078
|
+
@pending_count = nil
|
|
1079
|
+
else
|
|
1080
|
+
enter_normal_mode
|
|
1081
|
+
end
|
|
905
1082
|
end
|
|
906
1083
|
|
|
907
1084
|
def echo(msg)
|
|
@@ -974,8 +1151,69 @@ module RuVim
|
|
|
974
1151
|
command_line_active? || message_error?
|
|
975
1152
|
end
|
|
976
1153
|
|
|
1154
|
+
def assign_filetype(buffer, ft)
|
|
1155
|
+
buffer.options["filetype"] = ft
|
|
1156
|
+
buffer.lang_module = resolve_lang_module(ft)
|
|
1157
|
+
end
|
|
1158
|
+
|
|
977
1159
|
private
|
|
978
1160
|
|
|
1161
|
+
def resolve_lang_module(ft)
|
|
1162
|
+
case ft
|
|
1163
|
+
when "ruby" then Lang::Ruby
|
|
1164
|
+
else Lang::Base
|
|
1165
|
+
end
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def detect_filetype_from_shebang(path)
|
|
1169
|
+
line = read_first_line(path)
|
|
1170
|
+
return nil unless line.start_with?("#!")
|
|
1171
|
+
|
|
1172
|
+
cmd = shebang_command_name(line)
|
|
1173
|
+
return nil if cmd.nil? || cmd.empty?
|
|
1174
|
+
|
|
1175
|
+
rule = SHEBANG_FILETYPE_RULES.find do |matcher, _filetype|
|
|
1176
|
+
matcher.is_a?(Regexp) ? matcher.match?(cmd) : matcher.to_s == cmd
|
|
1177
|
+
end
|
|
1178
|
+
rule && rule[1]
|
|
1179
|
+
rescue StandardError
|
|
1180
|
+
nil
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
def read_first_line(path)
|
|
1184
|
+
return "" unless path && !path.empty?
|
|
1185
|
+
return "" unless File.file?(path)
|
|
1186
|
+
|
|
1187
|
+
File.open(path, "rb") do |f|
|
|
1188
|
+
(f.gets || "").to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
1189
|
+
end
|
|
1190
|
+
rescue StandardError
|
|
1191
|
+
""
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1194
|
+
def shebang_command_name(line)
|
|
1195
|
+
src = line.to_s.sub(/\A#!/, "").strip
|
|
1196
|
+
return nil if src.empty?
|
|
1197
|
+
|
|
1198
|
+
tokens = src.split(/\s+/)
|
|
1199
|
+
return nil if tokens.empty?
|
|
1200
|
+
|
|
1201
|
+
prog = tokens[0].to_s
|
|
1202
|
+
if File.basename(prog) == "env"
|
|
1203
|
+
i = 1
|
|
1204
|
+
while i < tokens.length && tokens[i].start_with?("-")
|
|
1205
|
+
if tokens[i] == "-S"
|
|
1206
|
+
i += 1
|
|
1207
|
+
break
|
|
1208
|
+
end
|
|
1209
|
+
i += 1
|
|
1210
|
+
end
|
|
1211
|
+
prog = tokens[i].to_s
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
File.basename(prog.to_s)
|
|
1215
|
+
end
|
|
1216
|
+
|
|
979
1217
|
def default_global_options
|
|
980
1218
|
OPTION_DEFS.each_with_object({}) { |(k, v), h| h[k] = v[:default] }
|
|
981
1219
|
end
|
|
@@ -1033,7 +1271,7 @@ module RuVim
|
|
|
1033
1271
|
|
|
1034
1272
|
def assign_detected_filetype(buffer)
|
|
1035
1273
|
ft = detect_filetype(buffer.path)
|
|
1036
|
-
buffer
|
|
1274
|
+
assign_filetype(buffer, ft) if ft && !ft.empty?
|
|
1037
1275
|
buffer
|
|
1038
1276
|
end
|
|
1039
1277
|
|
|
@@ -1070,6 +1308,51 @@ module RuVim
|
|
|
1070
1308
|
current_location
|
|
1071
1309
|
end
|
|
1072
1310
|
|
|
1311
|
+
def arglist
|
|
1312
|
+
@arglist.dup
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
def arglist_index
|
|
1316
|
+
@arglist_index
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
def set_arglist(paths)
|
|
1320
|
+
@arglist = Array(paths).dup
|
|
1321
|
+
@arglist_index = 0
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
def arglist_current
|
|
1325
|
+
@arglist[@arglist_index] if @arglist_index < @arglist.length
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1328
|
+
def arglist_next(count = 1)
|
|
1329
|
+
new_index = @arglist_index + count
|
|
1330
|
+
if new_index >= @arglist.length
|
|
1331
|
+
raise RuVim::CommandError, "Already at last argument"
|
|
1332
|
+
end
|
|
1333
|
+
@arglist_index = new_index
|
|
1334
|
+
@arglist[@arglist_index]
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
def arglist_prev(count = 1)
|
|
1338
|
+
new_index = @arglist_index - count
|
|
1339
|
+
if new_index < 0
|
|
1340
|
+
raise RuVim::CommandError, "Already at first argument"
|
|
1341
|
+
end
|
|
1342
|
+
@arglist_index = new_index
|
|
1343
|
+
@arglist[@arglist_index]
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def arglist_first
|
|
1347
|
+
@arglist_index = 0
|
|
1348
|
+
@arglist[@arglist_index] if @arglist.length > 0
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def arglist_last
|
|
1352
|
+
@arglist_index = [@arglist.length - 1, 0].max
|
|
1353
|
+
@arglist[@arglist_index] if @arglist.length > 0
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1073
1356
|
private
|
|
1074
1357
|
|
|
1075
1358
|
def first_nonblank_col(buffer, row)
|
|
@@ -1136,7 +1419,7 @@ module RuVim
|
|
|
1136
1419
|
|
|
1137
1420
|
def ensure_initial_tabpage!
|
|
1138
1421
|
return unless @tabpages.empty?
|
|
1139
|
-
return if @
|
|
1422
|
+
return if @layout_tree.nil?
|
|
1140
1423
|
|
|
1141
1424
|
@tabpages << new_tabpage_snapshot
|
|
1142
1425
|
@current_tabpage_index = 0
|
|
@@ -1150,18 +1433,16 @@ module RuVim
|
|
|
1150
1433
|
end
|
|
1151
1434
|
|
|
1152
1435
|
def load_tabpage_state!(tab)
|
|
1153
|
-
@
|
|
1436
|
+
@layout_tree = tree_deep_dup(tab[:layout_tree])
|
|
1154
1437
|
@current_window_id = tab[:current_window_id]
|
|
1155
|
-
@window_layout = tab[:window_layout]
|
|
1156
1438
|
current_window
|
|
1157
1439
|
end
|
|
1158
1440
|
|
|
1159
1441
|
def new_tabpage_snapshot(id: nil)
|
|
1160
1442
|
{
|
|
1161
1443
|
id: id || next_tabpage_id,
|
|
1162
|
-
|
|
1163
|
-
current_window_id: @current_window_id
|
|
1164
|
-
window_layout: @window_layout
|
|
1444
|
+
layout_tree: tree_deep_dup(@layout_tree),
|
|
1445
|
+
current_window_id: @current_window_id
|
|
1165
1446
|
}
|
|
1166
1447
|
end
|
|
1167
1448
|
|
|
@@ -1190,5 +1471,108 @@ module RuVim
|
|
|
1190
1471
|
ensure
|
|
1191
1472
|
@suspend_tab_autosave = prev
|
|
1192
1473
|
end
|
|
1474
|
+
|
|
1475
|
+
# --- Layout tree helpers ---
|
|
1476
|
+
|
|
1477
|
+
def tree_leaves(node)
|
|
1478
|
+
return [] if node.nil?
|
|
1479
|
+
return [node[:id]] if node[:type] == :window
|
|
1480
|
+
|
|
1481
|
+
node[:children].flat_map { |c| tree_leaves(c) }
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
def tree_deep_dup(node)
|
|
1485
|
+
return nil if node.nil?
|
|
1486
|
+
return { type: :window, id: node[:id] } if node[:type] == :window
|
|
1487
|
+
|
|
1488
|
+
{ type: node[:type], children: node[:children].map { |c| tree_deep_dup(c) } }
|
|
1489
|
+
end
|
|
1490
|
+
|
|
1491
|
+
# Split a leaf node into a new split node. If the parent is already the same
|
|
1492
|
+
# split type, merge into the parent instead of nesting.
|
|
1493
|
+
def tree_split_leaf(node, target_id, split_type, new_leaf, place)
|
|
1494
|
+
if node[:type] == :window
|
|
1495
|
+
if node[:id] == target_id
|
|
1496
|
+
children = place == :before ? [new_leaf, node] : [node, new_leaf]
|
|
1497
|
+
return { type: split_type, children: children }
|
|
1498
|
+
end
|
|
1499
|
+
return node
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
new_children = node[:children].flat_map do |child|
|
|
1503
|
+
result = tree_split_leaf(child, target_id, split_type, new_leaf, place)
|
|
1504
|
+
# If the child was replaced with the same split type as this node, merge
|
|
1505
|
+
if result[:type] == node[:type] && result != child
|
|
1506
|
+
result[:children]
|
|
1507
|
+
else
|
|
1508
|
+
[result]
|
|
1509
|
+
end
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
{ type: node[:type], children: new_children }
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
def tree_remove(node, target_id)
|
|
1516
|
+
return nil if node.nil?
|
|
1517
|
+
return nil if node[:type] == :window && node[:id] == target_id
|
|
1518
|
+
return node if node[:type] == :window
|
|
1519
|
+
|
|
1520
|
+
new_children = node[:children].filter_map { |c| tree_remove(c, target_id) }
|
|
1521
|
+
return nil if new_children.empty?
|
|
1522
|
+
return new_children.first if new_children.length == 1
|
|
1523
|
+
|
|
1524
|
+
{ type: node[:type], children: new_children }
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
# Check if the path from root to the target window passes through
|
|
1528
|
+
# a split node of the given type. Returns true if any ancestor of the
|
|
1529
|
+
# target window in the tree has type == split_type.
|
|
1530
|
+
def tree_path_has_split_type?(node, target_id, split_type)
|
|
1531
|
+
return false if node.nil?
|
|
1532
|
+
return false if node[:type] == :window
|
|
1533
|
+
# Check if target is in any child subtree
|
|
1534
|
+
node[:children].each do |child|
|
|
1535
|
+
if tree_subtree_contains?(child, target_id)
|
|
1536
|
+
return true if node[:type] == split_type
|
|
1537
|
+
return tree_path_has_split_type?(child, target_id, split_type)
|
|
1538
|
+
end
|
|
1539
|
+
end
|
|
1540
|
+
false
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
def tree_subtree_contains?(node, target_id)
|
|
1544
|
+
return false if node.nil?
|
|
1545
|
+
return node[:id] == target_id if node[:type] == :window
|
|
1546
|
+
node[:children].any? { |c| tree_subtree_contains?(c, target_id) }
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
# Compute normalized rects (0.0–1.0 coordinate space) for direction finding.
|
|
1550
|
+
# Separators are ignored here — this is purely for adjacency/direction logic.
|
|
1551
|
+
def tree_compute_rects(node, top:, left:, height:, width:)
|
|
1552
|
+
return {} if node.nil?
|
|
1553
|
+
|
|
1554
|
+
if node[:type] == :window
|
|
1555
|
+
return { node[:id] => { top: top, left: left, height: height, width: width } }
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
children = node[:children]
|
|
1559
|
+
n = children.length
|
|
1560
|
+
rects = {}
|
|
1561
|
+
|
|
1562
|
+
case node[:type]
|
|
1563
|
+
when :vsplit
|
|
1564
|
+
w_each = width / n.to_f
|
|
1565
|
+
children.each_with_index do |child, i|
|
|
1566
|
+
rects.merge!(tree_compute_rects(child, top: top, left: left + i * w_each, height: height, width: w_each))
|
|
1567
|
+
end
|
|
1568
|
+
when :hsplit
|
|
1569
|
+
h_each = height / n.to_f
|
|
1570
|
+
children.each_with_index do |child, i|
|
|
1571
|
+
rects.merge!(tree_compute_rects(child, top: top + i * h_each, left: left, height: h_each, width: width))
|
|
1572
|
+
end
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
rects
|
|
1576
|
+
end
|
|
1193
1577
|
end
|
|
1194
1578
|
end
|