ruvim 0.2.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/.github/workflows/test.yml +4 -0
- data/AGENTS.md +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -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 +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- 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 +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -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/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -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/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -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 +109 -0
- data/lib/ruvim/screen.rb +503 -109
- 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 +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -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/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/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -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 +165 -0
- data/test/indent_test.rb +287 -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 +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -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, :git_stream_handler, :git_stream_stop_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,9 +93,14 @@ 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
|
|
103
|
+
@hlsearch_suppressed = false
|
|
82
104
|
@last_find = nil
|
|
83
105
|
@registers = {}
|
|
84
106
|
@active_register_name = nil
|
|
@@ -89,8 +111,12 @@ module RuVim
|
|
|
89
111
|
@macros = {}
|
|
90
112
|
@macro_recording = nil
|
|
91
113
|
@visual_state = nil
|
|
114
|
+
@rich_state = nil
|
|
92
115
|
@quickfix_list = { items: [], index: nil }
|
|
93
116
|
@location_lists = Hash.new { |h, k| h[k] = { items: [], index: nil } }
|
|
117
|
+
@arglist = []
|
|
118
|
+
@arglist_index = 0
|
|
119
|
+
@hit_enter_lines = nil
|
|
94
120
|
end
|
|
95
121
|
|
|
96
122
|
def running?
|
|
@@ -127,6 +153,15 @@ module RuVim
|
|
|
127
153
|
|
|
128
154
|
def set_last_search(pattern:, direction:)
|
|
129
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
|
|
130
165
|
end
|
|
131
166
|
|
|
132
167
|
def set_last_find(char:, direction:, till:)
|
|
@@ -141,6 +176,22 @@ module RuVim
|
|
|
141
176
|
@buffers.fetch(current_window.buffer_id)
|
|
142
177
|
end
|
|
143
178
|
|
|
179
|
+
def stdin_stream_stop_or_cancel!
|
|
180
|
+
handler = @stdin_stream_stop_handler
|
|
181
|
+
return false unless handler
|
|
182
|
+
|
|
183
|
+
handler.call
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def invoke_app_action(name, **kwargs)
|
|
188
|
+
handler = @app_action_handler
|
|
189
|
+
return false unless handler
|
|
190
|
+
|
|
191
|
+
handler.call(name.to_sym, **kwargs)
|
|
192
|
+
true
|
|
193
|
+
end
|
|
194
|
+
|
|
144
195
|
def option_def(name)
|
|
145
196
|
OPTION_DEFS[name.to_s]
|
|
146
197
|
end
|
|
@@ -213,7 +264,7 @@ module RuVim
|
|
|
213
264
|
base = File.basename(p)
|
|
214
265
|
return "ruby" if %w[Gemfile Rakefile Guardfile].include?(base)
|
|
215
266
|
|
|
216
|
-
{
|
|
267
|
+
ext_ft = {
|
|
217
268
|
".rb" => "ruby",
|
|
218
269
|
".rake" => "ruby",
|
|
219
270
|
".ru" => "ruby",
|
|
@@ -225,14 +276,23 @@ module RuVim
|
|
|
225
276
|
".tsx" => "typescriptreact",
|
|
226
277
|
".jsx" => "javascriptreact",
|
|
227
278
|
".json" => "json",
|
|
279
|
+
".jsonl" => "jsonl",
|
|
228
280
|
".yml" => "yaml",
|
|
229
281
|
".yaml" => "yaml",
|
|
230
282
|
".md" => "markdown",
|
|
231
283
|
".txt" => "text",
|
|
232
284
|
".html" => "html",
|
|
233
285
|
".css" => "css",
|
|
234
|
-
".sh" => "sh"
|
|
286
|
+
".sh" => "sh",
|
|
287
|
+
".tsv" => "tsv",
|
|
288
|
+
".csv" => "csv",
|
|
289
|
+
".scm" => "scheme",
|
|
290
|
+
".ss" => "scheme",
|
|
291
|
+
".sld" => "scheme"
|
|
235
292
|
}[File.extname(base).downcase]
|
|
293
|
+
return ext_ft if ext_ft
|
|
294
|
+
|
|
295
|
+
detect_filetype_from_shebang(p)
|
|
236
296
|
end
|
|
237
297
|
|
|
238
298
|
def registers
|
|
@@ -481,6 +541,53 @@ module RuVim
|
|
|
481
541
|
end
|
|
482
542
|
end
|
|
483
543
|
|
|
544
|
+
def rich_state
|
|
545
|
+
@rich_state
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def rich_mode?
|
|
549
|
+
@mode == :rich
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def enter_rich_mode(format:, delimiter:)
|
|
553
|
+
@mode = :rich
|
|
554
|
+
@pending_count = nil
|
|
555
|
+
@rich_state = { format: format, delimiter: delimiter }
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def exit_rich_mode
|
|
559
|
+
@rich_state = nil
|
|
560
|
+
enter_normal_mode
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def hit_enter_active?
|
|
564
|
+
@mode == :hit_enter
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def hit_enter_lines
|
|
568
|
+
@hit_enter_lines
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def enter_hit_enter_mode(lines)
|
|
572
|
+
@mode = :hit_enter
|
|
573
|
+
@hit_enter_lines = Array(lines)
|
|
574
|
+
@pending_count = nil
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def exit_hit_enter_mode
|
|
578
|
+
@hit_enter_lines = nil
|
|
579
|
+
enter_normal_mode
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def echo_multiline(lines)
|
|
583
|
+
lines = Array(lines)
|
|
584
|
+
if lines.length <= 1
|
|
585
|
+
echo(lines.first.to_s)
|
|
586
|
+
else
|
|
587
|
+
enter_hit_enter_mode(lines)
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
484
591
|
def add_empty_buffer(path: nil)
|
|
485
592
|
id = next_buffer_id
|
|
486
593
|
buffer = Buffer.new(id:, path:)
|
|
@@ -492,7 +599,7 @@ module RuVim
|
|
|
492
599
|
def add_virtual_buffer(kind:, name:, lines:, filetype: nil, readonly: true, modifiable: false)
|
|
493
600
|
id = next_buffer_id
|
|
494
601
|
buffer = Buffer.new(id:, lines:, kind:, name:, readonly:, modifiable:)
|
|
495
|
-
buffer
|
|
602
|
+
assign_filetype(buffer, filetype) if filetype
|
|
496
603
|
@buffers[id] = buffer
|
|
497
604
|
buffer
|
|
498
605
|
end
|
|
@@ -509,7 +616,17 @@ module RuVim
|
|
|
509
616
|
id = next_window_id
|
|
510
617
|
window = Window.new(id:, buffer_id:)
|
|
511
618
|
@windows[id] = window
|
|
512
|
-
|
|
619
|
+
leaf = { type: :window, id: id }
|
|
620
|
+
if @layout_tree.nil?
|
|
621
|
+
@layout_tree = leaf
|
|
622
|
+
else
|
|
623
|
+
# Append as sibling — used for initial bootstrap only
|
|
624
|
+
if @layout_tree[:type] == :window
|
|
625
|
+
@layout_tree = { type: :hsplit, children: [@layout_tree, leaf] }
|
|
626
|
+
else
|
|
627
|
+
@layout_tree[:children] << leaf
|
|
628
|
+
end
|
|
629
|
+
end
|
|
513
630
|
@current_window_id ||= id
|
|
514
631
|
ensure_initial_tabpage!
|
|
515
632
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
@@ -522,15 +639,17 @@ module RuVim
|
|
|
522
639
|
id = next_window_id
|
|
523
640
|
win = Window.new(id:, buffer_id: src.buffer_id)
|
|
524
641
|
@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
642
|
ensure_initial_tabpage!
|
|
529
643
|
win.cursor_x = src.cursor_x
|
|
530
644
|
win.cursor_y = src.cursor_y
|
|
531
645
|
win.row_offset = src.row_offset
|
|
532
646
|
win.col_offset = src.col_offset
|
|
533
|
-
|
|
647
|
+
|
|
648
|
+
split_type = (layout.to_sym == :vertical ? :vsplit : :hsplit)
|
|
649
|
+
new_leaf = { type: :window, id: win.id }
|
|
650
|
+
|
|
651
|
+
@layout_tree = tree_split_leaf(@layout_tree, src.id, split_type, new_leaf, place.to_sym)
|
|
652
|
+
|
|
534
653
|
@current_window_id = win.id
|
|
535
654
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
536
655
|
win
|
|
@@ -541,18 +660,21 @@ module RuVim
|
|
|
541
660
|
end
|
|
542
661
|
|
|
543
662
|
def close_window(id)
|
|
544
|
-
|
|
545
|
-
return nil if
|
|
546
|
-
return nil
|
|
663
|
+
leaves = tree_leaves(@layout_tree)
|
|
664
|
+
return nil if leaves.empty?
|
|
665
|
+
return nil if leaves.length <= 1
|
|
666
|
+
return nil unless leaves.include?(id)
|
|
547
667
|
|
|
548
668
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
549
|
-
idx =
|
|
669
|
+
idx = leaves.index(id) || 0
|
|
550
670
|
@windows.delete(id)
|
|
551
|
-
@window_order.delete(id)
|
|
552
671
|
@location_lists.delete(id)
|
|
553
|
-
|
|
554
|
-
@
|
|
555
|
-
|
|
672
|
+
|
|
673
|
+
@layout_tree = tree_remove(@layout_tree, id)
|
|
674
|
+
|
|
675
|
+
new_leaves = tree_leaves(@layout_tree)
|
|
676
|
+
@current_window_id = new_leaves[[idx, new_leaves.length - 1].min] if @current_window_id == id
|
|
677
|
+
@current_window_id ||= new_leaves.first
|
|
556
678
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
557
679
|
current_window
|
|
558
680
|
end
|
|
@@ -563,7 +685,8 @@ module RuVim
|
|
|
563
685
|
|
|
564
686
|
save_current_tabpage_state!
|
|
565
687
|
removed = @tabpages.delete_at(@current_tabpage_index)
|
|
566
|
-
|
|
688
|
+
removed_tree = removed && removed[:layout_tree]
|
|
689
|
+
tree_leaves(removed_tree).each do |wid|
|
|
567
690
|
@windows.delete(wid)
|
|
568
691
|
@location_lists.delete(wid)
|
|
569
692
|
end
|
|
@@ -581,42 +704,55 @@ module RuVim
|
|
|
581
704
|
end
|
|
582
705
|
|
|
583
706
|
def focus_next_window
|
|
584
|
-
|
|
707
|
+
order = window_order
|
|
708
|
+
return current_window if order.length <= 1
|
|
585
709
|
|
|
586
|
-
idx =
|
|
587
|
-
focus_window(
|
|
710
|
+
idx = order.index(@current_window_id) || 0
|
|
711
|
+
focus_window(order[(idx + 1) % order.length])
|
|
588
712
|
end
|
|
589
713
|
|
|
590
714
|
def focus_prev_window
|
|
591
|
-
|
|
715
|
+
order = window_order
|
|
716
|
+
return current_window if order.length <= 1
|
|
592
717
|
|
|
593
|
-
idx =
|
|
594
|
-
focus_window(
|
|
718
|
+
idx = order.index(@current_window_id) || 0
|
|
719
|
+
focus_window(order[(idx - 1) % order.length])
|
|
595
720
|
end
|
|
596
721
|
|
|
597
722
|
def focus_window_direction(dir)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
723
|
+
leaves = tree_leaves(@layout_tree)
|
|
724
|
+
return current_window if leaves.length <= 1
|
|
725
|
+
|
|
726
|
+
rects = tree_compute_rects(@layout_tree, top: 0.0, left: 0.0, height: 1.0, width: 1.0)
|
|
727
|
+
cur = rects[@current_window_id]
|
|
728
|
+
return current_window unless cur
|
|
729
|
+
|
|
730
|
+
best_id = nil
|
|
731
|
+
best_dist = Float::INFINITY
|
|
732
|
+
cur_cx = cur[:left] + cur[:width] / 2.0
|
|
733
|
+
cur_cy = cur[:top] + cur[:height] / 2.0
|
|
734
|
+
|
|
735
|
+
rects.each do |wid, r|
|
|
736
|
+
next if wid == @current_window_id
|
|
737
|
+
rcx = r[:left] + r[:width] / 2.0
|
|
738
|
+
rcy = r[:top] + r[:height] / 2.0
|
|
739
|
+
|
|
740
|
+
in_direction = case dir
|
|
741
|
+
when :left then rcx < cur_cx
|
|
742
|
+
when :right then rcx > cur_cx
|
|
743
|
+
when :up then rcy < cur_cy
|
|
744
|
+
when :down then rcy > cur_cy
|
|
745
|
+
end
|
|
746
|
+
next unless in_direction
|
|
747
|
+
|
|
748
|
+
dist = (rcx - cur_cx).abs + (rcy - cur_cy).abs
|
|
749
|
+
if dist < best_dist
|
|
750
|
+
best_dist = dist
|
|
751
|
+
best_id = wid
|
|
608
752
|
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
753
|
end
|
|
754
|
+
|
|
755
|
+
best_id ? focus_window(best_id) : current_window
|
|
620
756
|
end
|
|
621
757
|
|
|
622
758
|
def switch_to_buffer(buffer_id)
|
|
@@ -634,9 +770,16 @@ module RuVim
|
|
|
634
770
|
end
|
|
635
771
|
|
|
636
772
|
def open_path(path)
|
|
773
|
+
handler = @open_path_handler
|
|
774
|
+
return handler.call(path) if handler
|
|
775
|
+
|
|
776
|
+
open_path_sync(path)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def open_path_sync(path)
|
|
637
780
|
buffer = add_buffer_from_file(path)
|
|
638
781
|
switch_to_buffer(buffer.id)
|
|
639
|
-
echo(path && File.exist?(path)
|
|
782
|
+
echo("[New File]") unless path && File.exist?(path)
|
|
640
783
|
buffer
|
|
641
784
|
end
|
|
642
785
|
|
|
@@ -672,7 +815,7 @@ module RuVim
|
|
|
672
815
|
current_buffer.replace_all_lines!(intro_lines)
|
|
673
816
|
current_buffer.configure_special!(kind: :intro, name: "[Intro]", readonly: true, modifiable: false)
|
|
674
817
|
current_buffer.modified = false
|
|
675
|
-
current_buffer
|
|
818
|
+
assign_filetype(current_buffer, "help")
|
|
676
819
|
current_window.cursor_x = 0
|
|
677
820
|
current_window.cursor_y = 0
|
|
678
821
|
current_window.row_offset = 0
|
|
@@ -685,7 +828,7 @@ module RuVim
|
|
|
685
828
|
return false unless current_buffer.intro_buffer?
|
|
686
829
|
|
|
687
830
|
current_buffer.become_normal_empty_buffer!
|
|
688
|
-
current_buffer
|
|
831
|
+
assign_filetype(current_buffer, nil)
|
|
689
832
|
current_window.cursor_x = 0
|
|
690
833
|
current_window.cursor_y = 0
|
|
691
834
|
current_window.row_offset = 0
|
|
@@ -745,7 +888,16 @@ module RuVim
|
|
|
745
888
|
end
|
|
746
889
|
|
|
747
890
|
def window_order
|
|
748
|
-
@
|
|
891
|
+
tree_leaves(@layout_tree)
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def window_layout
|
|
895
|
+
return :single if @layout_tree.nil? || @layout_tree[:type] == :window
|
|
896
|
+
case @layout_tree[:type]
|
|
897
|
+
when :vsplit then :vertical
|
|
898
|
+
when :hsplit then :horizontal
|
|
899
|
+
else :single
|
|
900
|
+
end
|
|
749
901
|
end
|
|
750
902
|
|
|
751
903
|
def tabpages
|
|
@@ -764,8 +916,12 @@ module RuVim
|
|
|
764
916
|
@tabpages.length
|
|
765
917
|
end
|
|
766
918
|
|
|
919
|
+
def tabpage_windows(tab)
|
|
920
|
+
tree_leaves(tab[:layout_tree])
|
|
921
|
+
end
|
|
922
|
+
|
|
767
923
|
def window_count
|
|
768
|
-
@
|
|
924
|
+
tree_leaves(@layout_tree).length
|
|
769
925
|
end
|
|
770
926
|
|
|
771
927
|
def quickfix_items
|
|
@@ -778,7 +934,7 @@ module RuVim
|
|
|
778
934
|
|
|
779
935
|
def set_quickfix_list(items)
|
|
780
936
|
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
781
|
-
@quickfix_list = { items: ary, index:
|
|
937
|
+
@quickfix_list = { items: ary, index: nil }
|
|
782
938
|
@quickfix_list
|
|
783
939
|
end
|
|
784
940
|
|
|
@@ -791,7 +947,12 @@ module RuVim
|
|
|
791
947
|
items = @quickfix_list[:items]
|
|
792
948
|
return nil if items.empty?
|
|
793
949
|
|
|
794
|
-
|
|
950
|
+
cur = @quickfix_list[:index]
|
|
951
|
+
@quickfix_list[:index] = if cur.nil?
|
|
952
|
+
step.to_i > 0 ? 0 : items.length - 1
|
|
953
|
+
else
|
|
954
|
+
(cur + step.to_i) % items.length
|
|
955
|
+
end
|
|
795
956
|
current_quickfix_item
|
|
796
957
|
end
|
|
797
958
|
|
|
@@ -814,7 +975,7 @@ module RuVim
|
|
|
814
975
|
|
|
815
976
|
def set_location_list(items, window_id: current_window_id)
|
|
816
977
|
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:
|
|
978
|
+
@location_lists[window_id] = { items: ary, index: nil }
|
|
818
979
|
@location_lists[window_id]
|
|
819
980
|
end
|
|
820
981
|
|
|
@@ -829,7 +990,12 @@ module RuVim
|
|
|
829
990
|
items = list[:items]
|
|
830
991
|
return nil if items.empty?
|
|
831
992
|
|
|
832
|
-
|
|
993
|
+
cur = list[:index]
|
|
994
|
+
list[:index] = if cur.nil?
|
|
995
|
+
step.to_i > 0 ? 0 : items.length - 1
|
|
996
|
+
else
|
|
997
|
+
(cur + step.to_i) % items.length
|
|
998
|
+
end
|
|
833
999
|
current_location_list_item(window_id)
|
|
834
1000
|
end
|
|
835
1001
|
|
|
@@ -843,9 +1009,22 @@ module RuVim
|
|
|
843
1009
|
current_location_list_item(window_id)
|
|
844
1010
|
end
|
|
845
1011
|
|
|
1012
|
+
# Check if the path from root to the current window passes through
|
|
1013
|
+
# a split node matching the direction's axis. Used by focus_or_split
|
|
1014
|
+
# to decide whether to split or stay put at edges.
|
|
1015
|
+
# left/right → check for :vsplit ancestor
|
|
1016
|
+
# up/down → check for :hsplit ancestor
|
|
1017
|
+
def has_split_ancestor_on_axis?(dir)
|
|
1018
|
+
target_type = case dir
|
|
1019
|
+
when :left, :right then :vsplit
|
|
1020
|
+
when :up, :down then :hsplit
|
|
1021
|
+
end
|
|
1022
|
+
tree_path_has_split_type?(@layout_tree, @current_window_id, target_type)
|
|
1023
|
+
end
|
|
1024
|
+
|
|
846
1025
|
def find_window_ids_by_buffer_kind(kind)
|
|
847
1026
|
sym = kind.to_sym
|
|
848
|
-
|
|
1027
|
+
window_order.select do |wid|
|
|
849
1028
|
win = @windows[wid]
|
|
850
1029
|
buf = win && @buffers[win.buffer_id]
|
|
851
1030
|
buf && buf.kind == sym
|
|
@@ -857,9 +1036,8 @@ module RuVim
|
|
|
857
1036
|
save_current_tabpage_state!
|
|
858
1037
|
|
|
859
1038
|
with_tab_autosave_suspended do
|
|
860
|
-
@
|
|
1039
|
+
@layout_tree = nil
|
|
861
1040
|
@current_window_id = nil
|
|
862
|
-
@window_layout = :single
|
|
863
1041
|
|
|
864
1042
|
buffer = path ? add_buffer_from_file(path) : add_empty_buffer
|
|
865
1043
|
add_window(buffer_id: buffer.id)
|
|
@@ -886,6 +1064,7 @@ module RuVim
|
|
|
886
1064
|
@mode = :normal
|
|
887
1065
|
@pending_count = nil
|
|
888
1066
|
clear_visual
|
|
1067
|
+
@rich_state = nil
|
|
889
1068
|
end
|
|
890
1069
|
|
|
891
1070
|
def enter_insert_mode
|
|
@@ -901,7 +1080,16 @@ module RuVim
|
|
|
901
1080
|
|
|
902
1081
|
def cancel_command_line
|
|
903
1082
|
@command_line.clear
|
|
904
|
-
|
|
1083
|
+
leave_command_line
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
def leave_command_line
|
|
1087
|
+
if @rich_state
|
|
1088
|
+
@mode = :rich
|
|
1089
|
+
@pending_count = nil
|
|
1090
|
+
else
|
|
1091
|
+
enter_normal_mode
|
|
1092
|
+
end
|
|
905
1093
|
end
|
|
906
1094
|
|
|
907
1095
|
def echo(msg)
|
|
@@ -974,8 +1162,69 @@ module RuVim
|
|
|
974
1162
|
command_line_active? || message_error?
|
|
975
1163
|
end
|
|
976
1164
|
|
|
1165
|
+
def assign_filetype(buffer, ft)
|
|
1166
|
+
buffer.options["filetype"] = ft
|
|
1167
|
+
buffer.lang_module = resolve_lang_module(ft)
|
|
1168
|
+
end
|
|
1169
|
+
|
|
977
1170
|
private
|
|
978
1171
|
|
|
1172
|
+
def resolve_lang_module(ft)
|
|
1173
|
+
case ft
|
|
1174
|
+
when "ruby" then Lang::Ruby
|
|
1175
|
+
else Lang::Base
|
|
1176
|
+
end
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def detect_filetype_from_shebang(path)
|
|
1180
|
+
line = read_first_line(path)
|
|
1181
|
+
return nil unless line.start_with?("#!")
|
|
1182
|
+
|
|
1183
|
+
cmd = shebang_command_name(line)
|
|
1184
|
+
return nil if cmd.nil? || cmd.empty?
|
|
1185
|
+
|
|
1186
|
+
rule = SHEBANG_FILETYPE_RULES.find do |matcher, _filetype|
|
|
1187
|
+
matcher.is_a?(Regexp) ? matcher.match?(cmd) : matcher.to_s == cmd
|
|
1188
|
+
end
|
|
1189
|
+
rule && rule[1]
|
|
1190
|
+
rescue StandardError
|
|
1191
|
+
nil
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1194
|
+
def read_first_line(path)
|
|
1195
|
+
return "" unless path && !path.empty?
|
|
1196
|
+
return "" unless File.file?(path)
|
|
1197
|
+
|
|
1198
|
+
File.open(path, "rb") do |f|
|
|
1199
|
+
(f.gets || "").to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
1200
|
+
end
|
|
1201
|
+
rescue StandardError
|
|
1202
|
+
""
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
def shebang_command_name(line)
|
|
1206
|
+
src = line.to_s.sub(/\A#!/, "").strip
|
|
1207
|
+
return nil if src.empty?
|
|
1208
|
+
|
|
1209
|
+
tokens = src.split(/\s+/)
|
|
1210
|
+
return nil if tokens.empty?
|
|
1211
|
+
|
|
1212
|
+
prog = tokens[0].to_s
|
|
1213
|
+
if File.basename(prog) == "env"
|
|
1214
|
+
i = 1
|
|
1215
|
+
while i < tokens.length && tokens[i].start_with?("-")
|
|
1216
|
+
if tokens[i] == "-S"
|
|
1217
|
+
i += 1
|
|
1218
|
+
break
|
|
1219
|
+
end
|
|
1220
|
+
i += 1
|
|
1221
|
+
end
|
|
1222
|
+
prog = tokens[i].to_s
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
File.basename(prog.to_s)
|
|
1226
|
+
end
|
|
1227
|
+
|
|
979
1228
|
def default_global_options
|
|
980
1229
|
OPTION_DEFS.each_with_object({}) { |(k, v), h| h[k] = v[:default] }
|
|
981
1230
|
end
|
|
@@ -1033,7 +1282,7 @@ module RuVim
|
|
|
1033
1282
|
|
|
1034
1283
|
def assign_detected_filetype(buffer)
|
|
1035
1284
|
ft = detect_filetype(buffer.path)
|
|
1036
|
-
buffer
|
|
1285
|
+
assign_filetype(buffer, ft) if ft && !ft.empty?
|
|
1037
1286
|
buffer
|
|
1038
1287
|
end
|
|
1039
1288
|
|
|
@@ -1070,6 +1319,51 @@ module RuVim
|
|
|
1070
1319
|
current_location
|
|
1071
1320
|
end
|
|
1072
1321
|
|
|
1322
|
+
def arglist
|
|
1323
|
+
@arglist.dup
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def arglist_index
|
|
1327
|
+
@arglist_index
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
def set_arglist(paths)
|
|
1331
|
+
@arglist = Array(paths).dup
|
|
1332
|
+
@arglist_index = 0
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def arglist_current
|
|
1336
|
+
@arglist[@arglist_index] if @arglist_index < @arglist.length
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
def arglist_next(count = 1)
|
|
1340
|
+
new_index = @arglist_index + count
|
|
1341
|
+
if new_index >= @arglist.length
|
|
1342
|
+
raise RuVim::CommandError, "Already at last argument"
|
|
1343
|
+
end
|
|
1344
|
+
@arglist_index = new_index
|
|
1345
|
+
@arglist[@arglist_index]
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def arglist_prev(count = 1)
|
|
1349
|
+
new_index = @arglist_index - count
|
|
1350
|
+
if new_index < 0
|
|
1351
|
+
raise RuVim::CommandError, "Already at first argument"
|
|
1352
|
+
end
|
|
1353
|
+
@arglist_index = new_index
|
|
1354
|
+
@arglist[@arglist_index]
|
|
1355
|
+
end
|
|
1356
|
+
|
|
1357
|
+
def arglist_first
|
|
1358
|
+
@arglist_index = 0
|
|
1359
|
+
@arglist[@arglist_index] if @arglist.length > 0
|
|
1360
|
+
end
|
|
1361
|
+
|
|
1362
|
+
def arglist_last
|
|
1363
|
+
@arglist_index = [@arglist.length - 1, 0].max
|
|
1364
|
+
@arglist[@arglist_index] if @arglist.length > 0
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1073
1367
|
private
|
|
1074
1368
|
|
|
1075
1369
|
def first_nonblank_col(buffer, row)
|
|
@@ -1136,7 +1430,7 @@ module RuVim
|
|
|
1136
1430
|
|
|
1137
1431
|
def ensure_initial_tabpage!
|
|
1138
1432
|
return unless @tabpages.empty?
|
|
1139
|
-
return if @
|
|
1433
|
+
return if @layout_tree.nil?
|
|
1140
1434
|
|
|
1141
1435
|
@tabpages << new_tabpage_snapshot
|
|
1142
1436
|
@current_tabpage_index = 0
|
|
@@ -1150,18 +1444,16 @@ module RuVim
|
|
|
1150
1444
|
end
|
|
1151
1445
|
|
|
1152
1446
|
def load_tabpage_state!(tab)
|
|
1153
|
-
@
|
|
1447
|
+
@layout_tree = tree_deep_dup(tab[:layout_tree])
|
|
1154
1448
|
@current_window_id = tab[:current_window_id]
|
|
1155
|
-
@window_layout = tab[:window_layout]
|
|
1156
1449
|
current_window
|
|
1157
1450
|
end
|
|
1158
1451
|
|
|
1159
1452
|
def new_tabpage_snapshot(id: nil)
|
|
1160
1453
|
{
|
|
1161
1454
|
id: id || next_tabpage_id,
|
|
1162
|
-
|
|
1163
|
-
current_window_id: @current_window_id
|
|
1164
|
-
window_layout: @window_layout
|
|
1455
|
+
layout_tree: tree_deep_dup(@layout_tree),
|
|
1456
|
+
current_window_id: @current_window_id
|
|
1165
1457
|
}
|
|
1166
1458
|
end
|
|
1167
1459
|
|
|
@@ -1190,5 +1482,108 @@ module RuVim
|
|
|
1190
1482
|
ensure
|
|
1191
1483
|
@suspend_tab_autosave = prev
|
|
1192
1484
|
end
|
|
1485
|
+
|
|
1486
|
+
# --- Layout tree helpers ---
|
|
1487
|
+
|
|
1488
|
+
def tree_leaves(node)
|
|
1489
|
+
return [] if node.nil?
|
|
1490
|
+
return [node[:id]] if node[:type] == :window
|
|
1491
|
+
|
|
1492
|
+
node[:children].flat_map { |c| tree_leaves(c) }
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1495
|
+
def tree_deep_dup(node)
|
|
1496
|
+
return nil if node.nil?
|
|
1497
|
+
return { type: :window, id: node[:id] } if node[:type] == :window
|
|
1498
|
+
|
|
1499
|
+
{ type: node[:type], children: node[:children].map { |c| tree_deep_dup(c) } }
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
# Split a leaf node into a new split node. If the parent is already the same
|
|
1503
|
+
# split type, merge into the parent instead of nesting.
|
|
1504
|
+
def tree_split_leaf(node, target_id, split_type, new_leaf, place)
|
|
1505
|
+
if node[:type] == :window
|
|
1506
|
+
if node[:id] == target_id
|
|
1507
|
+
children = place == :before ? [new_leaf, node] : [node, new_leaf]
|
|
1508
|
+
return { type: split_type, children: children }
|
|
1509
|
+
end
|
|
1510
|
+
return node
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
new_children = node[:children].flat_map do |child|
|
|
1514
|
+
result = tree_split_leaf(child, target_id, split_type, new_leaf, place)
|
|
1515
|
+
# If the child was replaced with the same split type as this node, merge
|
|
1516
|
+
if result[:type] == node[:type] && result != child
|
|
1517
|
+
result[:children]
|
|
1518
|
+
else
|
|
1519
|
+
[result]
|
|
1520
|
+
end
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
{ type: node[:type], children: new_children }
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
def tree_remove(node, target_id)
|
|
1527
|
+
return nil if node.nil?
|
|
1528
|
+
return nil if node[:type] == :window && node[:id] == target_id
|
|
1529
|
+
return node if node[:type] == :window
|
|
1530
|
+
|
|
1531
|
+
new_children = node[:children].filter_map { |c| tree_remove(c, target_id) }
|
|
1532
|
+
return nil if new_children.empty?
|
|
1533
|
+
return new_children.first if new_children.length == 1
|
|
1534
|
+
|
|
1535
|
+
{ type: node[:type], children: new_children }
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
# Check if the path from root to the target window passes through
|
|
1539
|
+
# a split node of the given type. Returns true if any ancestor of the
|
|
1540
|
+
# target window in the tree has type == split_type.
|
|
1541
|
+
def tree_path_has_split_type?(node, target_id, split_type)
|
|
1542
|
+
return false if node.nil?
|
|
1543
|
+
return false if node[:type] == :window
|
|
1544
|
+
# Check if target is in any child subtree
|
|
1545
|
+
node[:children].each do |child|
|
|
1546
|
+
if tree_subtree_contains?(child, target_id)
|
|
1547
|
+
return true if node[:type] == split_type
|
|
1548
|
+
return tree_path_has_split_type?(child, target_id, split_type)
|
|
1549
|
+
end
|
|
1550
|
+
end
|
|
1551
|
+
false
|
|
1552
|
+
end
|
|
1553
|
+
|
|
1554
|
+
def tree_subtree_contains?(node, target_id)
|
|
1555
|
+
return false if node.nil?
|
|
1556
|
+
return node[:id] == target_id if node[:type] == :window
|
|
1557
|
+
node[:children].any? { |c| tree_subtree_contains?(c, target_id) }
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
# Compute normalized rects (0.0–1.0 coordinate space) for direction finding.
|
|
1561
|
+
# Separators are ignored here — this is purely for adjacency/direction logic.
|
|
1562
|
+
def tree_compute_rects(node, top:, left:, height:, width:)
|
|
1563
|
+
return {} if node.nil?
|
|
1564
|
+
|
|
1565
|
+
if node[:type] == :window
|
|
1566
|
+
return { node[:id] => { top: top, left: left, height: height, width: width } }
|
|
1567
|
+
end
|
|
1568
|
+
|
|
1569
|
+
children = node[:children]
|
|
1570
|
+
n = children.length
|
|
1571
|
+
rects = {}
|
|
1572
|
+
|
|
1573
|
+
case node[:type]
|
|
1574
|
+
when :vsplit
|
|
1575
|
+
w_each = width / n.to_f
|
|
1576
|
+
children.each_with_index do |child, i|
|
|
1577
|
+
rects.merge!(tree_compute_rects(child, top: top, left: left + i * w_each, height: height, width: w_each))
|
|
1578
|
+
end
|
|
1579
|
+
when :hsplit
|
|
1580
|
+
h_each = height / n.to_f
|
|
1581
|
+
children.each_with_index do |child, i|
|
|
1582
|
+
rects.merge!(tree_compute_rects(child, top: top + i * h_each, left: left, height: h_each, width: width))
|
|
1583
|
+
end
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
rects
|
|
1587
|
+
end
|
|
1193
1588
|
end
|
|
1194
1589
|
end
|