ruvim 0.1.0 → 0.2.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/docs/binding.md +6 -0
- data/docs/command.md +16 -0
- data/docs/config.md +203 -84
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +39 -22
- data/docs/todo.md +187 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +2 -1
- data/lib/ruvim/app.rb +681 -123
- data/lib/ruvim/config_loader.rb +19 -5
- data/lib/ruvim/context.rb +0 -7
- data/lib/ruvim/dispatcher.rb +10 -0
- data/lib/ruvim/display_width.rb +25 -2
- data/lib/ruvim/editor.rb +173 -4
- data/lib/ruvim/global_commands.rb +500 -55
- data/lib/ruvim/input.rb +22 -10
- data/lib/ruvim/keyword_chars.rb +46 -0
- data/lib/ruvim/screen.rb +388 -53
- data/lib/ruvim/text_metrics.rb +26 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +35 -10
- data/lib/ruvim.rb +1 -0
- data/test/app_completion_test.rb +101 -0
- data/test/app_motion_test.rb +97 -2
- data/test/app_scenario_test.rb +270 -0
- data/test/app_startup_test.rb +10 -0
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +116 -0
- data/test/display_width_test.rb +18 -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/input_screen_integration_test.rb +26 -13
- data/test/screen_test.rb +166 -0
- data/test/window_test.rb +26 -0
- metadata +5 -1
data/lib/ruvim/config_loader.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module RuVim
|
|
2
2
|
class ConfigLoader
|
|
3
|
+
SAFE_FILETYPE_RE = /\A[a-zA-Z0-9_+-]+\z/.freeze
|
|
4
|
+
|
|
3
5
|
def initialize(command_registry:, ex_registry:, keymaps:, command_host:)
|
|
4
6
|
@command_registry = command_registry
|
|
5
7
|
@ex_registry = ex_registry
|
|
@@ -32,6 +34,7 @@ module RuVim
|
|
|
32
34
|
filetype = buffer.options["filetype"].to_s
|
|
33
35
|
return nil if filetype.empty?
|
|
34
36
|
return nil if buffer.options["__ftplugin_loaded__"] == filetype
|
|
37
|
+
return nil unless safe_filetype_name?(filetype)
|
|
35
38
|
|
|
36
39
|
path = ftplugin_path_for(filetype)
|
|
37
40
|
return nil unless path && File.file?(path)
|
|
@@ -58,11 +61,22 @@ module RuVim
|
|
|
58
61
|
|
|
59
62
|
def xdg_ftplugin_path(filetype)
|
|
60
63
|
base = ::ENV["XDG_CONFIG_HOME"]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
root =
|
|
65
|
+
if base && !base.empty?
|
|
66
|
+
File.join(base, "ruvim", "ftplugin")
|
|
67
|
+
else
|
|
68
|
+
File.expand_path("~/.config/ruvim/ftplugin")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
candidate = File.expand_path(File.join(root, "#{filetype}.rb"))
|
|
72
|
+
root_prefix = File.join(File.expand_path(root), "")
|
|
73
|
+
return nil unless candidate.start_with?(root_prefix)
|
|
74
|
+
|
|
75
|
+
candidate
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def safe_filetype_name?(filetype)
|
|
79
|
+
SAFE_FILETYPE_RE.match?(filetype.to_s)
|
|
66
80
|
end
|
|
67
81
|
end
|
|
68
82
|
end
|
data/lib/ruvim/context.rb
CHANGED
data/lib/ruvim/dispatcher.rb
CHANGED
|
@@ -19,6 +19,16 @@ module RuVim
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def dispatch_ex(editor, line)
|
|
22
|
+
raw = line.to_s.strip
|
|
23
|
+
if raw.start_with?("!")
|
|
24
|
+
command = raw[1..].to_s.strip
|
|
25
|
+
invocation = CommandInvocation.new(id: "__shell__", argv: [command])
|
|
26
|
+
ctx = Context.new(editor:, invocation:)
|
|
27
|
+
@command_host.ex_shell(ctx, command:)
|
|
28
|
+
editor.enter_normal_mode
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
if (sub = parse_global_substitute(line))
|
|
23
33
|
invocation = CommandInvocation.new(id: "__substitute__", kwargs: sub)
|
|
24
34
|
ctx = Context.new(editor:, invocation:)
|
data/lib/ruvim/display_width.rb
CHANGED
|
@@ -11,6 +11,25 @@ module RuVim
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
code = ch.ord
|
|
14
|
+
return cached_codepoint_width(code) if codepoint_cacheable?(code)
|
|
15
|
+
|
|
16
|
+
uncached_codepoint_width(code)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def codepoint_cacheable?(code)
|
|
20
|
+
!code.nil? && !code.zero?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def cached_codepoint_width(code)
|
|
24
|
+
aw = ambiguous_width
|
|
25
|
+
@codepoint_width_cache ||= {}
|
|
26
|
+
key = [code, aw]
|
|
27
|
+
return @codepoint_width_cache[key] if @codepoint_width_cache.key?(key)
|
|
28
|
+
|
|
29
|
+
@codepoint_width_cache[key] = uncached_codepoint_width(code)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def uncached_codepoint_width(code)
|
|
14
33
|
return 0 if code.zero?
|
|
15
34
|
return 0 if combining_mark?(code)
|
|
16
35
|
return 0 if zero_width_codepoint?(code)
|
|
@@ -102,9 +121,13 @@ module RuVim
|
|
|
102
121
|
|
|
103
122
|
def ambiguous_width
|
|
104
123
|
env = ::ENV["RUVIM_AMBIGUOUS_WIDTH"]
|
|
105
|
-
|
|
124
|
+
if !defined?(@ambiguous_width_cached) || @ambiguous_width_env != env
|
|
125
|
+
@ambiguous_width_env = env
|
|
126
|
+
@ambiguous_width_cached = (env == "2" ? 2 : 1)
|
|
127
|
+
@codepoint_width_cache = {}
|
|
128
|
+
end
|
|
106
129
|
|
|
107
|
-
|
|
130
|
+
@ambiguous_width_cached
|
|
108
131
|
end
|
|
109
132
|
end
|
|
110
133
|
end
|
data/lib/ruvim/editor.rb
CHANGED
|
@@ -3,15 +3,57 @@ module RuVim
|
|
|
3
3
|
OPTION_DEFS = {
|
|
4
4
|
"number" => { default_scope: :window, type: :bool, default: false },
|
|
5
5
|
"relativenumber" => { default_scope: :window, type: :bool, default: false },
|
|
6
|
+
"wrap" => { default_scope: :window, type: :bool, default: true },
|
|
7
|
+
"linebreak" => { default_scope: :window, type: :bool, default: false },
|
|
8
|
+
"breakindent" => { default_scope: :window, type: :bool, default: false },
|
|
9
|
+
"cursorline" => { default_scope: :window, type: :bool, default: false },
|
|
10
|
+
"scrolloff" => { default_scope: :window, type: :int, default: 0 },
|
|
11
|
+
"sidescrolloff" => { default_scope: :window, type: :int, default: 0 },
|
|
12
|
+
"numberwidth" => { default_scope: :window, type: :int, default: 4 },
|
|
13
|
+
"colorcolumn" => { default_scope: :window, type: :string, default: nil },
|
|
14
|
+
"signcolumn" => { default_scope: :window, type: :string, default: "auto" },
|
|
15
|
+
"list" => { default_scope: :window, type: :bool, default: false },
|
|
16
|
+
"listchars" => { default_scope: :window, type: :string, default: "tab:>-,trail:-,nbsp:+" },
|
|
17
|
+
"showbreak" => { default_scope: :window, type: :string, default: ">" },
|
|
18
|
+
"showmatch" => { default_scope: :global, type: :bool, default: false },
|
|
19
|
+
"matchtime" => { default_scope: :global, type: :int, default: 5 },
|
|
20
|
+
"whichwrap" => { default_scope: :global, type: :string, default: "" },
|
|
21
|
+
"virtualedit" => { default_scope: :global, type: :string, default: "" },
|
|
6
22
|
"ignorecase" => { default_scope: :global, type: :bool, default: false },
|
|
7
23
|
"smartcase" => { default_scope: :global, type: :bool, default: false },
|
|
8
24
|
"hlsearch" => { default_scope: :global, type: :bool, default: true },
|
|
25
|
+
"incsearch" => { default_scope: :global, type: :bool, default: false },
|
|
26
|
+
"splitbelow" => { default_scope: :global, type: :bool, default: false },
|
|
27
|
+
"splitright" => { default_scope: :global, type: :bool, default: false },
|
|
28
|
+
"hidden" => { default_scope: :global, type: :bool, default: false },
|
|
29
|
+
"autowrite" => { default_scope: :global, type: :bool, default: false },
|
|
30
|
+
"clipboard" => { default_scope: :global, type: :string, default: "" },
|
|
31
|
+
"timeoutlen" => { default_scope: :global, type: :int, default: 1000 },
|
|
32
|
+
"ttimeoutlen" => { default_scope: :global, type: :int, default: 50 },
|
|
33
|
+
"backspace" => { default_scope: :global, type: :string, default: "indent,eol,start" },
|
|
34
|
+
"completeopt" => { default_scope: :global, type: :string, default: "menu,menuone,noselect" },
|
|
35
|
+
"pumheight" => { default_scope: :global, type: :int, default: 10 },
|
|
36
|
+
"wildmode" => { default_scope: :global, type: :string, default: "full" },
|
|
37
|
+
"wildignore" => { default_scope: :global, type: :string, default: "" },
|
|
38
|
+
"wildignorecase" => { default_scope: :global, type: :bool, default: false },
|
|
39
|
+
"wildmenu" => { default_scope: :global, type: :bool, default: false },
|
|
40
|
+
"termguicolors" => { default_scope: :global, type: :bool, default: false },
|
|
41
|
+
"path" => { default_scope: :buffer, type: :string, default: nil },
|
|
42
|
+
"suffixesadd" => { default_scope: :buffer, type: :string, default: nil },
|
|
43
|
+
"textwidth" => { default_scope: :buffer, type: :int, default: 0 },
|
|
44
|
+
"formatoptions" => { default_scope: :buffer, type: :string, default: nil },
|
|
45
|
+
"expandtab" => { default_scope: :buffer, type: :bool, default: false },
|
|
46
|
+
"shiftwidth" => { default_scope: :buffer, type: :int, default: 2 },
|
|
47
|
+
"softtabstop" => { default_scope: :buffer, type: :int, default: 0 },
|
|
48
|
+
"autoindent" => { default_scope: :buffer, type: :bool, default: false },
|
|
49
|
+
"smartindent" => { default_scope: :buffer, type: :bool, default: false },
|
|
50
|
+
"iskeyword" => { default_scope: :buffer, type: :string, default: nil },
|
|
9
51
|
"tabstop" => { default_scope: :buffer, type: :int, default: 2 },
|
|
10
52
|
"filetype" => { default_scope: :buffer, type: :string, default: nil }
|
|
11
53
|
}.freeze
|
|
12
54
|
|
|
13
55
|
attr_reader :buffers, :windows
|
|
14
|
-
attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :window_layout, :restricted_mode
|
|
56
|
+
attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :window_layout, :restricted_mode, :current_window_view_height_hint
|
|
15
57
|
|
|
16
58
|
def initialize
|
|
17
59
|
@buffers = {}
|
|
@@ -29,8 +71,10 @@ module RuVim
|
|
|
29
71
|
@window_layout = :single
|
|
30
72
|
@message = ""
|
|
31
73
|
@message_kind = :info
|
|
74
|
+
@message_deadline = nil
|
|
32
75
|
@pending_count = nil
|
|
33
76
|
@restricted_mode = false
|
|
77
|
+
@current_window_view_height_hint = 1
|
|
34
78
|
@running = true
|
|
35
79
|
@global_options = default_global_options
|
|
36
80
|
@command_line = CommandLine.new
|
|
@@ -201,6 +245,12 @@ module RuVim
|
|
|
201
245
|
|
|
202
246
|
payload = write_register_payload(key, text: text.to_s, type: type.to_sym)
|
|
203
247
|
write_clipboard_register(key, payload)
|
|
248
|
+
if key == "\""
|
|
249
|
+
if (default_clip = clipboard_default_register_key)
|
|
250
|
+
mirror = write_register_payload(default_clip, text: payload[:text], type: payload[:type])
|
|
251
|
+
write_clipboard_register(default_clip, mirror)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
204
254
|
@registers["\""] = payload unless key == "\""
|
|
205
255
|
payload
|
|
206
256
|
end
|
|
@@ -225,6 +275,14 @@ module RuVim
|
|
|
225
275
|
|
|
226
276
|
def get_register(name = "\"")
|
|
227
277
|
key = name.to_s.downcase
|
|
278
|
+
if key == "\""
|
|
279
|
+
if (default_clip = clipboard_default_register_key)
|
|
280
|
+
if (payload = read_clipboard_register(default_clip))
|
|
281
|
+
@registers["\""] = dup_register_payload(payload)
|
|
282
|
+
return @registers["\""]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
228
286
|
return read_clipboard_register(key) if clipboard_register?(key)
|
|
229
287
|
|
|
230
288
|
@registers[key]
|
|
@@ -458,10 +516,16 @@ module RuVim
|
|
|
458
516
|
window
|
|
459
517
|
end
|
|
460
518
|
|
|
461
|
-
def split_current_window(layout: :horizontal)
|
|
519
|
+
def split_current_window(layout: :horizontal, place: :after)
|
|
462
520
|
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
463
521
|
src = current_window
|
|
464
|
-
|
|
522
|
+
id = next_window_id
|
|
523
|
+
win = Window.new(id:, buffer_id: src.buffer_id)
|
|
524
|
+
@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
|
+
ensure_initial_tabpage!
|
|
465
529
|
win.cursor_x = src.cursor_x
|
|
466
530
|
win.cursor_y = src.cursor_y
|
|
467
531
|
win.row_offset = src.row_offset
|
|
@@ -641,6 +705,45 @@ module RuVim
|
|
|
641
705
|
ids[(idx + step) % ids.length]
|
|
642
706
|
end
|
|
643
707
|
|
|
708
|
+
def delete_buffer(buffer_id)
|
|
709
|
+
id = buffer_id.to_i
|
|
710
|
+
buffer = @buffers[id]
|
|
711
|
+
return nil unless buffer
|
|
712
|
+
|
|
713
|
+
if @buffers.length <= 1
|
|
714
|
+
replacement = add_empty_buffer
|
|
715
|
+
else
|
|
716
|
+
replacement = nil
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
fallback_id =
|
|
720
|
+
if replacement
|
|
721
|
+
replacement.id
|
|
722
|
+
else
|
|
723
|
+
candidates = @buffers.keys.reject { |bid| bid == id }
|
|
724
|
+
alt = @alternate_buffer_id
|
|
725
|
+
(alt && alt != id && @buffers.key?(alt)) ? alt : candidates.first
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
@windows.each_value do |win|
|
|
729
|
+
next unless win.buffer_id == id
|
|
730
|
+
next unless fallback_id
|
|
731
|
+
|
|
732
|
+
win.buffer_id = fallback_id
|
|
733
|
+
win.cursor_x = 0
|
|
734
|
+
win.cursor_y = 0
|
|
735
|
+
win.row_offset = 0
|
|
736
|
+
win.col_offset = 0
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
@buffers.delete(id)
|
|
740
|
+
@local_marks.delete(id)
|
|
741
|
+
@alternate_buffer_id = nil if @alternate_buffer_id == id
|
|
742
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
743
|
+
ensure_bootstrap_buffer! if @buffers.empty?
|
|
744
|
+
true
|
|
745
|
+
end
|
|
746
|
+
|
|
644
747
|
def window_order
|
|
645
748
|
@window_order
|
|
646
749
|
end
|
|
@@ -692,6 +795,15 @@ module RuVim
|
|
|
692
795
|
current_quickfix_item
|
|
693
796
|
end
|
|
694
797
|
|
|
798
|
+
def select_quickfix(index)
|
|
799
|
+
items = @quickfix_list[:items]
|
|
800
|
+
return nil if items.empty?
|
|
801
|
+
|
|
802
|
+
i = [[index.to_i, 0].max, items.length - 1].min
|
|
803
|
+
@quickfix_list[:index] = i
|
|
804
|
+
current_quickfix_item
|
|
805
|
+
end
|
|
806
|
+
|
|
695
807
|
def location_list(window_id = current_window_id)
|
|
696
808
|
@location_lists[window_id]
|
|
697
809
|
end
|
|
@@ -721,6 +833,16 @@ module RuVim
|
|
|
721
833
|
current_location_list_item(window_id)
|
|
722
834
|
end
|
|
723
835
|
|
|
836
|
+
def select_location_list(index, window_id: current_window_id)
|
|
837
|
+
list = location_list(window_id)
|
|
838
|
+
items = list[:items]
|
|
839
|
+
return nil if items.empty?
|
|
840
|
+
|
|
841
|
+
i = [[index.to_i, 0].max, items.length - 1].min
|
|
842
|
+
list[:index] = i
|
|
843
|
+
current_location_list_item(window_id)
|
|
844
|
+
end
|
|
845
|
+
|
|
724
846
|
def find_window_ids_by_buffer_kind(kind)
|
|
725
847
|
sym = kind.to_sym
|
|
726
848
|
@window_order.select do |wid|
|
|
@@ -785,24 +907,62 @@ module RuVim
|
|
|
785
907
|
def echo(msg)
|
|
786
908
|
@message_kind = :info
|
|
787
909
|
@message = msg.to_s
|
|
910
|
+
@message_deadline = nil
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
def echo_temporary(msg, duration_seconds:)
|
|
914
|
+
@message_kind = :info
|
|
915
|
+
@message = msg.to_s
|
|
916
|
+
dur = duration_seconds.to_f
|
|
917
|
+
@message_deadline = dur.positive? ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + dur) : nil
|
|
918
|
+
rescue StandardError
|
|
919
|
+
@message_deadline = nil
|
|
788
920
|
end
|
|
789
921
|
|
|
790
922
|
def echo_error(msg)
|
|
791
923
|
@message_kind = :error
|
|
792
924
|
@message = msg.to_s
|
|
925
|
+
@message_deadline = nil
|
|
793
926
|
end
|
|
794
927
|
|
|
795
928
|
def clear_message
|
|
796
929
|
@message_kind = :info
|
|
797
930
|
@message = ""
|
|
931
|
+
@message_deadline = nil
|
|
798
932
|
end
|
|
799
933
|
|
|
800
934
|
def message_error?
|
|
801
935
|
!@message.to_s.empty? && @message_kind == :error
|
|
802
936
|
end
|
|
803
937
|
|
|
938
|
+
def transient_message_timeout_seconds(now: nil)
|
|
939
|
+
return nil unless @message_deadline
|
|
940
|
+
return nil if message_error?
|
|
941
|
+
return nil if command_line_active?
|
|
942
|
+
|
|
943
|
+
now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
944
|
+
[@message_deadline - now, 0.0].max
|
|
945
|
+
rescue StandardError
|
|
946
|
+
nil
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def clear_expired_transient_message!(now: nil)
|
|
950
|
+
return false unless @message_deadline
|
|
951
|
+
return false if message_error?
|
|
952
|
+
return false if command_line_active?
|
|
953
|
+
|
|
954
|
+
now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
955
|
+
return false if now < @message_deadline
|
|
956
|
+
|
|
957
|
+
clear_message
|
|
958
|
+
true
|
|
959
|
+
rescue StandardError
|
|
960
|
+
false
|
|
961
|
+
end
|
|
962
|
+
|
|
804
963
|
def text_viewport_size(rows:, cols:)
|
|
805
|
-
|
|
964
|
+
# Reserve one status row + one command/error row at the bottom.
|
|
965
|
+
text_rows = rows - 2
|
|
806
966
|
[text_rows, cols]
|
|
807
967
|
end
|
|
808
968
|
|
|
@@ -950,6 +1110,15 @@ module RuVim
|
|
|
950
1110
|
key == "+" || key == "*"
|
|
951
1111
|
end
|
|
952
1112
|
|
|
1113
|
+
def clipboard_default_register_key
|
|
1114
|
+
spec = @global_options["clipboard"].to_s
|
|
1115
|
+
parts = spec.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
1116
|
+
return "+" if parts.include?("unnamedplus")
|
|
1117
|
+
return "*" if parts.include?("unnamed")
|
|
1118
|
+
|
|
1119
|
+
nil
|
|
1120
|
+
end
|
|
1121
|
+
|
|
953
1122
|
def write_clipboard_register(key, payload)
|
|
954
1123
|
return unless clipboard_register?(key.downcase)
|
|
955
1124
|
|