ruvim 0.1.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 +29 -0
- data/docs/command.md +101 -0
- data/docs/config.md +203 -84
- data/docs/done.md +21 -0
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +195 -33
- data/docs/todo.md +183 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +94 -171
- data/lib/ruvim/app.rb +1543 -172
- 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 +21 -5
- data/lib/ruvim/context.rb +2 -7
- data/lib/ruvim/dispatcher.rb +153 -13
- data/lib/ruvim/display_width.rb +28 -2
- data/lib/ruvim/editor.rb +622 -69
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +1386 -114
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +52 -29
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +48 -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 +851 -119
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +28 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +37 -10
- data/lib/ruvim.rb +15 -0
- data/test/app_completion_test.rb +174 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +110 -2
- data/test/app_scenario_test.rb +998 -0
- data/test/app_startup_test.rb +197 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +438 -0
- data/test/display_width_test.rb +18 -0
- data/test/editor_register_test.rb +23 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +65 -14
- 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 +470 -0
- data/test/window_test.rb +26 -0
- metadata +37 -2
data/lib/ruvim/editor.rb
CHANGED
|
@@ -1,22 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class Editor
|
|
3
5
|
OPTION_DEFS = {
|
|
4
6
|
"number" => { default_scope: :window, type: :bool, default: false },
|
|
5
7
|
"relativenumber" => { default_scope: :window, type: :bool, default: false },
|
|
8
|
+
"wrap" => { default_scope: :window, type: :bool, default: true },
|
|
9
|
+
"linebreak" => { default_scope: :window, type: :bool, default: false },
|
|
10
|
+
"breakindent" => { default_scope: :window, type: :bool, default: false },
|
|
11
|
+
"cursorline" => { default_scope: :window, type: :bool, default: false },
|
|
12
|
+
"scrolloff" => { default_scope: :window, type: :int, default: 0 },
|
|
13
|
+
"sidescrolloff" => { default_scope: :window, type: :int, default: 0 },
|
|
14
|
+
"numberwidth" => { default_scope: :window, type: :int, default: 4 },
|
|
15
|
+
"colorcolumn" => { default_scope: :window, type: :string, default: nil },
|
|
16
|
+
"signcolumn" => { default_scope: :window, type: :string, default: "auto" },
|
|
17
|
+
"list" => { default_scope: :window, type: :bool, default: false },
|
|
18
|
+
"listchars" => { default_scope: :window, type: :string, default: "tab:>-,trail:-,nbsp:+" },
|
|
19
|
+
"showbreak" => { default_scope: :window, type: :string, default: ">" },
|
|
20
|
+
"showmatch" => { default_scope: :global, type: :bool, default: false },
|
|
21
|
+
"matchtime" => { default_scope: :global, type: :int, default: 5 },
|
|
22
|
+
"whichwrap" => { default_scope: :global, type: :string, default: "" },
|
|
23
|
+
"virtualedit" => { default_scope: :global, type: :string, default: "" },
|
|
6
24
|
"ignorecase" => { default_scope: :global, type: :bool, default: false },
|
|
7
25
|
"smartcase" => { default_scope: :global, type: :bool, default: false },
|
|
8
26
|
"hlsearch" => { default_scope: :global, type: :bool, default: true },
|
|
27
|
+
"incsearch" => { default_scope: :global, type: :bool, default: false },
|
|
28
|
+
"splitbelow" => { default_scope: :global, type: :bool, default: false },
|
|
29
|
+
"splitright" => { default_scope: :global, type: :bool, default: false },
|
|
30
|
+
"hidden" => { default_scope: :global, type: :bool, default: false },
|
|
31
|
+
"autowrite" => { default_scope: :global, type: :bool, default: false },
|
|
32
|
+
"clipboard" => { default_scope: :global, type: :string, default: "" },
|
|
33
|
+
"timeoutlen" => { default_scope: :global, type: :int, default: 1000 },
|
|
34
|
+
"ttimeoutlen" => { default_scope: :global, type: :int, default: 50 },
|
|
35
|
+
"backspace" => { default_scope: :global, type: :string, default: "indent,eol,start" },
|
|
36
|
+
"completeopt" => { default_scope: :global, type: :string, default: "menu,menuone,noselect" },
|
|
37
|
+
"pumheight" => { default_scope: :global, type: :int, default: 10 },
|
|
38
|
+
"wildmode" => { default_scope: :global, type: :string, default: "full" },
|
|
39
|
+
"wildignore" => { default_scope: :global, type: :string, default: "" },
|
|
40
|
+
"wildignorecase" => { default_scope: :global, type: :bool, default: false },
|
|
41
|
+
"wildmenu" => { default_scope: :global, type: :bool, default: false },
|
|
42
|
+
"termguicolors" => { default_scope: :global, type: :bool, default: false },
|
|
43
|
+
"path" => { default_scope: :buffer, type: :string, default: nil },
|
|
44
|
+
"suffixesadd" => { default_scope: :buffer, type: :string, default: nil },
|
|
45
|
+
"textwidth" => { default_scope: :buffer, type: :int, default: 0 },
|
|
46
|
+
"formatoptions" => { default_scope: :buffer, type: :string, default: nil },
|
|
47
|
+
"expandtab" => { default_scope: :buffer, type: :bool, default: false },
|
|
48
|
+
"shiftwidth" => { default_scope: :buffer, type: :int, default: 2 },
|
|
49
|
+
"softtabstop" => { default_scope: :buffer, type: :int, default: 0 },
|
|
50
|
+
"autoindent" => { default_scope: :buffer, type: :bool, default: true },
|
|
51
|
+
"smartindent" => { default_scope: :buffer, type: :bool, default: true },
|
|
52
|
+
"iskeyword" => { default_scope: :buffer, type: :string, default: nil },
|
|
9
53
|
"tabstop" => { default_scope: :buffer, type: :int, default: 2 },
|
|
10
|
-
"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" }
|
|
11
58
|
}.freeze
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
15
75
|
|
|
16
76
|
def initialize
|
|
17
77
|
@buffers = {}
|
|
18
78
|
@windows = {}
|
|
19
|
-
@
|
|
79
|
+
@layout_tree = nil
|
|
20
80
|
@tabpages = []
|
|
21
81
|
@current_tabpage_index = nil
|
|
22
82
|
@next_tabpage_id = 1
|
|
@@ -26,12 +86,17 @@ module RuVim
|
|
|
26
86
|
@current_window_id = nil
|
|
27
87
|
@alternate_buffer_id = nil
|
|
28
88
|
@mode = :normal
|
|
29
|
-
@window_layout = :single
|
|
30
89
|
@message = ""
|
|
31
90
|
@message_kind = :info
|
|
91
|
+
@message_deadline = nil
|
|
32
92
|
@pending_count = nil
|
|
33
93
|
@restricted_mode = false
|
|
94
|
+
@current_window_view_height_hint = 1
|
|
34
95
|
@running = true
|
|
96
|
+
@stdin_stream_stop_handler = nil
|
|
97
|
+
@open_path_handler = nil
|
|
98
|
+
@keymap_manager = nil
|
|
99
|
+
@app_action_handler = nil
|
|
35
100
|
@global_options = default_global_options
|
|
36
101
|
@command_line = CommandLine.new
|
|
37
102
|
@last_search = nil
|
|
@@ -45,8 +110,12 @@ module RuVim
|
|
|
45
110
|
@macros = {}
|
|
46
111
|
@macro_recording = nil
|
|
47
112
|
@visual_state = nil
|
|
113
|
+
@rich_state = nil
|
|
48
114
|
@quickfix_list = { items: [], index: nil }
|
|
49
115
|
@location_lists = Hash.new { |h, k| h[k] = { items: [], index: nil } }
|
|
116
|
+
@arglist = []
|
|
117
|
+
@arglist_index = 0
|
|
118
|
+
@hit_enter_lines = nil
|
|
50
119
|
end
|
|
51
120
|
|
|
52
121
|
def running?
|
|
@@ -97,6 +166,22 @@ module RuVim
|
|
|
97
166
|
@buffers.fetch(current_window.buffer_id)
|
|
98
167
|
end
|
|
99
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
|
+
|
|
100
185
|
def option_def(name)
|
|
101
186
|
OPTION_DEFS[name.to_s]
|
|
102
187
|
end
|
|
@@ -169,7 +254,7 @@ module RuVim
|
|
|
169
254
|
base = File.basename(p)
|
|
170
255
|
return "ruby" if %w[Gemfile Rakefile Guardfile].include?(base)
|
|
171
256
|
|
|
172
|
-
{
|
|
257
|
+
ext_ft = {
|
|
173
258
|
".rb" => "ruby",
|
|
174
259
|
".rake" => "ruby",
|
|
175
260
|
".ru" => "ruby",
|
|
@@ -187,8 +272,16 @@ module RuVim
|
|
|
187
272
|
".txt" => "text",
|
|
188
273
|
".html" => "html",
|
|
189
274
|
".css" => "css",
|
|
190
|
-
".sh" => "sh"
|
|
275
|
+
".sh" => "sh",
|
|
276
|
+
".tsv" => "tsv",
|
|
277
|
+
".csv" => "csv",
|
|
278
|
+
".scm" => "scheme",
|
|
279
|
+
".ss" => "scheme",
|
|
280
|
+
".sld" => "scheme"
|
|
191
281
|
}[File.extname(base).downcase]
|
|
282
|
+
return ext_ft if ext_ft
|
|
283
|
+
|
|
284
|
+
detect_filetype_from_shebang(p)
|
|
192
285
|
end
|
|
193
286
|
|
|
194
287
|
def registers
|
|
@@ -201,6 +294,12 @@ module RuVim
|
|
|
201
294
|
|
|
202
295
|
payload = write_register_payload(key, text: text.to_s, type: type.to_sym)
|
|
203
296
|
write_clipboard_register(key, payload)
|
|
297
|
+
if key == "\""
|
|
298
|
+
if (default_clip = clipboard_default_register_key)
|
|
299
|
+
mirror = write_register_payload(default_clip, text: payload[:text], type: payload[:type])
|
|
300
|
+
write_clipboard_register(default_clip, mirror)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
204
303
|
@registers["\""] = payload unless key == "\""
|
|
205
304
|
payload
|
|
206
305
|
end
|
|
@@ -225,6 +324,14 @@ module RuVim
|
|
|
225
324
|
|
|
226
325
|
def get_register(name = "\"")
|
|
227
326
|
key = name.to_s.downcase
|
|
327
|
+
if key == "\""
|
|
328
|
+
if (default_clip = clipboard_default_register_key)
|
|
329
|
+
if (payload = read_clipboard_register(default_clip))
|
|
330
|
+
@registers["\""] = dup_register_payload(payload)
|
|
331
|
+
return @registers["\""]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
228
335
|
return read_clipboard_register(key) if clipboard_register?(key)
|
|
229
336
|
|
|
230
337
|
@registers[key]
|
|
@@ -423,6 +530,53 @@ module RuVim
|
|
|
423
530
|
end
|
|
424
531
|
end
|
|
425
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
|
+
|
|
426
580
|
def add_empty_buffer(path: nil)
|
|
427
581
|
id = next_buffer_id
|
|
428
582
|
buffer = Buffer.new(id:, path:)
|
|
@@ -434,7 +588,7 @@ module RuVim
|
|
|
434
588
|
def add_virtual_buffer(kind:, name:, lines:, filetype: nil, readonly: true, modifiable: false)
|
|
435
589
|
id = next_buffer_id
|
|
436
590
|
buffer = Buffer.new(id:, lines:, kind:, name:, readonly:, modifiable:)
|
|
437
|
-
buffer
|
|
591
|
+
assign_filetype(buffer, filetype) if filetype
|
|
438
592
|
@buffers[id] = buffer
|
|
439
593
|
buffer
|
|
440
594
|
end
|
|
@@ -451,22 +605,40 @@ module RuVim
|
|
|
451
605
|
id = next_window_id
|
|
452
606
|
window = Window.new(id:, buffer_id:)
|
|
453
607
|
@windows[id] = window
|
|
454
|
-
|
|
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
|
|
455
619
|
@current_window_id ||= id
|
|
456
620
|
ensure_initial_tabpage!
|
|
457
621
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
458
622
|
window
|
|
459
623
|
end
|
|
460
624
|
|
|
461
|
-
def split_current_window(layout: :horizontal)
|
|
625
|
+
def split_current_window(layout: :horizontal, place: :after)
|
|
462
626
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
463
627
|
src = current_window
|
|
464
|
-
|
|
628
|
+
id = next_window_id
|
|
629
|
+
win = Window.new(id:, buffer_id: src.buffer_id)
|
|
630
|
+
@windows[id] = win
|
|
631
|
+
ensure_initial_tabpage!
|
|
465
632
|
win.cursor_x = src.cursor_x
|
|
466
633
|
win.cursor_y = src.cursor_y
|
|
467
634
|
win.row_offset = src.row_offset
|
|
468
635
|
win.col_offset = src.col_offset
|
|
469
|
-
|
|
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
|
+
|
|
470
642
|
@current_window_id = win.id
|
|
471
643
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
472
644
|
win
|
|
@@ -477,18 +649,21 @@ module RuVim
|
|
|
477
649
|
end
|
|
478
650
|
|
|
479
651
|
def close_window(id)
|
|
480
|
-
|
|
481
|
-
return nil if
|
|
482
|
-
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)
|
|
483
656
|
|
|
484
657
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
485
|
-
idx =
|
|
658
|
+
idx = leaves.index(id) || 0
|
|
486
659
|
@windows.delete(id)
|
|
487
|
-
@window_order.delete(id)
|
|
488
660
|
@location_lists.delete(id)
|
|
489
|
-
|
|
490
|
-
@
|
|
491
|
-
|
|
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
|
|
492
667
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
493
668
|
current_window
|
|
494
669
|
end
|
|
@@ -499,7 +674,8 @@ module RuVim
|
|
|
499
674
|
|
|
500
675
|
save_current_tabpage_state!
|
|
501
676
|
removed = @tabpages.delete_at(@current_tabpage_index)
|
|
502
|
-
|
|
677
|
+
removed_tree = removed && removed[:layout_tree]
|
|
678
|
+
tree_leaves(removed_tree).each do |wid|
|
|
503
679
|
@windows.delete(wid)
|
|
504
680
|
@location_lists.delete(wid)
|
|
505
681
|
end
|
|
@@ -517,42 +693,55 @@ module RuVim
|
|
|
517
693
|
end
|
|
518
694
|
|
|
519
695
|
def focus_next_window
|
|
520
|
-
|
|
696
|
+
order = window_order
|
|
697
|
+
return current_window if order.length <= 1
|
|
521
698
|
|
|
522
|
-
idx =
|
|
523
|
-
focus_window(
|
|
699
|
+
idx = order.index(@current_window_id) || 0
|
|
700
|
+
focus_window(order[(idx + 1) % order.length])
|
|
524
701
|
end
|
|
525
702
|
|
|
526
703
|
def focus_prev_window
|
|
527
|
-
|
|
704
|
+
order = window_order
|
|
705
|
+
return current_window if order.length <= 1
|
|
528
706
|
|
|
529
|
-
idx =
|
|
530
|
-
focus_window(
|
|
707
|
+
idx = order.index(@current_window_id) || 0
|
|
708
|
+
focus_window(order[(idx - 1) % order.length])
|
|
531
709
|
end
|
|
532
710
|
|
|
533
711
|
def focus_window_direction(dir)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
552
741
|
end
|
|
553
|
-
else
|
|
554
|
-
focus_next_window
|
|
555
742
|
end
|
|
743
|
+
|
|
744
|
+
best_id ? focus_window(best_id) : current_window
|
|
556
745
|
end
|
|
557
746
|
|
|
558
747
|
def switch_to_buffer(buffer_id)
|
|
@@ -570,9 +759,16 @@ module RuVim
|
|
|
570
759
|
end
|
|
571
760
|
|
|
572
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)
|
|
573
769
|
buffer = add_buffer_from_file(path)
|
|
574
770
|
switch_to_buffer(buffer.id)
|
|
575
|
-
echo(path && File.exist?(path)
|
|
771
|
+
echo("[New File]") unless path && File.exist?(path)
|
|
576
772
|
buffer
|
|
577
773
|
end
|
|
578
774
|
|
|
@@ -608,7 +804,7 @@ module RuVim
|
|
|
608
804
|
current_buffer.replace_all_lines!(intro_lines)
|
|
609
805
|
current_buffer.configure_special!(kind: :intro, name: "[Intro]", readonly: true, modifiable: false)
|
|
610
806
|
current_buffer.modified = false
|
|
611
|
-
current_buffer
|
|
807
|
+
assign_filetype(current_buffer, "help")
|
|
612
808
|
current_window.cursor_x = 0
|
|
613
809
|
current_window.cursor_y = 0
|
|
614
810
|
current_window.row_offset = 0
|
|
@@ -621,7 +817,7 @@ module RuVim
|
|
|
621
817
|
return false unless current_buffer.intro_buffer?
|
|
622
818
|
|
|
623
819
|
current_buffer.become_normal_empty_buffer!
|
|
624
|
-
current_buffer
|
|
820
|
+
assign_filetype(current_buffer, nil)
|
|
625
821
|
current_window.cursor_x = 0
|
|
626
822
|
current_window.cursor_y = 0
|
|
627
823
|
current_window.row_offset = 0
|
|
@@ -641,8 +837,56 @@ module RuVim
|
|
|
641
837
|
ids[(idx + step) % ids.length]
|
|
642
838
|
end
|
|
643
839
|
|
|
840
|
+
def delete_buffer(buffer_id)
|
|
841
|
+
id = buffer_id.to_i
|
|
842
|
+
buffer = @buffers[id]
|
|
843
|
+
return nil unless buffer
|
|
844
|
+
|
|
845
|
+
if @buffers.length <= 1
|
|
846
|
+
replacement = add_empty_buffer
|
|
847
|
+
else
|
|
848
|
+
replacement = nil
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
fallback_id =
|
|
852
|
+
if replacement
|
|
853
|
+
replacement.id
|
|
854
|
+
else
|
|
855
|
+
candidates = @buffers.keys.reject { |bid| bid == id }
|
|
856
|
+
alt = @alternate_buffer_id
|
|
857
|
+
(alt && alt != id && @buffers.key?(alt)) ? alt : candidates.first
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
@windows.each_value do |win|
|
|
861
|
+
next unless win.buffer_id == id
|
|
862
|
+
next unless fallback_id
|
|
863
|
+
|
|
864
|
+
win.buffer_id = fallback_id
|
|
865
|
+
win.cursor_x = 0
|
|
866
|
+
win.cursor_y = 0
|
|
867
|
+
win.row_offset = 0
|
|
868
|
+
win.col_offset = 0
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
@buffers.delete(id)
|
|
872
|
+
@local_marks.delete(id)
|
|
873
|
+
@alternate_buffer_id = nil if @alternate_buffer_id == id
|
|
874
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
875
|
+
ensure_bootstrap_buffer! if @buffers.empty?
|
|
876
|
+
true
|
|
877
|
+
end
|
|
878
|
+
|
|
644
879
|
def window_order
|
|
645
|
-
@
|
|
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
|
|
646
890
|
end
|
|
647
891
|
|
|
648
892
|
def tabpages
|
|
@@ -661,8 +905,12 @@ module RuVim
|
|
|
661
905
|
@tabpages.length
|
|
662
906
|
end
|
|
663
907
|
|
|
908
|
+
def tabpage_windows(tab)
|
|
909
|
+
tree_leaves(tab[:layout_tree])
|
|
910
|
+
end
|
|
911
|
+
|
|
664
912
|
def window_count
|
|
665
|
-
@
|
|
913
|
+
tree_leaves(@layout_tree).length
|
|
666
914
|
end
|
|
667
915
|
|
|
668
916
|
def quickfix_items
|
|
@@ -675,7 +923,7 @@ module RuVim
|
|
|
675
923
|
|
|
676
924
|
def set_quickfix_list(items)
|
|
677
925
|
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
678
|
-
@quickfix_list = { items: ary, index:
|
|
926
|
+
@quickfix_list = { items: ary, index: nil }
|
|
679
927
|
@quickfix_list
|
|
680
928
|
end
|
|
681
929
|
|
|
@@ -688,7 +936,21 @@ module RuVim
|
|
|
688
936
|
items = @quickfix_list[:items]
|
|
689
937
|
return nil if items.empty?
|
|
690
938
|
|
|
691
|
-
|
|
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
|
|
945
|
+
current_quickfix_item
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def select_quickfix(index)
|
|
949
|
+
items = @quickfix_list[:items]
|
|
950
|
+
return nil if items.empty?
|
|
951
|
+
|
|
952
|
+
i = [[index.to_i, 0].max, items.length - 1].min
|
|
953
|
+
@quickfix_list[:index] = i
|
|
692
954
|
current_quickfix_item
|
|
693
955
|
end
|
|
694
956
|
|
|
@@ -702,7 +964,7 @@ module RuVim
|
|
|
702
964
|
|
|
703
965
|
def set_location_list(items, window_id: current_window_id)
|
|
704
966
|
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
705
|
-
@location_lists[window_id] = { items: ary, index:
|
|
967
|
+
@location_lists[window_id] = { items: ary, index: nil }
|
|
706
968
|
@location_lists[window_id]
|
|
707
969
|
end
|
|
708
970
|
|
|
@@ -717,13 +979,41 @@ module RuVim
|
|
|
717
979
|
items = list[:items]
|
|
718
980
|
return nil if items.empty?
|
|
719
981
|
|
|
720
|
-
|
|
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
|
|
988
|
+
current_location_list_item(window_id)
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
def select_location_list(index, window_id: current_window_id)
|
|
992
|
+
list = location_list(window_id)
|
|
993
|
+
items = list[:items]
|
|
994
|
+
return nil if items.empty?
|
|
995
|
+
|
|
996
|
+
i = [[index.to_i, 0].max, items.length - 1].min
|
|
997
|
+
list[:index] = i
|
|
721
998
|
current_location_list_item(window_id)
|
|
722
999
|
end
|
|
723
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
|
+
|
|
724
1014
|
def find_window_ids_by_buffer_kind(kind)
|
|
725
1015
|
sym = kind.to_sym
|
|
726
|
-
|
|
1016
|
+
window_order.select do |wid|
|
|
727
1017
|
win = @windows[wid]
|
|
728
1018
|
buf = win && @buffers[win.buffer_id]
|
|
729
1019
|
buf && buf.kind == sym
|
|
@@ -735,9 +1025,8 @@ module RuVim
|
|
|
735
1025
|
save_current_tabpage_state!
|
|
736
1026
|
|
|
737
1027
|
with_tab_autosave_suspended do
|
|
738
|
-
@
|
|
1028
|
+
@layout_tree = nil
|
|
739
1029
|
@current_window_id = nil
|
|
740
|
-
@window_layout = :single
|
|
741
1030
|
|
|
742
1031
|
buffer = path ? add_buffer_from_file(path) : add_empty_buffer
|
|
743
1032
|
add_window(buffer_id: buffer.id)
|
|
@@ -764,6 +1053,7 @@ module RuVim
|
|
|
764
1053
|
@mode = :normal
|
|
765
1054
|
@pending_count = nil
|
|
766
1055
|
clear_visual
|
|
1056
|
+
@rich_state = nil
|
|
767
1057
|
end
|
|
768
1058
|
|
|
769
1059
|
def enter_insert_mode
|
|
@@ -779,30 +1069,77 @@ module RuVim
|
|
|
779
1069
|
|
|
780
1070
|
def cancel_command_line
|
|
781
1071
|
@command_line.clear
|
|
782
|
-
|
|
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
|
|
783
1082
|
end
|
|
784
1083
|
|
|
785
1084
|
def echo(msg)
|
|
786
1085
|
@message_kind = :info
|
|
787
1086
|
@message = msg.to_s
|
|
1087
|
+
@message_deadline = nil
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
def echo_temporary(msg, duration_seconds:)
|
|
1091
|
+
@message_kind = :info
|
|
1092
|
+
@message = msg.to_s
|
|
1093
|
+
dur = duration_seconds.to_f
|
|
1094
|
+
@message_deadline = dur.positive? ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + dur) : nil
|
|
1095
|
+
rescue StandardError
|
|
1096
|
+
@message_deadline = nil
|
|
788
1097
|
end
|
|
789
1098
|
|
|
790
1099
|
def echo_error(msg)
|
|
791
1100
|
@message_kind = :error
|
|
792
1101
|
@message = msg.to_s
|
|
1102
|
+
@message_deadline = nil
|
|
793
1103
|
end
|
|
794
1104
|
|
|
795
1105
|
def clear_message
|
|
796
1106
|
@message_kind = :info
|
|
797
1107
|
@message = ""
|
|
1108
|
+
@message_deadline = nil
|
|
798
1109
|
end
|
|
799
1110
|
|
|
800
1111
|
def message_error?
|
|
801
1112
|
!@message.to_s.empty? && @message_kind == :error
|
|
802
1113
|
end
|
|
803
1114
|
|
|
1115
|
+
def transient_message_timeout_seconds(now: nil)
|
|
1116
|
+
return nil unless @message_deadline
|
|
1117
|
+
return nil if message_error?
|
|
1118
|
+
return nil if command_line_active?
|
|
1119
|
+
|
|
1120
|
+
now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1121
|
+
[@message_deadline - now, 0.0].max
|
|
1122
|
+
rescue StandardError
|
|
1123
|
+
nil
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
def clear_expired_transient_message!(now: nil)
|
|
1127
|
+
return false unless @message_deadline
|
|
1128
|
+
return false if message_error?
|
|
1129
|
+
return false if command_line_active?
|
|
1130
|
+
|
|
1131
|
+
now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1132
|
+
return false if now < @message_deadline
|
|
1133
|
+
|
|
1134
|
+
clear_message
|
|
1135
|
+
true
|
|
1136
|
+
rescue StandardError
|
|
1137
|
+
false
|
|
1138
|
+
end
|
|
1139
|
+
|
|
804
1140
|
def text_viewport_size(rows:, cols:)
|
|
805
|
-
|
|
1141
|
+
# Reserve one status row + one command/error row at the bottom.
|
|
1142
|
+
text_rows = rows - 2
|
|
806
1143
|
[text_rows, cols]
|
|
807
1144
|
end
|
|
808
1145
|
|
|
@@ -814,8 +1151,69 @@ module RuVim
|
|
|
814
1151
|
command_line_active? || message_error?
|
|
815
1152
|
end
|
|
816
1153
|
|
|
1154
|
+
def assign_filetype(buffer, ft)
|
|
1155
|
+
buffer.options["filetype"] = ft
|
|
1156
|
+
buffer.lang_module = resolve_lang_module(ft)
|
|
1157
|
+
end
|
|
1158
|
+
|
|
817
1159
|
private
|
|
818
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
|
+
|
|
819
1217
|
def default_global_options
|
|
820
1218
|
OPTION_DEFS.each_with_object({}) { |(k, v), h| h[k] = v[:default] }
|
|
821
1219
|
end
|
|
@@ -873,7 +1271,7 @@ module RuVim
|
|
|
873
1271
|
|
|
874
1272
|
def assign_detected_filetype(buffer)
|
|
875
1273
|
ft = detect_filetype(buffer.path)
|
|
876
|
-
buffer
|
|
1274
|
+
assign_filetype(buffer, ft) if ft && !ft.empty?
|
|
877
1275
|
buffer
|
|
878
1276
|
end
|
|
879
1277
|
|
|
@@ -910,6 +1308,51 @@ module RuVim
|
|
|
910
1308
|
current_location
|
|
911
1309
|
end
|
|
912
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
|
+
|
|
913
1356
|
private
|
|
914
1357
|
|
|
915
1358
|
def first_nonblank_col(buffer, row)
|
|
@@ -950,6 +1393,15 @@ module RuVim
|
|
|
950
1393
|
key == "+" || key == "*"
|
|
951
1394
|
end
|
|
952
1395
|
|
|
1396
|
+
def clipboard_default_register_key
|
|
1397
|
+
spec = @global_options["clipboard"].to_s
|
|
1398
|
+
parts = spec.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
1399
|
+
return "+" if parts.include?("unnamedplus")
|
|
1400
|
+
return "*" if parts.include?("unnamed")
|
|
1401
|
+
|
|
1402
|
+
nil
|
|
1403
|
+
end
|
|
1404
|
+
|
|
953
1405
|
def write_clipboard_register(key, payload)
|
|
954
1406
|
return unless clipboard_register?(key.downcase)
|
|
955
1407
|
|
|
@@ -967,7 +1419,7 @@ module RuVim
|
|
|
967
1419
|
|
|
968
1420
|
def ensure_initial_tabpage!
|
|
969
1421
|
return unless @tabpages.empty?
|
|
970
|
-
return if @
|
|
1422
|
+
return if @layout_tree.nil?
|
|
971
1423
|
|
|
972
1424
|
@tabpages << new_tabpage_snapshot
|
|
973
1425
|
@current_tabpage_index = 0
|
|
@@ -981,18 +1433,16 @@ module RuVim
|
|
|
981
1433
|
end
|
|
982
1434
|
|
|
983
1435
|
def load_tabpage_state!(tab)
|
|
984
|
-
@
|
|
1436
|
+
@layout_tree = tree_deep_dup(tab[:layout_tree])
|
|
985
1437
|
@current_window_id = tab[:current_window_id]
|
|
986
|
-
@window_layout = tab[:window_layout]
|
|
987
1438
|
current_window
|
|
988
1439
|
end
|
|
989
1440
|
|
|
990
1441
|
def new_tabpage_snapshot(id: nil)
|
|
991
1442
|
{
|
|
992
1443
|
id: id || next_tabpage_id,
|
|
993
|
-
|
|
994
|
-
current_window_id: @current_window_id
|
|
995
|
-
window_layout: @window_layout
|
|
1444
|
+
layout_tree: tree_deep_dup(@layout_tree),
|
|
1445
|
+
current_window_id: @current_window_id
|
|
996
1446
|
}
|
|
997
1447
|
end
|
|
998
1448
|
|
|
@@ -1021,5 +1471,108 @@ module RuVim
|
|
|
1021
1471
|
ensure
|
|
1022
1472
|
@suspend_tab_autosave = prev
|
|
1023
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
|
|
1024
1577
|
end
|
|
1025
1578
|
end
|