ruvim 0.1.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 +7 -0
- data/.github/workflows/test.yml +15 -0
- data/README.md +135 -0
- data/Rakefile +36 -0
- data/docs/binding.md +125 -0
- data/docs/command.md +306 -0
- data/docs/config.md +155 -0
- data/docs/done.md +112 -0
- data/docs/plugin.md +559 -0
- data/docs/spec.md +655 -0
- data/docs/todo.md +63 -0
- data/docs/tutorial.md +490 -0
- data/docs/vim_diff.md +179 -0
- data/exe/ruvim +6 -0
- data/lib/ruvim/app.rb +1600 -0
- data/lib/ruvim/buffer.rb +421 -0
- data/lib/ruvim/cli.rb +264 -0
- data/lib/ruvim/clipboard.rb +73 -0
- data/lib/ruvim/command_invocation.rb +14 -0
- data/lib/ruvim/command_line.rb +63 -0
- data/lib/ruvim/command_registry.rb +38 -0
- data/lib/ruvim/config_dsl.rb +134 -0
- data/lib/ruvim/config_loader.rb +68 -0
- data/lib/ruvim/context.rb +26 -0
- data/lib/ruvim/dispatcher.rb +120 -0
- data/lib/ruvim/display_width.rb +110 -0
- data/lib/ruvim/editor.rb +1025 -0
- data/lib/ruvim/ex_command_registry.rb +80 -0
- data/lib/ruvim/global_commands.rb +1889 -0
- data/lib/ruvim/highlighter.rb +52 -0
- data/lib/ruvim/input.rb +66 -0
- data/lib/ruvim/keymap_manager.rb +96 -0
- data/lib/ruvim/screen.rb +452 -0
- data/lib/ruvim/terminal.rb +30 -0
- data/lib/ruvim/text_metrics.rb +96 -0
- data/lib/ruvim/version.rb +5 -0
- data/lib/ruvim/window.rb +71 -0
- data/lib/ruvim.rb +30 -0
- data/sig/ruvim.rbs +4 -0
- data/test/app_completion_test.rb +39 -0
- data/test/app_dot_repeat_test.rb +54 -0
- data/test/app_motion_test.rb +73 -0
- data/test/app_register_test.rb +47 -0
- data/test/app_scenario_test.rb +77 -0
- data/test/app_startup_test.rb +199 -0
- data/test/app_text_object_test.rb +54 -0
- data/test/app_unicode_behavior_test.rb +66 -0
- data/test/buffer_test.rb +72 -0
- data/test/cli_test.rb +165 -0
- data/test/config_dsl_test.rb +78 -0
- data/test/dispatcher_test.rb +124 -0
- data/test/editor_mark_test.rb +69 -0
- data/test/editor_register_test.rb +64 -0
- data/test/fixtures/render_basic_snapshot.txt +8 -0
- data/test/fixtures/render_basic_snapshot_nonumber.txt +8 -0
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +7 -0
- data/test/highlighter_test.rb +16 -0
- data/test/input_screen_integration_test.rb +69 -0
- data/test/keymap_manager_test.rb +48 -0
- data/test/render_snapshot_test.rb +70 -0
- data/test/screen_test.rb +123 -0
- data/test/search_option_test.rb +39 -0
- data/test/test_helper.rb +15 -0
- data/test/text_metrics_test.rb +42 -0
- data/test/window_test.rb +21 -0
- metadata +106 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
module Clipboard
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def available?
|
|
8
|
+
!backend.nil?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def read
|
|
12
|
+
cmd = backend&.dig(:read)
|
|
13
|
+
return nil unless cmd
|
|
14
|
+
|
|
15
|
+
out, status = Open3.capture2(*cmd)
|
|
16
|
+
return nil unless status.success?
|
|
17
|
+
|
|
18
|
+
out
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def write(text)
|
|
24
|
+
cmd = backend&.dig(:write)
|
|
25
|
+
return false unless cmd
|
|
26
|
+
|
|
27
|
+
Open3.popen2(*cmd) do |stdin, _stdout, wait|
|
|
28
|
+
stdin.write(text.to_s)
|
|
29
|
+
stdin.close
|
|
30
|
+
return wait.value.success?
|
|
31
|
+
end
|
|
32
|
+
rescue StandardError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_backend!
|
|
37
|
+
@backend = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def backend
|
|
41
|
+
@backend ||= detect_backend
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def detect_backend
|
|
45
|
+
return pbcopy_backend if command_available?("pbcopy") && command_available?("pbpaste")
|
|
46
|
+
return wayland_backend if command_available?("wl-copy") && command_available?("wl-paste")
|
|
47
|
+
return xclip_backend if command_available?("xclip")
|
|
48
|
+
return xsel_backend if command_available?("xsel")
|
|
49
|
+
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pbcopy_backend
|
|
54
|
+
{ write: %w[pbcopy], read: %w[pbpaste] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def wayland_backend
|
|
58
|
+
{ write: %w[wl-copy], read: %w[wl-paste -n] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def xclip_backend
|
|
62
|
+
{ write: %w[xclip -selection clipboard -in], read: %w[xclip -selection clipboard -out] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def xsel_backend
|
|
66
|
+
{ write: %w[xsel --clipboard --input], read: %w[xsel --clipboard --output] }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def command_available?(name)
|
|
70
|
+
system("which", name, out: File::NULL, err: File::NULL)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class CommandInvocation
|
|
3
|
+
attr_accessor :id, :argv, :kwargs, :count, :bang, :raw_keys
|
|
4
|
+
|
|
5
|
+
def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: nil, raw_keys: nil)
|
|
6
|
+
@id = id
|
|
7
|
+
@argv = argv || []
|
|
8
|
+
@kwargs = kwargs || {}
|
|
9
|
+
@count = count || 1
|
|
10
|
+
@bang = !!bang
|
|
11
|
+
@raw_keys = raw_keys
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class CommandLine
|
|
3
|
+
attr_reader :prefix, :text, :cursor
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
reset(prefix: ":")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def reset(prefix: ":")
|
|
10
|
+
@prefix = prefix
|
|
11
|
+
@text = +""
|
|
12
|
+
@cursor = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def clear
|
|
16
|
+
@text.clear
|
|
17
|
+
@cursor = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def replace_text(str)
|
|
21
|
+
@text = str.to_s.dup
|
|
22
|
+
@cursor = @text.length
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replace_span(start_idx, end_idx, replacement, cursor_at: :end)
|
|
26
|
+
s = [[start_idx.to_i, 0].max, @text.length].min
|
|
27
|
+
e = [[end_idx.to_i, s].max, @text.length].min
|
|
28
|
+
rep = replacement.to_s
|
|
29
|
+
@text = @text[0...s].to_s + rep + @text[e..].to_s
|
|
30
|
+
@cursor =
|
|
31
|
+
case cursor_at
|
|
32
|
+
when :start then s
|
|
33
|
+
when Integer then [[cursor_at, 0].max, @text.length].min
|
|
34
|
+
else
|
|
35
|
+
s + rep.length
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def insert(str)
|
|
40
|
+
@text.insert(@cursor, str)
|
|
41
|
+
@cursor += str.length
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def backspace
|
|
45
|
+
return if @cursor.zero?
|
|
46
|
+
|
|
47
|
+
@text.slice!(@cursor - 1)
|
|
48
|
+
@cursor -= 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def move_left
|
|
52
|
+
@cursor -= 1 if @cursor.positive?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def move_right
|
|
56
|
+
@cursor += 1 if @cursor < @text.length
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def content
|
|
60
|
+
"#{@prefix}#{@text}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class CommandRegistry
|
|
3
|
+
CommandSpec = Struct.new(
|
|
4
|
+
:id,
|
|
5
|
+
:call,
|
|
6
|
+
:desc,
|
|
7
|
+
:source,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
include Singleton
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@specs = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register(id, call:, desc: "", source: :builtin)
|
|
18
|
+
key = id.to_s
|
|
19
|
+
@specs[key] = CommandSpec.new(id: key, call:, desc:, source:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch(id)
|
|
23
|
+
@specs.fetch(id.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def registered?(id)
|
|
27
|
+
@specs.key?(id.to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def all
|
|
31
|
+
@specs.values.sort_by(&:id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear!
|
|
35
|
+
@specs.clear
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class ConfigDSL < BasicObject
|
|
3
|
+
def initialize(command_registry:, ex_registry:, keymaps:, command_host:, editor: nil, filetype: nil)
|
|
4
|
+
@command_registry = command_registry
|
|
5
|
+
@ex_registry = ex_registry
|
|
6
|
+
@keymaps = keymaps
|
|
7
|
+
@command_host = command_host
|
|
8
|
+
@editor = editor
|
|
9
|
+
@filetype = filetype&.to_s
|
|
10
|
+
@inline_map_command_seq = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def nmap(seq, command_id = nil, desc: "user keymap", **opts, &block)
|
|
14
|
+
command_id = inline_map_command_id(:normal, seq, desc:, &block) if block
|
|
15
|
+
raise ::ArgumentError, "command_id or block required" if command_id.nil?
|
|
16
|
+
|
|
17
|
+
if @filetype && !@filetype.empty?
|
|
18
|
+
@keymaps.bind_filetype(@filetype, seq, command_id.to_s, mode: :normal, **opts)
|
|
19
|
+
else
|
|
20
|
+
@keymaps.bind(:normal, seq, command_id.to_s, **opts)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def imap(seq, command_id = nil, desc: "user keymap", **opts, &block)
|
|
25
|
+
command_id = inline_map_command_id(:insert, seq, desc:, &block) if block
|
|
26
|
+
raise ::ArgumentError, "command_id or block required" if command_id.nil?
|
|
27
|
+
|
|
28
|
+
if @filetype && !@filetype.empty?
|
|
29
|
+
@keymaps.bind_filetype(@filetype, seq, command_id.to_s, mode: :insert, **opts)
|
|
30
|
+
else
|
|
31
|
+
@keymaps.bind(:insert, seq, command_id.to_s, **opts)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def map_global(seq, command_id = nil, mode: :normal, desc: "user keymap", **opts, &block)
|
|
36
|
+
command_id = inline_map_command_id(mode || :global, seq, desc:, &block) if block
|
|
37
|
+
raise ::ArgumentError, "command_id or block required" if command_id.nil?
|
|
38
|
+
|
|
39
|
+
if mode
|
|
40
|
+
@keymaps.bind(mode.to_sym, seq, command_id.to_s, **opts)
|
|
41
|
+
else
|
|
42
|
+
@keymaps.bind_global(seq, command_id.to_s, **opts)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def command(id, desc: "user command", &block)
|
|
47
|
+
raise ::ArgumentError, "block required" unless block
|
|
48
|
+
|
|
49
|
+
@command_registry.register(id.to_s, call: block, desc:, source: :user)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ex_command(name, desc: "user ex", aliases: [], nargs: :any, bang: false, &block)
|
|
53
|
+
raise ::ArgumentError, "block required" unless block
|
|
54
|
+
|
|
55
|
+
@ex_registry.unregister(name.to_s) if @ex_registry.registered?(name.to_s)
|
|
56
|
+
@ex_registry.register(name.to_s, call: block, desc:, aliases:, nargs:, bang:, source: :user)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Convenience: define an Ex command that forwards to an existing internal command ID.
|
|
60
|
+
def ex_command_call(name, command_id, desc: "user ex", aliases: [], nargs: :any, bang: false)
|
|
61
|
+
ex_command(name, desc:, aliases:, nargs:, bang:) do |ctx, argv:, kwargs:, bang:, count:|
|
|
62
|
+
spec = @command_registry.fetch(command_id.to_s)
|
|
63
|
+
@command_host.call(spec.call, ctx, argv:, kwargs:, bang:, count:)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def set(option_expr)
|
|
68
|
+
apply_option_expr(option_expr, scope: :auto)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def setlocal(option_expr)
|
|
72
|
+
apply_option_expr(option_expr, scope: :local)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def setglobal(option_expr)
|
|
76
|
+
apply_option_expr(option_expr, scope: :global)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def inline_map_command_id(mode, seq, desc:, &block)
|
|
82
|
+
raise ::ArgumentError, "block required" unless block
|
|
83
|
+
|
|
84
|
+
@inline_map_command_seq += 1
|
|
85
|
+
id = "user.keymap.#{normalize_mode_name(mode)}.#{sanitize_seq_label(seq)}.#{@inline_map_command_seq}"
|
|
86
|
+
command(id, desc:, &block)
|
|
87
|
+
id
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def normalize_mode_name(mode)
|
|
91
|
+
(mode || :global).to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sanitize_seq_label(seq)
|
|
95
|
+
raw =
|
|
96
|
+
case seq
|
|
97
|
+
when ::Array
|
|
98
|
+
seq.map(&:to_s).join("_")
|
|
99
|
+
else
|
|
100
|
+
seq.to_s
|
|
101
|
+
end
|
|
102
|
+
s = raw.gsub(/[^A-Za-z0-9]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
103
|
+
s.empty? ? "anonymous" : s
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def apply_option_expr(expr, scope:)
|
|
107
|
+
raise ::ArgumentError, "editor context required for option DSL" unless @editor
|
|
108
|
+
|
|
109
|
+
token = expr.to_s.strip
|
|
110
|
+
raise ::ArgumentError, "empty option expression" if token.empty?
|
|
111
|
+
|
|
112
|
+
if token.start_with?("no")
|
|
113
|
+
name = token[2..]
|
|
114
|
+
@editor.set_option(name, false, scope: resolve_scope(name, scope))
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if token.include?("=")
|
|
119
|
+
name, raw = token.split("=", 2)
|
|
120
|
+
@editor.set_option(name, raw, scope: resolve_scope(name, scope))
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@editor.set_option(token, true, scope: resolve_scope(token, scope))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolve_scope(name, scope)
|
|
128
|
+
return :auto if scope == :auto
|
|
129
|
+
return :global if scope == :global
|
|
130
|
+
|
|
131
|
+
@editor.option_default_scope(name) == :buffer ? :buffer : :window
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class ConfigLoader
|
|
3
|
+
def initialize(command_registry:, ex_registry:, keymaps:, command_host:)
|
|
4
|
+
@command_registry = command_registry
|
|
5
|
+
@ex_registry = ex_registry
|
|
6
|
+
@keymaps = keymaps
|
|
7
|
+
@command_host = command_host
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def load_default!
|
|
11
|
+
path = xdg_config_path
|
|
12
|
+
return nil unless File.file?(path)
|
|
13
|
+
|
|
14
|
+
load_file(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_file(path, editor: nil, filetype: nil)
|
|
18
|
+
dsl = ConfigDSL.new(
|
|
19
|
+
command_registry: @command_registry,
|
|
20
|
+
ex_registry: @ex_registry,
|
|
21
|
+
keymaps: @keymaps,
|
|
22
|
+
command_host: @command_host,
|
|
23
|
+
editor: editor,
|
|
24
|
+
filetype: filetype
|
|
25
|
+
)
|
|
26
|
+
code = File.read(path)
|
|
27
|
+
dsl.instance_eval(code, path, 1)
|
|
28
|
+
path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load_ftplugin!(editor, buffer)
|
|
32
|
+
filetype = buffer.options["filetype"].to_s
|
|
33
|
+
return nil if filetype.empty?
|
|
34
|
+
return nil if buffer.options["__ftplugin_loaded__"] == filetype
|
|
35
|
+
|
|
36
|
+
path = ftplugin_path_for(filetype)
|
|
37
|
+
return nil unless path && File.file?(path)
|
|
38
|
+
|
|
39
|
+
load_file(path, editor:, filetype:)
|
|
40
|
+
buffer.options["__ftplugin_loaded__"] = filetype
|
|
41
|
+
path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def xdg_config_path
|
|
47
|
+
base = ::ENV["XDG_CONFIG_HOME"]
|
|
48
|
+
if base && !base.empty?
|
|
49
|
+
File.join(base, "ruvim", "init.rb")
|
|
50
|
+
else
|
|
51
|
+
File.expand_path("~/.config/ruvim/init.rb")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ftplugin_path_for(filetype)
|
|
56
|
+
xdg_ftplugin_path(filetype)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def xdg_ftplugin_path(filetype)
|
|
60
|
+
base = ::ENV["XDG_CONFIG_HOME"]
|
|
61
|
+
if base && !base.empty?
|
|
62
|
+
File.join(base, "ruvim", "ftplugin", "#{filetype}.rb")
|
|
63
|
+
else
|
|
64
|
+
File.expand_path("~/.config/ruvim/ftplugin/#{filetype}.rb")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class Context
|
|
3
|
+
attr_reader :editor, :invocation
|
|
4
|
+
|
|
5
|
+
def initialize(editor:, invocation: nil)
|
|
6
|
+
@editor = editor
|
|
7
|
+
@invocation = invocation
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def window
|
|
11
|
+
editor.current_window
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def buffer
|
|
15
|
+
editor.current_buffer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def count
|
|
19
|
+
invocation&.count || 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bang?
|
|
23
|
+
invocation&.bang || false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class Dispatcher
|
|
5
|
+
ExCall = Struct.new(:name, :argv, :bang, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def initialize(command_registry: CommandRegistry.instance, ex_registry: ExCommandRegistry.instance, command_host: GlobalCommands.instance)
|
|
8
|
+
@command_registry = command_registry
|
|
9
|
+
@ex_registry = ex_registry
|
|
10
|
+
@command_host = command_host
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def dispatch(editor, invocation)
|
|
14
|
+
spec = @command_registry.fetch(invocation.id)
|
|
15
|
+
ctx = Context.new(editor:, invocation:)
|
|
16
|
+
@command_host.call(spec.call, ctx, argv: invocation.argv, kwargs: invocation.kwargs, bang: invocation.bang, count: invocation.count)
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
editor.echo_error("Error: #{e.message}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def dispatch_ex(editor, line)
|
|
22
|
+
if (sub = parse_global_substitute(line))
|
|
23
|
+
invocation = CommandInvocation.new(id: "__substitute__", kwargs: sub)
|
|
24
|
+
ctx = Context.new(editor:, invocation:)
|
|
25
|
+
@command_host.ex_substitute(ctx, **sub)
|
|
26
|
+
editor.enter_normal_mode
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
parsed = parse_ex(line)
|
|
31
|
+
return if parsed.nil?
|
|
32
|
+
|
|
33
|
+
spec = @ex_registry.fetch(parsed.name)
|
|
34
|
+
validate_ex_args!(spec, parsed.argv, parsed.bang)
|
|
35
|
+
invocation = CommandInvocation.new(id: spec.name, argv: parsed.argv, bang: parsed.bang)
|
|
36
|
+
ctx = Context.new(editor:, invocation:)
|
|
37
|
+
@command_host.call(spec.call, ctx, argv: parsed.argv, bang: parsed.bang, count: 1, kwargs: {})
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
editor.echo_error("Error: #{e.message}")
|
|
40
|
+
ensure
|
|
41
|
+
editor.enter_normal_mode
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_ex(line)
|
|
45
|
+
raw = line.to_s.strip
|
|
46
|
+
return nil if raw.empty?
|
|
47
|
+
|
|
48
|
+
tokens = Shellwords.shellsplit(raw)
|
|
49
|
+
return nil if tokens.empty?
|
|
50
|
+
|
|
51
|
+
head = tokens.shift
|
|
52
|
+
bang = head.end_with?("!")
|
|
53
|
+
name = bang ? head[0...-1] : head
|
|
54
|
+
ExCall.new(name:, argv: tokens, bang:)
|
|
55
|
+
rescue ArgumentError => e
|
|
56
|
+
raise RuVim::CommandError, "Parse error: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_global_substitute(line)
|
|
60
|
+
raw = line.to_s.strip
|
|
61
|
+
return nil unless raw.start_with?("%s")
|
|
62
|
+
return nil if raw.length < 4
|
|
63
|
+
|
|
64
|
+
delim = raw[2]
|
|
65
|
+
return nil if delim.nil? || delim =~ /\s/
|
|
66
|
+
i = 3
|
|
67
|
+
pat, i = parse_delimited_segment(raw, i, delim)
|
|
68
|
+
return nil unless pat
|
|
69
|
+
rep, i = parse_delimited_segment(raw, i, delim)
|
|
70
|
+
return nil unless rep
|
|
71
|
+
flags = raw[i..].to_s
|
|
72
|
+
{
|
|
73
|
+
pattern: pat,
|
|
74
|
+
replacement: rep,
|
|
75
|
+
global: flags.include?("g")
|
|
76
|
+
}
|
|
77
|
+
rescue StandardError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def parse_delimited_segment(str, idx, delim)
|
|
84
|
+
out = +""
|
|
85
|
+
i = idx
|
|
86
|
+
while i < str.length
|
|
87
|
+
ch = str[i]
|
|
88
|
+
if ch == "\\"
|
|
89
|
+
nxt = str[i + 1]
|
|
90
|
+
return nil unless nxt
|
|
91
|
+
out << "\\"
|
|
92
|
+
out << nxt
|
|
93
|
+
i += 2
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
if ch == delim
|
|
97
|
+
return [out, i + 1]
|
|
98
|
+
end
|
|
99
|
+
out << ch
|
|
100
|
+
i += 1
|
|
101
|
+
end
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_ex_args!(spec, argv, bang)
|
|
106
|
+
case spec.nargs
|
|
107
|
+
when 0
|
|
108
|
+
raise RuVim::CommandError, "#{spec.name} takes no arguments" unless argv.empty?
|
|
109
|
+
when 1
|
|
110
|
+
raise RuVim::CommandError, "#{spec.name} requires one argument" unless argv.length == 1
|
|
111
|
+
when :maybe_one
|
|
112
|
+
raise RuVim::CommandError, "#{spec.name} takes at most one argument" unless argv.length <= 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if bang && !spec.bang
|
|
116
|
+
raise RuVim::CommandError, "#{spec.name} does not accept !"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
module DisplayWidth
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def cell_width(ch, col: 0, tabstop: 2)
|
|
6
|
+
return 1 if ch.nil? || ch.empty?
|
|
7
|
+
|
|
8
|
+
if ch == "\t"
|
|
9
|
+
width = tabstop - (col % tabstop)
|
|
10
|
+
return width.zero? ? tabstop : width
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
code = ch.ord
|
|
14
|
+
return 0 if code.zero?
|
|
15
|
+
return 0 if combining_mark?(code)
|
|
16
|
+
return 0 if zero_width_codepoint?(code)
|
|
17
|
+
return ambiguous_width if ambiguous_codepoint?(code)
|
|
18
|
+
return 2 if emoji_codepoint?(code)
|
|
19
|
+
return 2 if wide_codepoint?(code)
|
|
20
|
+
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def display_width(str, tabstop: 2, start_col: 0)
|
|
25
|
+
col = start_col
|
|
26
|
+
str.to_s.each_char { |ch| col += cell_width(ch, col:, tabstop:) }
|
|
27
|
+
col - start_col
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def expand_tabs(str, tabstop: 2, start_col: 0)
|
|
31
|
+
col = start_col
|
|
32
|
+
out = +""
|
|
33
|
+
str.to_s.each_char do |ch|
|
|
34
|
+
if ch == "\t"
|
|
35
|
+
n = cell_width(ch, col:, tabstop:)
|
|
36
|
+
out << (" " * n)
|
|
37
|
+
col += n
|
|
38
|
+
else
|
|
39
|
+
out << ch
|
|
40
|
+
col += cell_width(ch, col:, tabstop:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
out
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def combining_mark?(code)
|
|
47
|
+
(0x0300..0x036F).cover?(code) ||
|
|
48
|
+
(0x1AB0..0x1AFF).cover?(code) ||
|
|
49
|
+
(0x1DC0..0x1DFF).cover?(code) ||
|
|
50
|
+
(0x20D0..0x20FF).cover?(code) ||
|
|
51
|
+
(0xFE20..0xFE2F).cover?(code)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def zero_width_codepoint?(code)
|
|
55
|
+
(0x200D..0x200D).cover?(code) || # ZWJ
|
|
56
|
+
(0xFE00..0xFE0F).cover?(code) || # variation selectors
|
|
57
|
+
(0xE0100..0xE01EF).cover?(code) # variation selectors supplement
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def wide_codepoint?(code)
|
|
61
|
+
(0x1100..0x115F).cover?(code) ||
|
|
62
|
+
(0x2329..0x232A).cover?(code) ||
|
|
63
|
+
(0x2E80..0xA4CF).cover?(code) ||
|
|
64
|
+
(0xAC00..0xD7A3).cover?(code) ||
|
|
65
|
+
(0xF900..0xFAFF).cover?(code) ||
|
|
66
|
+
(0xFE10..0xFE19).cover?(code) ||
|
|
67
|
+
(0xFE30..0xFE6F).cover?(code) ||
|
|
68
|
+
(0xFF00..0xFF60).cover?(code) ||
|
|
69
|
+
(0xFFE0..0xFFE6).cover?(code)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def emoji_codepoint?(code)
|
|
73
|
+
(0x1F300..0x1FAFF).cover?(code) ||
|
|
74
|
+
(0x2600..0x27BF).cover?(code)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ambiguous_codepoint?(code)
|
|
78
|
+
(0x00A1..0x00A1).cover?(code) ||
|
|
79
|
+
(0x00A4..0x00A4).cover?(code) ||
|
|
80
|
+
(0x00A7..0x00A8).cover?(code) ||
|
|
81
|
+
(0x00AA..0x00AA).cover?(code) ||
|
|
82
|
+
(0x00AD..0x00AE).cover?(code) ||
|
|
83
|
+
(0x00B0..0x00B4).cover?(code) ||
|
|
84
|
+
(0x00B6..0x00BA).cover?(code) ||
|
|
85
|
+
(0x00BC..0x00BF).cover?(code) ||
|
|
86
|
+
(0x0391..0x03A9).cover?(code) ||
|
|
87
|
+
(0x03B1..0x03C9).cover?(code) ||
|
|
88
|
+
(0x2010..0x2010).cover?(code) ||
|
|
89
|
+
(0x2013..0x2016).cover?(code) ||
|
|
90
|
+
(0x2018..0x2019).cover?(code) ||
|
|
91
|
+
(0x201C..0x201D).cover?(code) ||
|
|
92
|
+
(0x2020..0x2022).cover?(code) ||
|
|
93
|
+
(0x2024..0x2027).cover?(code) ||
|
|
94
|
+
(0x2030..0x2030).cover?(code) ||
|
|
95
|
+
(0x2032..0x2033).cover?(code) ||
|
|
96
|
+
(0x2035..0x2035).cover?(code) ||
|
|
97
|
+
(0x203B..0x203B).cover?(code) ||
|
|
98
|
+
(0x203E..0x203E).cover?(code) ||
|
|
99
|
+
(0x2460..0x24E9).cover?(code) ||
|
|
100
|
+
(0x2500..0x257F).cover?(code)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ambiguous_width
|
|
104
|
+
env = ::ENV["RUVIM_AMBIGUOUS_WIDTH"]
|
|
105
|
+
return 2 if env == "2"
|
|
106
|
+
|
|
107
|
+
1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|