ruvim 0.3.0 → 0.6.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/AGENTS.md +68 -7
- data/README.md +30 -7
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +18 -1
- data/docs/command.md +156 -10
- data/docs/config.md +10 -2
- data/docs/done.md +23 -0
- data/docs/spec.md +162 -25
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +33 -1
- data/docs/vim_diff.md +31 -8
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +246 -2525
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +43 -20
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +74 -80
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +255 -0
- data/lib/ruvim/git/branch.rb +112 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +125 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +351 -77
- data/lib/ruvim/highlighter.rb +4 -11
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +43 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +40 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -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/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +30 -7
- data/lib/ruvim/screen.rb +135 -84
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +31 -4
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +65 -16
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +182 -8
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +77 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +134 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/display_width_test.rb +41 -0
- data/test/ex_command_registry_test.rb +106 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +198 -0
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +792 -0
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +169 -0
- data/test/indent_test.rb +223 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +279 -23
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/search_option_test.rb +19 -0
- data/test/stream_test.rb +165 -0
- data/test/test_helper.rb +9 -0
- data/test/window_test.rb +59 -0
- metadata +68 -2
data/lib/ruvim/app.rb
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require_relative "file_watcher"
|
|
6
|
+
require_relative "stream_mixer"
|
|
7
|
+
require_relative "completion_manager"
|
|
8
|
+
require_relative "key_handler"
|
|
5
9
|
|
|
6
10
|
module RuVim
|
|
7
11
|
class App
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
StartupState = Struct.new(
|
|
13
|
+
:readonly, :diff_mode, :quickfix_errorfile, :session_file,
|
|
14
|
+
:nomodifiable, :follow, :time_path, :time_origin, :timeline,
|
|
15
|
+
:open_layout, :open_count, :skip_user_config, :config_path,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
def initialize(path: nil, paths: nil, stdin: STDIN, ui_stdin: nil, stdin_stream_mode: false, stdout: STDOUT, pre_config_actions: [], startup_actions: [], clean: false, skip_user_config: false, config_path: nil, readonly: false, diff_mode: false, quickfix_errorfile: nil, session_file: nil, nomodifiable: false, follow: false, restricted: false, verbose_level: 0, verbose_io: STDERR, startup_time_path: nil, startup_open_layout: nil, startup_open_count: nil)
|
|
14
19
|
startup_paths = Array(paths || path).compact
|
|
15
20
|
@ui_stdin = ui_stdin || stdin
|
|
16
21
|
@stdin_stream_mode = !!stdin_stream_mode
|
|
17
|
-
@stdin_stream_source = @stdin_stream_mode ? stdin : nil
|
|
18
22
|
@editor = Editor.new
|
|
19
23
|
@terminal = Terminal.new(stdin: @ui_stdin, stdout:)
|
|
20
24
|
@input = Input.new(@ui_stdin)
|
|
@@ -22,41 +26,57 @@ module RuVim
|
|
|
22
26
|
@dispatcher = Dispatcher.new
|
|
23
27
|
@keymaps = KeymapManager.new
|
|
24
28
|
@signal_r, @signal_w = IO.pipe
|
|
25
|
-
@stream_event_queue = nil
|
|
26
|
-
@stream_reader_thread = nil
|
|
27
|
-
@stream_buffer_id = nil
|
|
28
|
-
@stream_stop_requested = false
|
|
29
|
-
@async_file_loads = {}
|
|
30
|
-
@cmdline_history = Hash.new { |h, k| h[k] = [] }
|
|
31
|
-
@cmdline_history_index = nil
|
|
32
|
-
@cmdline_completion = nil
|
|
33
|
-
@pending_key_deadline = nil
|
|
34
|
-
@pending_ambiguous_invocation = nil
|
|
35
|
-
@insert_start_location = nil
|
|
36
|
-
@incsearch_preview = nil
|
|
37
29
|
@needs_redraw = true
|
|
38
30
|
@clean_mode = clean
|
|
39
|
-
@
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
@startup = StartupState.new(
|
|
32
|
+
skip_user_config: skip_user_config,
|
|
33
|
+
config_path: config_path,
|
|
34
|
+
readonly: readonly,
|
|
35
|
+
diff_mode: diff_mode,
|
|
36
|
+
quickfix_errorfile: quickfix_errorfile,
|
|
37
|
+
session_file: session_file,
|
|
38
|
+
nomodifiable: nomodifiable,
|
|
39
|
+
follow: follow,
|
|
40
|
+
time_path: startup_time_path,
|
|
41
|
+
time_origin: monotonic_now,
|
|
42
|
+
timeline: [],
|
|
43
|
+
open_layout: startup_open_layout,
|
|
44
|
+
open_count: startup_open_count
|
|
45
|
+
)
|
|
46
46
|
@restricted_mode = restricted
|
|
47
47
|
@verbose_level = verbose_level.to_i
|
|
48
48
|
@verbose_io = verbose_io
|
|
49
|
-
|
|
50
|
-
@
|
|
51
|
-
@
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
|
|
50
|
+
@stream_mixer = StreamMixer.new(editor: @editor, signal_w: @signal_w)
|
|
51
|
+
@completion = CompletionManager.new(
|
|
52
|
+
editor: @editor,
|
|
53
|
+
terminal: @terminal,
|
|
54
|
+
verbose_logger: method(:verbose_log)
|
|
55
|
+
)
|
|
56
|
+
@key_handler = KeyHandler.new(
|
|
57
|
+
editor: @editor,
|
|
58
|
+
dispatcher: @dispatcher,
|
|
59
|
+
keymaps: @keymaps,
|
|
60
|
+
terminal: @terminal,
|
|
61
|
+
screen: @screen,
|
|
62
|
+
completion: @completion,
|
|
63
|
+
stream_mixer: @stream_mixer
|
|
64
|
+
)
|
|
65
|
+
|
|
54
66
|
@editor.restricted_mode = @restricted_mode
|
|
55
|
-
@editor.
|
|
56
|
-
@editor.open_path_handler = method(:open_path_with_large_file_support)
|
|
67
|
+
@editor.open_path_handler = @stream_mixer.method(:open_path_with_large_file_support)
|
|
57
68
|
@editor.keymap_manager = @keymaps
|
|
58
|
-
@editor.app_action_handler = method(:handle_editor_app_action)
|
|
59
|
-
|
|
69
|
+
@editor.app_action_handler = @key_handler.method(:handle_editor_app_action)
|
|
70
|
+
@editor.git_stream_handler = @stream_mixer.method(:start_git_stream_command)
|
|
71
|
+
@editor.git_stream_stop_handler = @stream_mixer.method(:stop_git_stream!)
|
|
72
|
+
@editor.run_stream_handler = @stream_mixer.method(:start_command_stream!)
|
|
73
|
+
@editor.shell_executor = ->(command) {
|
|
74
|
+
result = @terminal.suspend_for_shell(command)
|
|
75
|
+
@screen.invalidate_cache!
|
|
76
|
+
result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@completion.load_history!
|
|
60
80
|
|
|
61
81
|
startup_mark("init.start")
|
|
62
82
|
register_builtins!
|
|
@@ -74,12 +94,12 @@ module RuVim
|
|
|
74
94
|
|
|
75
95
|
if @stdin_stream_mode && startup_paths.empty?
|
|
76
96
|
verbose_log(1, "startup: stdin stream buffer")
|
|
77
|
-
prepare_stdin_stream_buffer!
|
|
97
|
+
@stdin_stream_buf = @stream_mixer.prepare_stdin_stream_buffer!(stdin)
|
|
78
98
|
elsif startup_paths.empty?
|
|
79
99
|
verbose_log(1, "startup: intro")
|
|
80
100
|
@editor.show_intro_buffer_if_applicable!
|
|
81
101
|
else
|
|
82
|
-
verbose_log(1, "startup: open_paths #{startup_paths.inspect} layout=#{@
|
|
102
|
+
verbose_log(1, "startup: open_paths #{startup_paths.inspect} layout=#{@startup.open_layout || :single}")
|
|
83
103
|
open_startup_paths!(startup_paths)
|
|
84
104
|
end
|
|
85
105
|
startup_mark("buffers.opened")
|
|
@@ -90,14 +110,15 @@ module RuVim
|
|
|
90
110
|
verbose_log(1, "startup: run_startup_actions count=#{Array(startup_actions).length}")
|
|
91
111
|
run_startup_actions!(startup_actions)
|
|
92
112
|
startup_mark("startup_actions.done")
|
|
93
|
-
|
|
113
|
+
@stream_mixer.start_pending_stdin! if @stdin_stream_buf
|
|
94
114
|
write_startuptime_log!
|
|
115
|
+
@startup = nil
|
|
95
116
|
end
|
|
96
117
|
|
|
97
118
|
def run
|
|
98
119
|
@terminal.with_ui do
|
|
99
120
|
loop do
|
|
100
|
-
@needs_redraw = true if
|
|
121
|
+
@needs_redraw = true if @stream_mixer.drain_events!
|
|
101
122
|
if @needs_redraw
|
|
102
123
|
@screen.render(@editor)
|
|
103
124
|
@needs_redraw = false
|
|
@@ -106,29 +127,39 @@ module RuVim
|
|
|
106
127
|
|
|
107
128
|
key = @input.read_key(
|
|
108
129
|
wakeup_ios: [@signal_r],
|
|
109
|
-
timeout: loop_timeout_seconds,
|
|
110
|
-
esc_timeout: escape_sequence_timeout_seconds
|
|
130
|
+
timeout: @key_handler.loop_timeout_seconds,
|
|
131
|
+
esc_timeout: @key_handler.escape_sequence_timeout_seconds
|
|
111
132
|
)
|
|
112
133
|
if key.nil?
|
|
113
|
-
|
|
114
|
-
clear_expired_transient_message_if_any
|
|
134
|
+
@needs_redraw = true if @key_handler.handle_idle_timeout
|
|
115
135
|
next
|
|
116
136
|
end
|
|
117
137
|
|
|
118
|
-
|
|
138
|
+
needs_redraw_from_key = @key_handler.handle(key)
|
|
119
139
|
@needs_redraw = true
|
|
140
|
+
load_current_ftplugin!
|
|
141
|
+
|
|
142
|
+
# Force redraw after suspend_to_shell
|
|
143
|
+
@needs_redraw = true if needs_redraw_from_key
|
|
120
144
|
|
|
121
145
|
# Batch insert-mode keystrokes to avoid per-char rendering during paste
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
if @editor.mode == :insert && @input.has_pending_input?
|
|
147
|
+
@key_handler.paste_batch = true
|
|
148
|
+
begin
|
|
149
|
+
while @editor.mode == :insert && @input.has_pending_input?
|
|
150
|
+
batch_key = @input.read_key(timeout: 0, esc_timeout: 0)
|
|
151
|
+
break unless batch_key
|
|
152
|
+
@key_handler.handle(batch_key)
|
|
153
|
+
end
|
|
154
|
+
ensure
|
|
155
|
+
@key_handler.paste_batch = false
|
|
156
|
+
end
|
|
126
157
|
end
|
|
127
158
|
end
|
|
128
159
|
end
|
|
129
160
|
ensure
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
@stream_mixer.shutdown!
|
|
162
|
+
@completion.save_history!
|
|
132
163
|
end
|
|
133
164
|
|
|
134
165
|
def run_startup_actions!(actions, log_prefix: "startup")
|
|
@@ -140,58 +171,10 @@ module RuVim
|
|
|
140
171
|
|
|
141
172
|
private
|
|
142
173
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
[@pending_key_deadline - monotonic_now, 0.0].max
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def loop_timeout_seconds
|
|
150
|
-
now = monotonic_now
|
|
151
|
-
timeouts = []
|
|
152
|
-
if @pending_key_deadline
|
|
153
|
-
timeouts << [@pending_key_deadline - now, 0.0].max
|
|
154
|
-
end
|
|
155
|
-
if (msg_to = @editor.transient_message_timeout_seconds(now:))
|
|
156
|
-
timeouts << msg_to
|
|
157
|
-
end
|
|
158
|
-
timeouts.min
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def pending_key_timeout_expired?
|
|
162
|
-
@pending_key_deadline && monotonic_now >= @pending_key_deadline
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def escape_sequence_timeout_seconds
|
|
166
|
-
ms = @editor.global_options["ttimeoutlen"].to_i
|
|
167
|
-
ms = 50 if ms <= 0
|
|
168
|
-
ms / 1000.0
|
|
169
|
-
rescue StandardError
|
|
170
|
-
0.005
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def arm_pending_key_timeout
|
|
174
|
-
ms = @editor.global_options["timeoutlen"].to_i
|
|
175
|
-
ms = 1000 if ms <= 0
|
|
176
|
-
@pending_key_deadline = monotonic_now + (ms / 1000.0)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def clear_pending_key_timeout
|
|
180
|
-
@pending_key_deadline = nil
|
|
181
|
-
@pending_ambiguous_invocation = nil
|
|
174
|
+
def clear_expired_transient_message_if_any
|
|
175
|
+
@needs_redraw = true if @key_handler.handle_idle_timeout
|
|
182
176
|
end
|
|
183
177
|
|
|
184
|
-
def handle_pending_key_timeout
|
|
185
|
-
inv = @pending_ambiguous_invocation
|
|
186
|
-
clear_pending_key_timeout
|
|
187
|
-
if inv
|
|
188
|
-
@dispatcher.dispatch(@editor, dup_invocation(inv))
|
|
189
|
-
elsif @pending_keys && !@pending_keys.empty?
|
|
190
|
-
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
191
|
-
end
|
|
192
|
-
@editor.pending_count = nil
|
|
193
|
-
@pending_keys = []
|
|
194
|
-
end
|
|
195
178
|
|
|
196
179
|
def register_builtins!
|
|
197
180
|
cmd = CommandRegistry.instance
|
|
@@ -303,16 +286,20 @@ module RuVim
|
|
|
303
286
|
register_internal_unless(
|
|
304
287
|
cmd,
|
|
305
288
|
"stdin.stream_stop",
|
|
306
|
-
call: ->(ctx, **) {
|
|
307
|
-
|
|
289
|
+
call: ->(ctx, **) {
|
|
290
|
+
return if ctx.editor.stream_stop_or_cancel!
|
|
291
|
+
ctx.editor.invoke_app_action(:normal_ctrl_c)
|
|
292
|
+
},
|
|
293
|
+
desc: "Stop stream (or cancel pending state)"
|
|
308
294
|
)
|
|
309
295
|
|
|
310
|
-
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :
|
|
296
|
+
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :any, bang: true)
|
|
311
297
|
register_ex_unless(ex, "q", call: :app_quit, aliases: %w[quit], desc: "Quit", nargs: 0, bang: true)
|
|
312
298
|
register_ex_unless(ex, "qa", call: :app_quit_all, aliases: %w[qall], desc: "Quit all", nargs: 0, bang: true)
|
|
313
299
|
register_ex_unless(ex, "wq", call: :file_write_quit, desc: "Write and quit", nargs: :maybe_one, bang: true)
|
|
314
300
|
register_ex_unless(ex, "wqa", call: :file_write_quit_all, aliases: %w[wqall xa xall], desc: "Write all and quit", nargs: 0, bang: true)
|
|
315
301
|
register_ex_unless(ex, "e", call: :file_edit, aliases: %w[edit], desc: "Edit file / reload", nargs: :maybe_one, bang: true)
|
|
302
|
+
register_ex_unless(ex, "r", call: :ex_read, aliases: %w[read], desc: "Read file or command output into buffer", nargs: :any)
|
|
316
303
|
register_ex_unless(ex, "help", call: :ex_help, desc: "Show help / topics", nargs: :any)
|
|
317
304
|
register_ex_unless(ex, "command", call: :ex_define_command, desc: "Define user command", nargs: :any, bang: true)
|
|
318
305
|
register_ex_unless(ex, "ruby", call: :ex_ruby, aliases: %w[rb], desc: "Evaluate Ruby", nargs: :any, bang: false)
|
|
@@ -352,10 +339,30 @@ module RuVim
|
|
|
352
339
|
register_ex_unless(ex, "d", call: :ex_delete_lines, aliases: %w[delete], desc: "Delete lines", nargs: :any)
|
|
353
340
|
register_ex_unless(ex, "y", call: :ex_yank_lines, aliases: %w[yank], desc: "Yank lines", nargs: :any)
|
|
354
341
|
register_ex_unless(ex, "rich", call: :ex_rich, desc: "Open/close Rich View", nargs: :maybe_one)
|
|
342
|
+
register_ex_unless(ex, "follow", call: ->(ctx, **) { ctx.editor.invoke_app_action(:follow_toggle) }, desc: "Toggle file follow mode", nargs: 0)
|
|
343
|
+
register_ex_unless(ex, "nohlsearch", call: ->(ctx, **) { ctx.editor.suppress_hlsearch! }, aliases: %w[noh nohl nohlsearc nohlsear nohlsea nohlse nohls], desc: "Temporarily clear search highlight", nargs: 0)
|
|
344
|
+
register_ex_unless(ex, "filter", call: :ex_filter, desc: "Filter lines matching search pattern", nargs: :any)
|
|
345
|
+
register_internal_unless(cmd, "search.filter", call: :search_filter, desc: "Filter lines matching search pattern")
|
|
355
346
|
register_internal_unless(cmd, "rich.toggle", call: :rich_toggle, desc: "Toggle Rich View")
|
|
347
|
+
register_internal_unless(cmd, "rich.close_buffer", call: :rich_view_close_buffer, desc: "Close rich view buffer")
|
|
356
348
|
register_internal_unless(cmd, "quickfix.next", call: :ex_cnext, desc: "Next quickfix item")
|
|
357
349
|
register_internal_unless(cmd, "quickfix.prev", call: :ex_cprev, desc: "Prev quickfix item")
|
|
358
350
|
register_internal_unless(cmd, "quickfix.open", call: :ex_copen, desc: "Open quickfix list")
|
|
351
|
+
|
|
352
|
+
register_internal_unless(cmd, "git.blame", call: :git_blame, desc: "Open git blame buffer")
|
|
353
|
+
register_internal_unless(cmd, "git.blame.prev", call: :git_blame_prev, desc: "Blame at parent commit")
|
|
354
|
+
register_internal_unless(cmd, "git.blame.back", call: :git_blame_back, desc: "Restore previous blame")
|
|
355
|
+
register_internal_unless(cmd, "git.blame.commit", call: :git_blame_commit, desc: "Show commit details")
|
|
356
|
+
register_internal_unless(cmd, "git.command_mode", call: :enter_git_command_mode, desc: "Enter Git command-line mode")
|
|
357
|
+
register_internal_unless(cmd, "git.close_buffer", call: :git_close_buffer, desc: "Close git buffer")
|
|
358
|
+
register_internal_unless(cmd, "git.status.open_file", call: :git_status_open_file, desc: "Open file from git status")
|
|
359
|
+
register_internal_unless(cmd, "git.diff.open_file", call: :git_diff_open_file, desc: "Open file from git diff")
|
|
360
|
+
register_internal_unless(cmd, "git.grep.open_file", call: :git_grep_open_file, desc: "Open file from git grep")
|
|
361
|
+
register_internal_unless(cmd, "git.branch.checkout", call: :git_branch_checkout, desc: "Checkout branch under cursor")
|
|
362
|
+
register_internal_unless(cmd, "git.commit.execute", call: :git_commit_execute, desc: "Execute git commit")
|
|
363
|
+
register_ex_unless(ex, "run", call: :ex_run, desc: "Run command and show output in buffer", nargs: :any, raw_args: true)
|
|
364
|
+
register_ex_unless(ex, "git", call: :ex_git, desc: "Git subcommand dispatcher", nargs: :any)
|
|
365
|
+
register_ex_unless(ex, "gh", call: :ex_gh, desc: "GitHub subcommand dispatcher", nargs: :any)
|
|
359
366
|
end
|
|
360
367
|
|
|
361
368
|
def bind_default_keys!
|
|
@@ -448,6 +455,8 @@ module RuVim
|
|
|
448
455
|
@keymaps.bind(:normal, "g#", "search.word_backward_partial")
|
|
449
456
|
@keymaps.bind(:normal, "gf", "file.goto_under_cursor")
|
|
450
457
|
@keymaps.bind(:normal, "gr", "rich.toggle")
|
|
458
|
+
@keymaps.bind(:normal, "g/", "search.filter")
|
|
459
|
+
@keymaps.bind(:normal, ["<C-g>"], "git.command_mode")
|
|
451
460
|
@keymaps.bind(:normal, "Q", "quickfix.open")
|
|
452
461
|
@keymaps.bind(:normal, ["]", "q"], "quickfix.next")
|
|
453
462
|
@keymaps.bind(:normal, ["[", "q"], "quickfix.prev")
|
|
@@ -456,2488 +465,200 @@ module RuVim
|
|
|
456
465
|
@keymaps.bind(:normal, "\e", "ui.clear_message")
|
|
457
466
|
end
|
|
458
467
|
|
|
459
|
-
def
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if key == :ctrl_z
|
|
465
|
-
suspend_to_shell
|
|
466
|
-
track_mode_transition(mode_before)
|
|
467
|
-
return
|
|
468
|
-
end
|
|
469
|
-
if key == :ctrl_c && @editor.mode != :normal
|
|
470
|
-
handle_ctrl_c
|
|
471
|
-
track_mode_transition(mode_before)
|
|
472
|
-
record_macro_key_if_needed(key)
|
|
473
|
-
return
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
case @editor.mode
|
|
477
|
-
when :hit_enter
|
|
478
|
-
handle_hit_enter_key(key)
|
|
479
|
-
when :insert
|
|
480
|
-
handle_insert_key(key)
|
|
481
|
-
when :command_line
|
|
482
|
-
handle_command_line_key(key)
|
|
483
|
-
when :visual_char, :visual_line, :visual_block
|
|
484
|
-
handle_visual_key(key)
|
|
485
|
-
when :rich
|
|
486
|
-
handle_rich_key(key)
|
|
487
|
-
else
|
|
488
|
-
handle_normal_key(key)
|
|
489
|
-
end
|
|
490
|
-
track_mode_transition(mode_before)
|
|
491
|
-
load_current_ftplugin!
|
|
492
|
-
record_macro_key_if_needed(key)
|
|
493
|
-
rescue RuVim::CommandError => e
|
|
494
|
-
@editor.echo_error(e.message)
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
def clear_stale_message_before_key(key)
|
|
498
|
-
return if @editor.message.to_s.empty?
|
|
499
|
-
return if @editor.command_line_active?
|
|
500
|
-
return if @editor.hit_enter_active?
|
|
501
|
-
|
|
502
|
-
# Keep the error visible while the user is still dismissing/cancelling;
|
|
503
|
-
# otherwise, the next operation replaces the command-line area naturally.
|
|
504
|
-
return if key == :ctrl_c
|
|
505
|
-
|
|
506
|
-
@editor.clear_message
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
def handle_editor_app_action(name, **kwargs)
|
|
510
|
-
if @editor.rich_mode?
|
|
511
|
-
case name.to_sym
|
|
512
|
-
when :normal_operator_start
|
|
513
|
-
op = (kwargs[:name] || kwargs["name"]).to_sym
|
|
514
|
-
return if op == :delete || op == :change
|
|
515
|
-
when :normal_replace_pending_start, :normal_change_repeat
|
|
516
|
-
return
|
|
517
|
-
end
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
case name.to_sym
|
|
521
|
-
when :normal_register_pending_start
|
|
522
|
-
start_register_pending
|
|
523
|
-
when :normal_operator_start
|
|
524
|
-
start_operator_pending((kwargs[:name] || kwargs["name"]).to_sym)
|
|
525
|
-
when :normal_replace_pending_start
|
|
526
|
-
start_replace_pending
|
|
527
|
-
when :normal_find_pending_start
|
|
528
|
-
start_find_pending((kwargs[:token] || kwargs["token"]).to_s)
|
|
529
|
-
when :normal_find_repeat
|
|
530
|
-
repeat_last_find(reverse: !!(kwargs[:reverse] || kwargs["reverse"]))
|
|
531
|
-
when :normal_change_repeat
|
|
532
|
-
repeat_last_change
|
|
533
|
-
when :normal_macro_record_toggle
|
|
534
|
-
toggle_macro_recording_or_start_pending
|
|
535
|
-
when :normal_macro_play_pending_start
|
|
536
|
-
start_macro_play_pending
|
|
537
|
-
when :normal_mark_pending_start
|
|
538
|
-
start_mark_pending
|
|
539
|
-
when :normal_jump_pending_start
|
|
540
|
-
start_jump_pending(
|
|
541
|
-
linewise: !!(kwargs[:linewise] || kwargs["linewise"]),
|
|
542
|
-
repeat_token: (kwargs[:repeat_token] || kwargs["repeat_token"]).to_s
|
|
543
|
-
)
|
|
544
|
-
else
|
|
545
|
-
raise RuVim::CommandError, "Unknown app action: #{name}"
|
|
546
|
-
end
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
def handle_normal_key(key)
|
|
550
|
-
case
|
|
551
|
-
when handle_normal_key_pre_dispatch(key)
|
|
552
|
-
when (token = normalize_key_token(key)).nil?
|
|
553
|
-
when handle_normal_pending_state(token)
|
|
554
|
-
when handle_normal_direct_token(token)
|
|
555
|
-
else
|
|
556
|
-
@pending_keys ||= []
|
|
557
|
-
@pending_keys << token
|
|
558
|
-
resolve_normal_key_sequence
|
|
559
|
-
end
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
def handle_normal_key_pre_dispatch(key)
|
|
563
|
-
case
|
|
564
|
-
when key == :enter && handle_list_window_enter
|
|
565
|
-
when digit_key?(key) && count_digit_allowed?(key)
|
|
566
|
-
@editor.pending_count = (@editor.pending_count.to_s + key).to_i
|
|
567
|
-
@editor.echo(@editor.pending_count.to_s)
|
|
568
|
-
@pending_keys = []
|
|
569
|
-
else
|
|
570
|
-
return false
|
|
571
|
-
end
|
|
572
|
-
true
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
def handle_normal_pending_state(token)
|
|
576
|
-
case
|
|
577
|
-
when @pending_keys && !@pending_keys.empty?
|
|
578
|
-
@pending_keys << token
|
|
579
|
-
resolve_normal_key_sequence
|
|
580
|
-
when @operator_pending
|
|
581
|
-
handle_operator_pending_key(token)
|
|
582
|
-
when @register_pending
|
|
583
|
-
finish_register_pending(token)
|
|
584
|
-
when @mark_pending
|
|
585
|
-
finish_mark_pending(token)
|
|
586
|
-
when @jump_pending
|
|
587
|
-
finish_jump_pending(token)
|
|
588
|
-
when @macro_record_pending
|
|
589
|
-
finish_macro_record_pending(token)
|
|
590
|
-
when @macro_play_pending
|
|
591
|
-
finish_macro_play_pending(token)
|
|
592
|
-
when @replace_pending
|
|
593
|
-
handle_replace_pending_key(token)
|
|
594
|
-
when @find_pending
|
|
595
|
-
finish_find_pending(token)
|
|
596
|
-
else
|
|
597
|
-
return false
|
|
598
|
-
end
|
|
599
|
-
true
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
def handle_normal_direct_token(token)
|
|
603
|
-
false
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
def resolve_normal_key_sequence
|
|
607
|
-
match = @keymaps.resolve_with_context(:normal, @pending_keys, editor: @editor)
|
|
608
|
-
case match.status
|
|
609
|
-
when :pending, :ambiguous
|
|
610
|
-
if match.status == :ambiguous && match.invocation
|
|
611
|
-
inv = dup_invocation(match.invocation)
|
|
612
|
-
inv.count = @editor.pending_count
|
|
613
|
-
@pending_ambiguous_invocation = inv
|
|
614
|
-
else
|
|
615
|
-
@pending_ambiguous_invocation = nil
|
|
616
|
-
end
|
|
617
|
-
arm_pending_key_timeout
|
|
618
|
-
return
|
|
619
|
-
when :match
|
|
620
|
-
clear_pending_key_timeout
|
|
621
|
-
matched_keys = @pending_keys.dup
|
|
622
|
-
repeat_count = @editor.pending_count
|
|
623
|
-
@pending_keys = []
|
|
624
|
-
invocation = dup_invocation(match.invocation)
|
|
625
|
-
invocation.count = repeat_count
|
|
626
|
-
if @editor.rich_mode? && rich_mode_block_command?(invocation.id)
|
|
627
|
-
@editor.pending_count = nil
|
|
628
|
-
@pending_keys = []
|
|
629
|
-
return
|
|
630
|
-
end
|
|
631
|
-
@dispatcher.dispatch(@editor, invocation)
|
|
632
|
-
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
633
|
-
else
|
|
634
|
-
clear_pending_key_timeout
|
|
635
|
-
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
468
|
+
def install_signal_handlers
|
|
469
|
+
Signal.trap("WINCH") do
|
|
470
|
+
@screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
|
|
471
|
+
@needs_redraw = true
|
|
472
|
+
notify_signal_wakeup
|
|
636
473
|
end
|
|
637
|
-
|
|
638
|
-
|
|
474
|
+
rescue ArgumentError
|
|
475
|
+
nil
|
|
639
476
|
end
|
|
640
477
|
|
|
641
|
-
def
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
@editor.echo("")
|
|
649
|
-
when :backspace
|
|
650
|
-
clear_insert_completion
|
|
651
|
-
return unless insert_backspace_allowed?
|
|
652
|
-
insert_backspace_in_insert_mode
|
|
653
|
-
when :ctrl_n
|
|
654
|
-
insert_complete(+1)
|
|
655
|
-
when :ctrl_p
|
|
656
|
-
insert_complete(-1)
|
|
657
|
-
when :ctrl_i
|
|
658
|
-
clear_insert_completion
|
|
659
|
-
insert_tab_in_insert_mode
|
|
660
|
-
when :enter
|
|
661
|
-
clear_insert_completion
|
|
662
|
-
y, x = @editor.current_buffer.insert_newline(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
|
|
663
|
-
x = apply_insert_autoindent(y, x, previous_row: y - 1)
|
|
664
|
-
@editor.current_window.cursor_y = y
|
|
665
|
-
@editor.current_window.cursor_x = x
|
|
666
|
-
when :left
|
|
667
|
-
clear_insert_completion
|
|
668
|
-
dispatch_insert_cursor_motion("cursor.left")
|
|
669
|
-
when :right
|
|
670
|
-
clear_insert_completion
|
|
671
|
-
dispatch_insert_cursor_motion("cursor.right")
|
|
672
|
-
when :up
|
|
673
|
-
clear_insert_completion
|
|
674
|
-
@editor.current_window.move_up(@editor.current_buffer, 1)
|
|
675
|
-
when :down
|
|
676
|
-
clear_insert_completion
|
|
677
|
-
@editor.current_window.move_down(@editor.current_buffer, 1)
|
|
678
|
-
when :pageup, :pagedown
|
|
679
|
-
clear_insert_completion
|
|
680
|
-
invoke_page_key(key)
|
|
681
|
-
else
|
|
682
|
-
return unless key.is_a?(String)
|
|
683
|
-
|
|
684
|
-
clear_insert_completion
|
|
685
|
-
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
686
|
-
@editor.current_window.cursor_x += 1
|
|
687
|
-
maybe_showmatch_after_insert(key)
|
|
688
|
-
maybe_dedent_after_insert(key)
|
|
689
|
-
end
|
|
478
|
+
def init_config_loader!
|
|
479
|
+
@config_loader = ConfigLoader.new(
|
|
480
|
+
command_registry: CommandRegistry.instance,
|
|
481
|
+
ex_registry: ExCommandRegistry.instance,
|
|
482
|
+
keymaps: @keymaps,
|
|
483
|
+
command_host: GlobalCommands.instance
|
|
484
|
+
)
|
|
690
485
|
end
|
|
691
486
|
|
|
692
|
-
def
|
|
693
|
-
if
|
|
694
|
-
|
|
695
|
-
return
|
|
696
|
-
end
|
|
697
|
-
|
|
698
|
-
if paging_key?(key)
|
|
699
|
-
invoke_page_key(key)
|
|
700
|
-
return
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
token = normalize_key_token(key)
|
|
704
|
-
return if token.nil?
|
|
487
|
+
def load_user_config!
|
|
488
|
+
return if @clean_mode || @restricted_mode
|
|
489
|
+
return if @startup.skip_user_config
|
|
705
490
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
@register_pending = false
|
|
709
|
-
@visual_pending = nil
|
|
710
|
-
@editor.enter_normal_mode
|
|
711
|
-
when "v"
|
|
712
|
-
if @editor.mode == :visual_char
|
|
713
|
-
@editor.enter_normal_mode
|
|
714
|
-
else
|
|
715
|
-
@editor.enter_visual(:visual_char)
|
|
716
|
-
end
|
|
717
|
-
when "V"
|
|
718
|
-
if @editor.mode == :visual_line
|
|
719
|
-
@editor.enter_normal_mode
|
|
720
|
-
else
|
|
721
|
-
@editor.enter_visual(:visual_line)
|
|
722
|
-
end
|
|
723
|
-
when "<C-v>"
|
|
724
|
-
if @editor.mode == :visual_block
|
|
725
|
-
@editor.enter_normal_mode
|
|
726
|
-
else
|
|
727
|
-
@editor.enter_visual(:visual_block)
|
|
728
|
-
end
|
|
729
|
-
when "y"
|
|
730
|
-
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_yank"))
|
|
731
|
-
when "d"
|
|
732
|
-
@visual_pending = nil
|
|
733
|
-
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
|
|
734
|
-
when "="
|
|
735
|
-
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_indent"))
|
|
736
|
-
when "\""
|
|
737
|
-
start_register_pending
|
|
738
|
-
when "i", "a"
|
|
739
|
-
@visual_pending = token
|
|
491
|
+
if @startup.config_path
|
|
492
|
+
@config_loader.load_file(@startup.config_path)
|
|
740
493
|
else
|
|
741
|
-
|
|
742
|
-
finish_register_pending(token)
|
|
743
|
-
return
|
|
744
|
-
end
|
|
745
|
-
if @visual_pending
|
|
746
|
-
if @editor.mode == :visual_block
|
|
747
|
-
@visual_pending = nil
|
|
748
|
-
@editor.echo_error("text object in Visual block not supported yet")
|
|
749
|
-
return
|
|
750
|
-
end
|
|
751
|
-
motion = "#{@visual_pending}#{token}"
|
|
752
|
-
@visual_pending = nil
|
|
753
|
-
inv = CommandInvocation.new(id: "buffer.visual_select_text_object", kwargs: { motion: motion })
|
|
754
|
-
@dispatcher.dispatch(@editor, inv)
|
|
755
|
-
else
|
|
756
|
-
handle_visual_motion_token(token)
|
|
757
|
-
end
|
|
494
|
+
@config_loader.load_default!
|
|
758
495
|
end
|
|
759
|
-
|
|
760
|
-
@
|
|
496
|
+
rescue StandardError => e
|
|
497
|
+
@editor.echo_error("config error: #{e.message}")
|
|
761
498
|
end
|
|
762
499
|
|
|
763
|
-
def
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
"j" => "cursor.down",
|
|
767
|
-
"k" => "cursor.up",
|
|
768
|
-
"l" => "cursor.right",
|
|
769
|
-
"0" => "cursor.line_start",
|
|
770
|
-
"$" => "cursor.line_end",
|
|
771
|
-
"^" => "cursor.first_nonblank",
|
|
772
|
-
"w" => "cursor.word_forward",
|
|
773
|
-
"b" => "cursor.word_backward",
|
|
774
|
-
"e" => "cursor.word_end",
|
|
775
|
-
"G" => "cursor.buffer_end"
|
|
776
|
-
}[token]
|
|
777
|
-
|
|
778
|
-
if token == "g"
|
|
779
|
-
@pending_keys ||= []
|
|
780
|
-
@pending_keys << token
|
|
781
|
-
arm_pending_key_timeout
|
|
782
|
-
return
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
if @pending_keys == ["g"] && token == "g"
|
|
786
|
-
id = "cursor.buffer_start"
|
|
787
|
-
end
|
|
788
|
-
|
|
789
|
-
if id
|
|
790
|
-
clear_pending_key_timeout
|
|
791
|
-
count = @editor.pending_count
|
|
792
|
-
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
793
|
-
else
|
|
794
|
-
clear_pending_key_timeout
|
|
795
|
-
@editor.echo_error("Unknown visual key: #{token}")
|
|
796
|
-
end
|
|
797
|
-
ensure
|
|
798
|
-
@pending_keys = [] unless token == "g"
|
|
799
|
-
end
|
|
500
|
+
def load_current_ftplugin!
|
|
501
|
+
return if @clean_mode || @restricted_mode
|
|
502
|
+
return unless @config_loader
|
|
800
503
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
when :escape
|
|
805
|
-
clear_command_line_completion
|
|
806
|
-
cancel_incsearch_preview_if_any
|
|
807
|
-
@editor.cancel_command_line
|
|
808
|
-
when :enter
|
|
809
|
-
clear_command_line_completion
|
|
810
|
-
line = cmd.text.dup
|
|
811
|
-
push_command_line_history(cmd.prefix, line)
|
|
812
|
-
handle_command_line_submit(cmd.prefix, line)
|
|
813
|
-
when :backspace
|
|
814
|
-
clear_command_line_completion
|
|
815
|
-
if cmd.text.empty? && cmd.cursor.zero?
|
|
816
|
-
cancel_incsearch_preview_if_any
|
|
817
|
-
@editor.cancel_command_line
|
|
818
|
-
return
|
|
819
|
-
end
|
|
820
|
-
cmd.backspace
|
|
821
|
-
when :up
|
|
822
|
-
clear_command_line_completion
|
|
823
|
-
command_line_history_move(-1)
|
|
824
|
-
when :down
|
|
825
|
-
clear_command_line_completion
|
|
826
|
-
command_line_history_move(1)
|
|
827
|
-
when :left
|
|
828
|
-
clear_command_line_completion
|
|
829
|
-
cmd.move_left
|
|
830
|
-
when :right
|
|
831
|
-
clear_command_line_completion
|
|
832
|
-
cmd.move_right
|
|
833
|
-
else
|
|
834
|
-
if key == :ctrl_i
|
|
835
|
-
command_line_complete
|
|
836
|
-
elsif key.is_a?(String)
|
|
837
|
-
clear_command_line_completion
|
|
838
|
-
@cmdline_history_index = nil
|
|
839
|
-
cmd.insert(key)
|
|
840
|
-
end
|
|
841
|
-
end
|
|
842
|
-
update_incsearch_preview_if_needed
|
|
504
|
+
@config_loader.load_ftplugin!(@editor, @editor.current_buffer)
|
|
505
|
+
rescue StandardError => e
|
|
506
|
+
@editor.echo_error("ftplugin error: #{e.message}")
|
|
843
507
|
end
|
|
844
508
|
|
|
845
|
-
def
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
source_window_id = source_window_id.to_i if source_window_id
|
|
857
|
-
source_window_id = nil unless source_window_id && @editor.windows.key?(source_window_id)
|
|
858
|
-
|
|
859
|
-
item =
|
|
860
|
-
if buffer.kind == :quickfix
|
|
861
|
-
@editor.select_quickfix(item_index)
|
|
862
|
-
else
|
|
863
|
-
owner_window_id = source_window_id || @editor.current_window_id
|
|
864
|
-
@editor.select_location_list(item_index, window_id: owner_window_id)
|
|
865
|
-
end
|
|
866
|
-
|
|
867
|
-
unless item
|
|
868
|
-
@editor.echo_error("#{buffer.kind == :quickfix ? 'quickfix' : 'location list'} item not found")
|
|
869
|
-
return true
|
|
870
|
-
end
|
|
871
|
-
|
|
872
|
-
if source_window_id
|
|
873
|
-
@editor.current_window_id = source_window_id
|
|
509
|
+
def run_startup_action!(action, log_prefix: "startup")
|
|
510
|
+
case action[:type]
|
|
511
|
+
when :ex
|
|
512
|
+
verbose_log(2, "#{log_prefix} ex: #{action[:value]}")
|
|
513
|
+
@dispatcher.dispatch_ex(@editor, action[:value].to_s)
|
|
514
|
+
when :line
|
|
515
|
+
verbose_log(2, "#{log_prefix} line: #{action[:value]}")
|
|
516
|
+
move_cursor_to_line(action[:value].to_i)
|
|
517
|
+
when :line_end
|
|
518
|
+
verbose_log(2, "#{log_prefix} line_end")
|
|
519
|
+
move_cursor_to_line(@editor.current_buffer.line_count)
|
|
874
520
|
end
|
|
875
|
-
@editor.jump_to_location(item)
|
|
876
|
-
@editor.echo(
|
|
877
|
-
if buffer.kind == :quickfix
|
|
878
|
-
"qf #{@editor.quickfix_index.to_i + 1}/#{@editor.quickfix_items.length}"
|
|
879
|
-
else
|
|
880
|
-
owner_window_id = source_window_id || @editor.current_window_id
|
|
881
|
-
list = @editor.location_list(owner_window_id)
|
|
882
|
-
"ll #{list[:index].to_i + 1}/#{list[:items].length}"
|
|
883
|
-
end
|
|
884
|
-
)
|
|
885
|
-
true
|
|
886
521
|
end
|
|
887
522
|
|
|
888
|
-
def
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
def paging_key?(key)
|
|
893
|
-
%i[pageup pagedown].include?(key)
|
|
894
|
-
end
|
|
523
|
+
def verbose_log(level, message)
|
|
524
|
+
return if @verbose_level < level
|
|
525
|
+
return unless @verbose_io
|
|
895
526
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
up: "cursor.up",
|
|
901
|
-
down: "cursor.down"
|
|
902
|
-
}.fetch(key)
|
|
903
|
-
inv = CommandInvocation.new(id:, count: @editor.pending_count)
|
|
904
|
-
@dispatcher.dispatch(@editor, inv)
|
|
905
|
-
@editor.pending_count = nil
|
|
906
|
-
@pending_keys = []
|
|
527
|
+
@verbose_io.puts("[ruvim:v#{@verbose_level}] #{message}")
|
|
528
|
+
@verbose_io.flush if @verbose_io.respond_to?(:flush)
|
|
529
|
+
rescue StandardError
|
|
530
|
+
nil
|
|
907
531
|
end
|
|
908
532
|
|
|
909
|
-
def
|
|
910
|
-
|
|
911
|
-
inv = CommandInvocation.new(
|
|
912
|
-
id: id,
|
|
913
|
-
count: @editor.pending_count,
|
|
914
|
-
kwargs: { page_lines: current_page_step_lines }
|
|
915
|
-
)
|
|
916
|
-
@dispatcher.dispatch(@editor, inv)
|
|
917
|
-
@editor.pending_count = nil
|
|
918
|
-
@pending_keys = []
|
|
919
|
-
end
|
|
533
|
+
def startup_mark(label)
|
|
534
|
+
return unless @startup&.time_path
|
|
920
535
|
|
|
921
|
-
|
|
922
|
-
key.is_a?(String) && key.match?(/\A\d\z/)
|
|
536
|
+
@startup.timeline << [label.to_s, monotonic_now]
|
|
923
537
|
end
|
|
924
538
|
|
|
925
|
-
def
|
|
926
|
-
return
|
|
927
|
-
return true unless @editor.pending_count.nil?
|
|
928
|
-
|
|
929
|
-
key != "0"
|
|
930
|
-
end
|
|
539
|
+
def write_startuptime_log!
|
|
540
|
+
return unless @startup&.time_path
|
|
931
541
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
when :ctrl_u then "<C-u>"
|
|
939
|
-
when :ctrl_f then "<C-f>"
|
|
940
|
-
when :ctrl_b then "<C-b>"
|
|
941
|
-
when :ctrl_e then "<C-e>"
|
|
942
|
-
when :ctrl_y then "<C-y>"
|
|
943
|
-
when :ctrl_v then "<C-v>"
|
|
944
|
-
when :ctrl_i then "<C-i>"
|
|
945
|
-
when :ctrl_o then "<C-o>"
|
|
946
|
-
when :ctrl_w then "<C-w>"
|
|
947
|
-
when :ctrl_l then "<C-l>"
|
|
948
|
-
when :ctrl_c then "<C-c>"
|
|
949
|
-
when :left then "<Left>"
|
|
950
|
-
when :right then "<Right>"
|
|
951
|
-
when :up then "<Up>"
|
|
952
|
-
when :down then "<Down>"
|
|
953
|
-
when :home then "<Home>"
|
|
954
|
-
when :end then "<End>"
|
|
955
|
-
when :pageup then "<PageUp>"
|
|
956
|
-
when :pagedown then "<PageDown>"
|
|
957
|
-
when :shift_up then "<S-Up>"
|
|
958
|
-
when :shift_down then "<S-Down>"
|
|
959
|
-
when :shift_left then "<S-Left>"
|
|
960
|
-
when :shift_right then "<S-Right>"
|
|
961
|
-
else nil
|
|
542
|
+
prev = @startup.time_origin
|
|
543
|
+
lines = @startup.timeline.map do |label, t|
|
|
544
|
+
total_ms = ((t - @startup.time_origin) * 1000.0)
|
|
545
|
+
delta_ms = ((t - prev) * 1000.0)
|
|
546
|
+
prev = t
|
|
547
|
+
format("%9.3f %9.3f %s", total_ms, delta_ms, label)
|
|
962
548
|
end
|
|
549
|
+
File.write(@startup.time_path, lines.join("\n") + "\n")
|
|
550
|
+
rescue StandardError => e
|
|
551
|
+
verbose_log(1, "startuptime write error: #{e.message}")
|
|
963
552
|
end
|
|
964
553
|
|
|
965
|
-
def
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
kwargs: inv.kwargs.dup,
|
|
970
|
-
count: inv.count,
|
|
971
|
-
bang: inv.bang,
|
|
972
|
-
raw_keys: inv.raw_keys&.dup
|
|
973
|
-
)
|
|
554
|
+
def monotonic_now
|
|
555
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
556
|
+
rescue StandardError
|
|
557
|
+
Time.now.to_f
|
|
974
558
|
end
|
|
975
559
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
buffer.delete_char buffer.delete_line buffer.delete_motion
|
|
981
|
-
buffer.change_motion buffer.change_line
|
|
982
|
-
buffer.paste_after buffer.paste_before
|
|
983
|
-
buffer.replace_char
|
|
984
|
-
buffer.visual_delete
|
|
985
|
-
].freeze
|
|
986
|
-
|
|
987
|
-
def handle_hit_enter_key(key)
|
|
988
|
-
token = normalize_key_token(key)
|
|
989
|
-
case token
|
|
990
|
-
when ":"
|
|
991
|
-
@editor.exit_hit_enter_mode
|
|
992
|
-
@editor.enter_command_line_mode(":")
|
|
993
|
-
when "/", "?"
|
|
994
|
-
@editor.exit_hit_enter_mode
|
|
995
|
-
@editor.enter_command_line_mode(token)
|
|
996
|
-
else
|
|
997
|
-
@editor.exit_hit_enter_mode
|
|
998
|
-
end
|
|
560
|
+
def apply_startup_buffer_flags!
|
|
561
|
+
apply_startup_readonly! if @startup.readonly
|
|
562
|
+
apply_startup_nomodifiable! if @startup.nomodifiable
|
|
563
|
+
apply_startup_follow! if @startup.follow
|
|
999
564
|
end
|
|
1000
565
|
|
|
1001
|
-
def
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
RuVim::RichView.close!(@editor)
|
|
1005
|
-
return
|
|
1006
|
-
end
|
|
1007
|
-
|
|
1008
|
-
handle_normal_key(key)
|
|
1009
|
-
end
|
|
566
|
+
def apply_startup_readonly!
|
|
567
|
+
buf = @editor.current_buffer
|
|
568
|
+
return unless buf&.file_buffer?
|
|
1010
569
|
|
|
1011
|
-
|
|
1012
|
-
|
|
570
|
+
buf.readonly = true
|
|
571
|
+
@editor.echo("readonly: #{buf.display_name}")
|
|
1013
572
|
end
|
|
1014
573
|
|
|
1015
|
-
def
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
when :insert
|
|
1020
|
-
finish_insert_change_group
|
|
1021
|
-
finish_dot_change_capture
|
|
1022
|
-
clear_insert_completion
|
|
1023
|
-
clear_pending_key_timeout
|
|
1024
|
-
@editor.enter_normal_mode
|
|
1025
|
-
@editor.echo("")
|
|
1026
|
-
when :command_line
|
|
1027
|
-
clear_pending_key_timeout
|
|
1028
|
-
cancel_incsearch_preview_if_any
|
|
1029
|
-
@editor.cancel_command_line
|
|
1030
|
-
when :visual_char, :visual_line, :visual_block
|
|
1031
|
-
@visual_pending = nil
|
|
1032
|
-
@register_pending = false
|
|
1033
|
-
@mark_pending = false
|
|
1034
|
-
@jump_pending = nil
|
|
1035
|
-
clear_pending_key_timeout
|
|
1036
|
-
@editor.enter_normal_mode
|
|
1037
|
-
when :rich
|
|
1038
|
-
clear_pending_key_timeout
|
|
1039
|
-
@editor.pending_count = nil
|
|
1040
|
-
@pending_keys = []
|
|
1041
|
-
@operator_pending = nil
|
|
1042
|
-
@replace_pending = nil
|
|
1043
|
-
@register_pending = false
|
|
1044
|
-
@mark_pending = false
|
|
1045
|
-
@jump_pending = nil
|
|
1046
|
-
@macro_record_pending = false
|
|
1047
|
-
@macro_play_pending = false
|
|
1048
|
-
RuVim::RichView.close!(@editor)
|
|
1049
|
-
else
|
|
1050
|
-
clear_pending_key_timeout
|
|
1051
|
-
@editor.pending_count = nil
|
|
1052
|
-
@pending_keys = []
|
|
1053
|
-
@operator_pending = nil
|
|
1054
|
-
@replace_pending = nil
|
|
1055
|
-
@register_pending = false
|
|
1056
|
-
@mark_pending = false
|
|
1057
|
-
@jump_pending = nil
|
|
1058
|
-
@macro_record_pending = false
|
|
1059
|
-
@macro_play_pending = false
|
|
1060
|
-
@editor.clear_message
|
|
1061
|
-
end
|
|
1062
|
-
end
|
|
574
|
+
def apply_startup_follow!
|
|
575
|
+
buf = @editor.current_buffer
|
|
576
|
+
return unless buf&.file_buffer?
|
|
577
|
+
return if @stream_mixer.follow_active?(buf)
|
|
1063
578
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
@
|
|
1068
|
-
@operator_pending = nil
|
|
1069
|
-
@replace_pending = nil
|
|
1070
|
-
@register_pending = false
|
|
1071
|
-
@mark_pending = false
|
|
1072
|
-
@jump_pending = nil
|
|
1073
|
-
@macro_record_pending = false
|
|
1074
|
-
@macro_play_pending = false
|
|
1075
|
-
@editor.clear_message
|
|
579
|
+
win = @editor.current_window
|
|
580
|
+
win.cursor_y = buf.line_count - 1
|
|
581
|
+
win.clamp_to_buffer(buf)
|
|
582
|
+
@stream_mixer.start_follow!(buf)
|
|
1076
583
|
end
|
|
1077
584
|
|
|
1078
|
-
def
|
|
1079
|
-
@
|
|
1080
|
-
|
|
1081
|
-
@needs_redraw = true
|
|
1082
|
-
rescue StandardError => e
|
|
1083
|
-
@editor.echo_error("suspend failed: #{e.message}")
|
|
1084
|
-
end
|
|
585
|
+
def apply_startup_nomodifiable!
|
|
586
|
+
buf = @editor.current_buffer
|
|
587
|
+
return unless buf&.file_buffer?
|
|
1085
588
|
|
|
1086
|
-
|
|
1087
|
-
|
|
589
|
+
buf.modifiable = false
|
|
590
|
+
buf.readonly = true
|
|
591
|
+
@editor.echo("nomodifiable: #{buf.display_name}")
|
|
1088
592
|
end
|
|
1089
593
|
|
|
1090
|
-
def
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
verbose_log(2, "ex: #{line}")
|
|
1095
|
-
@dispatcher.dispatch_ex(@editor, line)
|
|
1096
|
-
when "/"
|
|
1097
|
-
verbose_log(2, "search(/): #{line}")
|
|
1098
|
-
submit_search(line, direction: :forward)
|
|
1099
|
-
when "?"
|
|
1100
|
-
verbose_log(2, "search(?): #{line}")
|
|
1101
|
-
submit_search(line, direction: :backward)
|
|
1102
|
-
else
|
|
1103
|
-
@editor.echo_error("Unknown command-line prefix: #{prefix}")
|
|
1104
|
-
@editor.enter_normal_mode
|
|
594
|
+
def apply_startup_compat_mode_messages!
|
|
595
|
+
if @startup.diff_mode
|
|
596
|
+
verbose_log(1, "startup: -d requested (diff mode placeholder)")
|
|
597
|
+
@editor.echo("diff mode (-d) is not implemented yet")
|
|
1105
598
|
end
|
|
1106
|
-
@cmdline_history_index = nil
|
|
1107
|
-
end
|
|
1108
|
-
|
|
1109
|
-
def start_operator_pending(name)
|
|
1110
|
-
@operator_pending = { name:, count: @editor.pending_count }
|
|
1111
|
-
@editor.pending_count = nil
|
|
1112
|
-
@pending_keys = []
|
|
1113
|
-
@editor.echo(name == :delete ? "d" : name.to_s)
|
|
1114
|
-
end
|
|
1115
|
-
|
|
1116
|
-
def start_register_pending
|
|
1117
|
-
@register_pending = true
|
|
1118
|
-
@editor.echo('"')
|
|
1119
|
-
end
|
|
1120
599
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
@editor.set_active_register(token)
|
|
1125
|
-
@editor.echo(%("#{token}))
|
|
1126
|
-
else
|
|
1127
|
-
@editor.echo_error("Invalid register")
|
|
600
|
+
if @startup.quickfix_errorfile
|
|
601
|
+
verbose_log(1, "startup: -q #{@startup.quickfix_errorfile} requested (quickfix placeholder)")
|
|
602
|
+
@editor.echo("quickfix startup (-q #{@startup.quickfix_errorfile}) is not implemented yet")
|
|
1128
603
|
end
|
|
1129
|
-
end
|
|
1130
|
-
|
|
1131
|
-
def start_mark_pending
|
|
1132
|
-
@mark_pending = true
|
|
1133
|
-
@editor.echo("m")
|
|
1134
|
-
end
|
|
1135
604
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
@editor.clear_message
|
|
1140
|
-
return
|
|
1141
|
-
end
|
|
1142
|
-
unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
|
|
1143
|
-
@editor.echo_error("Invalid mark")
|
|
1144
|
-
return
|
|
605
|
+
if @startup.session_file
|
|
606
|
+
verbose_log(1, "startup: -S #{@startup.session_file} requested (session placeholder)")
|
|
607
|
+
@editor.echo("session startup (-S #{@startup.session_file}) is not implemented yet")
|
|
1145
608
|
end
|
|
1146
|
-
|
|
1147
|
-
inv = CommandInvocation.new(id: "mark.set", kwargs: { mark: token })
|
|
1148
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1149
|
-
end
|
|
1150
|
-
|
|
1151
|
-
def start_jump_pending(linewise:, repeat_token:)
|
|
1152
|
-
@jump_pending = { linewise: linewise, repeat_token: repeat_token }
|
|
1153
|
-
@editor.echo(repeat_token)
|
|
1154
609
|
end
|
|
1155
610
|
|
|
1156
|
-
def
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
return unless pending
|
|
1160
|
-
if token == "\e"
|
|
1161
|
-
@editor.clear_message
|
|
1162
|
-
return
|
|
1163
|
-
end
|
|
1164
|
-
|
|
1165
|
-
if token == pending[:repeat_token]
|
|
1166
|
-
inv = CommandInvocation.new(id: "jump.older", kwargs: { linewise: pending[:linewise] })
|
|
1167
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1168
|
-
return
|
|
1169
|
-
end
|
|
1170
|
-
|
|
1171
|
-
unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
|
|
1172
|
-
@editor.echo_error("Invalid mark")
|
|
1173
|
-
return
|
|
1174
|
-
end
|
|
611
|
+
def open_startup_paths!(paths)
|
|
612
|
+
list = Array(paths).compact
|
|
613
|
+
return if list.empty?
|
|
1175
614
|
|
|
1176
|
-
|
|
1177
|
-
@
|
|
1178
|
-
end
|
|
615
|
+
evict_bootstrap_buffer!
|
|
616
|
+
@editor.set_arglist(list)
|
|
1179
617
|
|
|
1180
|
-
|
|
1181
|
-
@
|
|
1182
|
-
|
|
1183
|
-
end
|
|
618
|
+
first, *rest = list
|
|
619
|
+
@editor.open_path(first)
|
|
620
|
+
apply_startup_buffer_flags!
|
|
1184
621
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
622
|
+
case @startup.open_layout
|
|
623
|
+
when :horizontal
|
|
624
|
+
first_win_id = @editor.current_window_id
|
|
625
|
+
rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
|
|
626
|
+
@editor.focus_window(first_win_id)
|
|
627
|
+
when :vertical
|
|
628
|
+
first_win_id = @editor.current_window_id
|
|
629
|
+
rest.each { |p| open_path_in_split!(p, layout: :vertical) }
|
|
630
|
+
@editor.focus_window(first_win_id)
|
|
631
|
+
when :tab
|
|
632
|
+
rest.each { |p| open_path_in_tab!(p) }
|
|
633
|
+
@editor.tabnext(-(@editor.tabpage_count - 1))
|
|
1188
634
|
else
|
|
1189
|
-
|
|
635
|
+
rest.each do |p|
|
|
636
|
+
buf = @editor.add_buffer_from_file(p)
|
|
637
|
+
@stream_mixer.start_follow!(buf) if @startup.follow
|
|
638
|
+
end
|
|
1190
639
|
end
|
|
1191
640
|
end
|
|
1192
641
|
|
|
1193
|
-
def
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
return
|
|
1198
|
-
end
|
|
1199
|
-
unless token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
|
|
1200
|
-
@editor.echo_error("Invalid macro register")
|
|
1201
|
-
return
|
|
1202
|
-
end
|
|
1203
|
-
|
|
1204
|
-
unless @editor.start_macro_recording(token)
|
|
1205
|
-
@editor.echo("Failed to start recording")
|
|
1206
|
-
return
|
|
642
|
+
def evict_bootstrap_buffer!
|
|
643
|
+
bid = @editor.buffer_ids.find do |id|
|
|
644
|
+
b = @editor.buffers[id]
|
|
645
|
+
b.path.nil? && !b.modified? && b.line_count <= 1 && b.kind == :file
|
|
1207
646
|
end
|
|
1208
|
-
|
|
1209
|
-
@editor.echo("recording @#{token}")
|
|
1210
|
-
end
|
|
1211
|
-
|
|
1212
|
-
def stop_macro_recording
|
|
1213
|
-
reg = @editor.macro_recording_name
|
|
1214
|
-
@editor.stop_macro_recording
|
|
1215
|
-
@editor.echo("recording @#{reg} stopped")
|
|
1216
|
-
end
|
|
647
|
+
return unless bid
|
|
1217
648
|
|
|
1218
|
-
|
|
1219
|
-
@
|
|
1220
|
-
@editor.echo("@")
|
|
649
|
+
@editor.buffers.delete(bid)
|
|
650
|
+
@editor.instance_variable_set(:@next_buffer_id, 1)
|
|
1221
651
|
end
|
|
1222
652
|
|
|
1223
|
-
def
|
|
1224
|
-
@
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
return
|
|
1228
|
-
end
|
|
1229
|
-
name =
|
|
1230
|
-
if token == "@"
|
|
1231
|
-
@last_macro_name
|
|
1232
|
-
elsif token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
|
|
1233
|
-
token
|
|
1234
|
-
end
|
|
1235
|
-
unless name
|
|
1236
|
-
@editor.echo_error("Invalid macro register")
|
|
1237
|
-
return
|
|
1238
|
-
end
|
|
1239
|
-
|
|
1240
|
-
count = @editor.pending_count
|
|
1241
|
-
@editor.pending_count = nil
|
|
1242
|
-
play_macro(name, count:)
|
|
1243
|
-
end
|
|
1244
|
-
|
|
1245
|
-
def play_macro(name, count:)
|
|
1246
|
-
reg = name.to_s.downcase
|
|
1247
|
-
keys = @editor.macro_keys(reg)
|
|
1248
|
-
if keys.nil? || keys.empty?
|
|
1249
|
-
@editor.echo("Macro empty: #{reg}")
|
|
1250
|
-
return
|
|
1251
|
-
end
|
|
1252
|
-
|
|
1253
|
-
@macro_play_stack ||= []
|
|
1254
|
-
if @macro_play_stack.include?(reg) || @macro_play_stack.length >= 20
|
|
1255
|
-
@editor.echo("Macro recursion blocked: #{reg}")
|
|
1256
|
-
return
|
|
1257
|
-
end
|
|
1258
|
-
|
|
1259
|
-
@last_macro_name = reg
|
|
1260
|
-
@macro_play_stack << reg
|
|
1261
|
-
@suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
|
|
1262
|
-
[count.to_i, 1].max.times do
|
|
1263
|
-
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
1264
|
-
end
|
|
1265
|
-
@editor.echo("@#{reg}")
|
|
1266
|
-
ensure
|
|
1267
|
-
@suspend_macro_recording_depth = [(@suspend_macro_recording_depth || 1) - 1, 0].max
|
|
1268
|
-
@macro_play_stack.pop if @macro_play_stack && !@macro_play_stack.empty?
|
|
1269
|
-
end
|
|
1270
|
-
|
|
1271
|
-
def record_macro_key_if_needed(key)
|
|
1272
|
-
return if @skip_record_for_current_key
|
|
1273
|
-
return unless @editor.macro_recording?
|
|
1274
|
-
return if (@suspend_macro_recording_depth || 0).positive?
|
|
1275
|
-
return if (@dot_replay_depth || 0).positive?
|
|
1276
|
-
|
|
1277
|
-
@editor.record_macro_key(key)
|
|
1278
|
-
end
|
|
1279
|
-
|
|
1280
|
-
def dup_macro_runtime_key(key)
|
|
1281
|
-
case key
|
|
1282
|
-
when String
|
|
1283
|
-
key.dup
|
|
1284
|
-
when Array
|
|
1285
|
-
key.map { |v| v.is_a?(String) ? v.dup : v }
|
|
1286
|
-
else
|
|
1287
|
-
key
|
|
1288
|
-
end
|
|
1289
|
-
end
|
|
1290
|
-
|
|
1291
|
-
def handle_operator_pending_key(token)
|
|
1292
|
-
op = @operator_pending
|
|
1293
|
-
if %w[i a].include?(token) && !op[:motion_prefix]
|
|
1294
|
-
@operator_pending[:motion_prefix] = token
|
|
1295
|
-
@editor.echo("#{op[:name].to_s[0]}#{token}")
|
|
1296
|
-
return
|
|
1297
|
-
end
|
|
1298
|
-
|
|
1299
|
-
motion = [op[:motion_prefix], token].compact.join
|
|
1300
|
-
@operator_pending = nil
|
|
1301
|
-
|
|
1302
|
-
if token == "\e"
|
|
1303
|
-
@editor.clear_message
|
|
1304
|
-
return
|
|
1305
|
-
end
|
|
1306
|
-
|
|
1307
|
-
if op[:name] == :delete && motion == "d"
|
|
1308
|
-
inv = CommandInvocation.new(id: "buffer.delete_line", count: op[:count])
|
|
1309
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1310
|
-
record_last_change_keys(count_prefixed_keys(op[:count], ["d", "d"]))
|
|
1311
|
-
return
|
|
1312
|
-
end
|
|
1313
|
-
|
|
1314
|
-
if op[:name] == :delete
|
|
1315
|
-
inv = CommandInvocation.new(id: "buffer.delete_motion", count: op[:count], kwargs: { motion: motion })
|
|
1316
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1317
|
-
record_last_change_keys(count_prefixed_keys(op[:count], ["d", *motion.each_char.to_a]))
|
|
1318
|
-
return
|
|
1319
|
-
end
|
|
1320
|
-
|
|
1321
|
-
if op[:name] == :yank && motion == "y"
|
|
1322
|
-
inv = CommandInvocation.new(id: "buffer.yank_line", count: op[:count])
|
|
1323
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1324
|
-
return
|
|
1325
|
-
end
|
|
1326
|
-
|
|
1327
|
-
if op[:name] == :yank
|
|
1328
|
-
inv = CommandInvocation.new(id: "buffer.yank_motion", count: op[:count], kwargs: { motion: motion })
|
|
1329
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1330
|
-
return
|
|
1331
|
-
end
|
|
1332
|
-
|
|
1333
|
-
if op[:name] == :indent && motion == "="
|
|
1334
|
-
inv = CommandInvocation.new(id: "buffer.indent_lines", count: op[:count])
|
|
1335
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1336
|
-
return
|
|
1337
|
-
end
|
|
1338
|
-
|
|
1339
|
-
if op[:name] == :indent
|
|
1340
|
-
inv = CommandInvocation.new(id: "buffer.indent_motion", count: op[:count], kwargs: { motion: motion })
|
|
1341
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1342
|
-
return
|
|
1343
|
-
end
|
|
1344
|
-
|
|
1345
|
-
if op[:name] == :change && motion == "c"
|
|
1346
|
-
inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
|
|
1347
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1348
|
-
begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", "c"])) if @editor.mode == :insert
|
|
1349
|
-
return
|
|
1350
|
-
end
|
|
1351
|
-
|
|
1352
|
-
if op[:name] == :change
|
|
1353
|
-
inv = CommandInvocation.new(id: "buffer.change_motion", count: op[:count], kwargs: { motion: motion })
|
|
1354
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1355
|
-
begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", *motion.each_char.to_a])) if @editor.mode == :insert
|
|
1356
|
-
return
|
|
1357
|
-
end
|
|
1358
|
-
|
|
1359
|
-
@editor.echo_error("Unknown operator")
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
|
-
def start_replace_pending
|
|
1363
|
-
@replace_pending = { count: @editor.pending_count }
|
|
1364
|
-
@editor.pending_count = nil
|
|
1365
|
-
@pending_keys = []
|
|
1366
|
-
@editor.echo("r")
|
|
1367
|
-
end
|
|
1368
|
-
|
|
1369
|
-
def handle_replace_pending_key(token)
|
|
1370
|
-
pending = @replace_pending
|
|
1371
|
-
@replace_pending = nil
|
|
1372
|
-
if token == "\e"
|
|
1373
|
-
@editor.clear_message
|
|
1374
|
-
return
|
|
1375
|
-
end
|
|
1376
|
-
|
|
1377
|
-
if token.is_a?(String) && !token.empty?
|
|
1378
|
-
inv = CommandInvocation.new(id: "buffer.replace_char", argv: [token], count: pending[:count])
|
|
1379
|
-
@dispatcher.dispatch(@editor, inv)
|
|
1380
|
-
record_last_change_keys(count_prefixed_keys(pending[:count], ["r", token]))
|
|
1381
|
-
else
|
|
1382
|
-
@editor.echo("r expects one character")
|
|
1383
|
-
end
|
|
1384
|
-
end
|
|
1385
|
-
|
|
1386
|
-
def repeat_last_change
|
|
1387
|
-
keys = @last_change_keys
|
|
1388
|
-
if keys.nil? || keys.empty?
|
|
1389
|
-
@editor.echo("No previous change")
|
|
1390
|
-
return
|
|
1391
|
-
end
|
|
1392
|
-
|
|
1393
|
-
@dot_replay_depth = (@dot_replay_depth || 0) + 1
|
|
1394
|
-
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
1395
|
-
@editor.echo(".")
|
|
1396
|
-
ensure
|
|
1397
|
-
@dot_replay_depth = [(@dot_replay_depth || 1) - 1, 0].max
|
|
1398
|
-
end
|
|
1399
|
-
|
|
1400
|
-
def maybe_record_simple_dot_change(invocation, matched_keys, count)
|
|
1401
|
-
return if (@dot_replay_depth || 0).positive?
|
|
1402
|
-
|
|
1403
|
-
case invocation.id
|
|
1404
|
-
when "buffer.delete_char", "buffer.delete_motion", "buffer.join_lines", "buffer.swapcase_char", "buffer.paste_after", "buffer.paste_before"
|
|
1405
|
-
record_last_change_keys(count_prefixed_keys(count, matched_keys))
|
|
1406
|
-
when "mode.insert", "mode.append", "mode.append_line_end", "mode.insert_nonblank", "mode.open_below", "mode.open_above", "buffer.substitute_char", "buffer.change_motion", "buffer.change_line"
|
|
1407
|
-
begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
|
|
1408
|
-
end
|
|
1409
|
-
end
|
|
1410
|
-
|
|
1411
|
-
def begin_dot_change_capture(prefix_keys)
|
|
1412
|
-
return if (@dot_replay_depth || 0).positive?
|
|
1413
|
-
|
|
1414
|
-
@dot_change_capture_keys = Array(prefix_keys).map { |k| dup_macro_runtime_key(k) }
|
|
1415
|
-
@dot_change_capture_active = true
|
|
1416
|
-
end
|
|
1417
|
-
|
|
1418
|
-
def append_dot_change_capture_key(key)
|
|
1419
|
-
return unless @dot_change_capture_active
|
|
1420
|
-
return if (@dot_replay_depth || 0).positive?
|
|
1421
|
-
|
|
1422
|
-
@dot_change_capture_keys ||= []
|
|
1423
|
-
@dot_change_capture_keys << dup_macro_runtime_key(key)
|
|
1424
|
-
end
|
|
1425
|
-
|
|
1426
|
-
def finish_dot_change_capture
|
|
1427
|
-
return unless @dot_change_capture_active
|
|
1428
|
-
|
|
1429
|
-
keys = Array(@dot_change_capture_keys)
|
|
1430
|
-
@dot_change_capture_active = false
|
|
1431
|
-
@dot_change_capture_keys = nil
|
|
1432
|
-
record_last_change_keys(keys)
|
|
1433
|
-
end
|
|
1434
|
-
|
|
1435
|
-
def record_last_change_keys(keys)
|
|
1436
|
-
return if (@dot_replay_depth || 0).positive?
|
|
1437
|
-
|
|
1438
|
-
@last_change_keys = Array(keys).map { |k| dup_macro_runtime_key(k) }
|
|
1439
|
-
end
|
|
1440
|
-
|
|
1441
|
-
def count_prefixed_keys(count, keys)
|
|
1442
|
-
c = count.to_i
|
|
1443
|
-
prefix = c > 1 ? c.to_s.each_char.to_a : []
|
|
1444
|
-
prefix + Array(keys)
|
|
1445
|
-
end
|
|
1446
|
-
|
|
1447
|
-
def start_find_pending(token)
|
|
1448
|
-
@find_pending = {
|
|
1449
|
-
direction: (token == "f" || token == "t") ? :forward : :backward,
|
|
1450
|
-
till: (token == "t" || token == "T"),
|
|
1451
|
-
count: @editor.pending_count
|
|
1452
|
-
}
|
|
1453
|
-
@editor.pending_count = nil
|
|
1454
|
-
@pending_keys = []
|
|
1455
|
-
@editor.echo(token)
|
|
1456
|
-
end
|
|
1457
|
-
|
|
1458
|
-
def finish_find_pending(token)
|
|
1459
|
-
pending = @find_pending
|
|
1460
|
-
@find_pending = nil
|
|
1461
|
-
if token == "\e"
|
|
1462
|
-
@editor.clear_message
|
|
1463
|
-
return
|
|
1464
|
-
end
|
|
1465
|
-
unless token.is_a?(String) && !token.empty?
|
|
1466
|
-
@editor.echo("find expects one character")
|
|
1467
|
-
return
|
|
1468
|
-
end
|
|
1469
|
-
|
|
1470
|
-
moved = perform_find_on_line(
|
|
1471
|
-
char: token,
|
|
1472
|
-
direction: pending[:direction],
|
|
1473
|
-
till: pending[:till],
|
|
1474
|
-
count: pending[:count]
|
|
1475
|
-
)
|
|
1476
|
-
if moved
|
|
1477
|
-
@editor.set_last_find(char: token, direction: pending[:direction], till: pending[:till])
|
|
1478
|
-
else
|
|
1479
|
-
@editor.echo("Char not found: #{token}")
|
|
1480
|
-
end
|
|
1481
|
-
end
|
|
1482
|
-
|
|
1483
|
-
def repeat_last_find(reverse:)
|
|
1484
|
-
last = @editor.last_find
|
|
1485
|
-
unless last
|
|
1486
|
-
@editor.echo("No previous f/t")
|
|
1487
|
-
return
|
|
1488
|
-
end
|
|
1489
|
-
|
|
1490
|
-
direction =
|
|
1491
|
-
if reverse
|
|
1492
|
-
last[:direction] == :forward ? :backward : :forward
|
|
1493
|
-
else
|
|
1494
|
-
last[:direction]
|
|
1495
|
-
end
|
|
1496
|
-
count = @editor.pending_count
|
|
1497
|
-
@editor.pending_count = nil
|
|
1498
|
-
@pending_keys = []
|
|
1499
|
-
moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
|
|
1500
|
-
@editor.echo("Char not found: #{last[:char]}") unless moved
|
|
1501
|
-
end
|
|
1502
|
-
|
|
1503
|
-
def perform_find_on_line(char:, direction:, till:, count:)
|
|
1504
|
-
win = @editor.current_window
|
|
1505
|
-
buf = @editor.current_buffer
|
|
1506
|
-
line = buf.line_at(win.cursor_y)
|
|
1507
|
-
pos = win.cursor_x
|
|
1508
|
-
target = nil
|
|
1509
|
-
|
|
1510
|
-
[count.to_i, 1].max.times do
|
|
1511
|
-
idx =
|
|
1512
|
-
if direction == :forward
|
|
1513
|
-
line.index(char, pos + 1)
|
|
1514
|
-
else
|
|
1515
|
-
rindex_from(line, char, pos - 1)
|
|
1516
|
-
end
|
|
1517
|
-
return false if idx.nil?
|
|
1518
|
-
|
|
1519
|
-
target = idx
|
|
1520
|
-
pos = idx
|
|
1521
|
-
end
|
|
1522
|
-
|
|
1523
|
-
if till
|
|
1524
|
-
target =
|
|
1525
|
-
if direction == :forward
|
|
1526
|
-
RuVim::TextMetrics.previous_grapheme_char_index(line, target)
|
|
1527
|
-
else
|
|
1528
|
-
RuVim::TextMetrics.next_grapheme_char_index(line, target)
|
|
1529
|
-
end
|
|
1530
|
-
end
|
|
1531
|
-
|
|
1532
|
-
win.cursor_x = target
|
|
1533
|
-
win.clamp_to_buffer(buf)
|
|
1534
|
-
true
|
|
1535
|
-
end
|
|
1536
|
-
|
|
1537
|
-
def rindex_from(line, char, pos)
|
|
1538
|
-
return nil if pos.negative?
|
|
1539
|
-
|
|
1540
|
-
line.rindex(char, pos)
|
|
1541
|
-
end
|
|
1542
|
-
|
|
1543
|
-
def submit_search(line, direction:)
|
|
1544
|
-
inv = CommandInvocation.new(id: "__search_submit__", argv: [line], kwargs: { pattern: line, direction: direction })
|
|
1545
|
-
ctx = Context.new(editor: @editor, invocation: inv)
|
|
1546
|
-
GlobalCommands.instance.submit_search(ctx, pattern: line, direction: direction)
|
|
1547
|
-
@editor.enter_normal_mode
|
|
1548
|
-
rescue StandardError => e
|
|
1549
|
-
@editor.echo_error("Error: #{e.message}")
|
|
1550
|
-
@editor.enter_normal_mode
|
|
1551
|
-
end
|
|
1552
|
-
|
|
1553
|
-
def push_command_line_history(prefix, line)
|
|
1554
|
-
text = line.to_s
|
|
1555
|
-
return if text.empty?
|
|
1556
|
-
|
|
1557
|
-
hist = @cmdline_history[prefix]
|
|
1558
|
-
hist.delete(text)
|
|
1559
|
-
hist << text
|
|
1560
|
-
hist.shift while hist.length > 100
|
|
1561
|
-
@cmdline_history_index = nil
|
|
1562
|
-
end
|
|
1563
|
-
|
|
1564
|
-
def load_command_line_history!
|
|
1565
|
-
path = command_line_history_file_path
|
|
1566
|
-
return unless path
|
|
1567
|
-
return unless File.file?(path)
|
|
1568
|
-
|
|
1569
|
-
raw = File.read(path)
|
|
1570
|
-
data = JSON.parse(raw)
|
|
1571
|
-
return unless data.is_a?(Hash)
|
|
1572
|
-
|
|
1573
|
-
loaded = Hash.new { |h, k| h[k] = [] }
|
|
1574
|
-
data.each do |prefix, items|
|
|
1575
|
-
key = prefix.to_s
|
|
1576
|
-
next unless [":", "/", "?"].include?(key)
|
|
1577
|
-
next unless items.is_a?(Array)
|
|
1578
|
-
|
|
1579
|
-
hist = loaded[key]
|
|
1580
|
-
items.each do |item|
|
|
1581
|
-
text = item.to_s
|
|
1582
|
-
next if text.empty?
|
|
1583
|
-
|
|
1584
|
-
hist.delete(text)
|
|
1585
|
-
hist << text
|
|
1586
|
-
end
|
|
1587
|
-
hist.shift while hist.length > 100
|
|
1588
|
-
end
|
|
1589
|
-
@cmdline_history = loaded
|
|
1590
|
-
rescue StandardError => e
|
|
1591
|
-
verbose_log(1, "history load error: #{e.message}")
|
|
1592
|
-
end
|
|
1593
|
-
|
|
1594
|
-
def save_command_line_history!
|
|
1595
|
-
path = command_line_history_file_path
|
|
1596
|
-
return unless path
|
|
1597
|
-
|
|
1598
|
-
payload = {
|
|
1599
|
-
":" => Array(@cmdline_history[":"]).map(&:to_s).last(100),
|
|
1600
|
-
"/" => Array(@cmdline_history["/"]).map(&:to_s).last(100),
|
|
1601
|
-
"?" => Array(@cmdline_history["?"]).map(&:to_s).last(100)
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
1605
|
-
tmp = "#{path}.tmp"
|
|
1606
|
-
File.write(tmp, JSON.pretty_generate(payload) + "\n")
|
|
1607
|
-
File.rename(tmp, path)
|
|
1608
|
-
rescue StandardError => e
|
|
1609
|
-
verbose_log(1, "history save error: #{e.message}")
|
|
1610
|
-
end
|
|
1611
|
-
|
|
1612
|
-
def command_line_history_file_path
|
|
1613
|
-
xdg_state_home = ENV["XDG_STATE_HOME"].to_s
|
|
1614
|
-
if !xdg_state_home.empty?
|
|
1615
|
-
return File.join(xdg_state_home, "ruvim", "history.json")
|
|
1616
|
-
end
|
|
1617
|
-
|
|
1618
|
-
home = ENV["HOME"].to_s
|
|
1619
|
-
return nil if home.empty?
|
|
1620
|
-
|
|
1621
|
-
File.join(home, ".ruvim", "history.json")
|
|
1622
|
-
end
|
|
1623
|
-
|
|
1624
|
-
def command_line_history_move(delta)
|
|
1625
|
-
cmd = @editor.command_line
|
|
1626
|
-
hist = @cmdline_history[cmd.prefix]
|
|
1627
|
-
return if hist.empty?
|
|
1628
|
-
|
|
1629
|
-
@cmdline_history_index =
|
|
1630
|
-
if @cmdline_history_index.nil?
|
|
1631
|
-
delta.negative? ? hist.length - 1 : hist.length
|
|
1632
|
-
else
|
|
1633
|
-
@cmdline_history_index + delta
|
|
1634
|
-
end
|
|
1635
|
-
|
|
1636
|
-
@cmdline_history_index = [[@cmdline_history_index, 0].max, hist.length].min
|
|
1637
|
-
if @cmdline_history_index == hist.length
|
|
1638
|
-
cmd.replace_text("")
|
|
1639
|
-
else
|
|
1640
|
-
cmd.replace_text(hist[@cmdline_history_index])
|
|
1641
|
-
end
|
|
1642
|
-
update_incsearch_preview_if_needed
|
|
1643
|
-
end
|
|
1644
|
-
|
|
1645
|
-
def command_line_complete
|
|
1646
|
-
cmd = @editor.command_line
|
|
1647
|
-
return unless cmd.prefix == ":"
|
|
1648
|
-
|
|
1649
|
-
ctx = ex_completion_context(cmd)
|
|
1650
|
-
return unless ctx
|
|
1651
|
-
|
|
1652
|
-
matches = reusable_command_line_completion_matches(cmd, ctx) || ex_completion_candidates(ctx)
|
|
1653
|
-
case matches.length
|
|
1654
|
-
when 0
|
|
1655
|
-
clear_command_line_completion
|
|
1656
|
-
@editor.echo("No completion")
|
|
1657
|
-
when 1
|
|
1658
|
-
clear_command_line_completion
|
|
1659
|
-
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches.first)
|
|
1660
|
-
else
|
|
1661
|
-
apply_wildmode_completion(cmd, ctx, matches)
|
|
1662
|
-
end
|
|
1663
|
-
update_incsearch_preview_if_needed
|
|
1664
|
-
end
|
|
1665
|
-
|
|
1666
|
-
def reusable_command_line_completion_matches(cmd, ctx)
|
|
1667
|
-
state = @cmdline_completion
|
|
1668
|
-
return nil unless state
|
|
1669
|
-
return nil unless state[:prefix] == cmd.prefix
|
|
1670
|
-
return nil unless state[:kind] == ctx[:kind]
|
|
1671
|
-
return nil unless state[:command] == ctx[:command]
|
|
1672
|
-
return nil unless state[:arg_index] == ctx[:arg_index]
|
|
1673
|
-
return nil unless state[:token_start] == ctx[:token_start]
|
|
1674
|
-
|
|
1675
|
-
before_text = cmd.text[0...ctx[:token_start]].to_s
|
|
1676
|
-
after_text = cmd.text[ctx[:token_end]..].to_s
|
|
1677
|
-
return nil unless state[:before_text] == before_text
|
|
1678
|
-
return nil unless state[:after_text] == after_text
|
|
1679
|
-
|
|
1680
|
-
matches = Array(state[:matches]).map(&:to_s)
|
|
1681
|
-
return nil if matches.empty?
|
|
1682
|
-
|
|
1683
|
-
current_token = cmd.text[ctx[:token_start]...ctx[:token_end]].to_s
|
|
1684
|
-
return nil unless current_token.empty? || matches.include?(current_token) || common_prefix(matches).start_with?(current_token) || current_token.start_with?(common_prefix(matches))
|
|
1685
|
-
|
|
1686
|
-
matches
|
|
1687
|
-
end
|
|
1688
|
-
|
|
1689
|
-
def clear_command_line_completion
|
|
1690
|
-
@cmdline_completion = nil
|
|
1691
|
-
end
|
|
1692
|
-
|
|
1693
|
-
def apply_wildmode_completion(cmd, ctx, matches)
|
|
1694
|
-
mode_steps = wildmode_steps
|
|
1695
|
-
mode_steps = [:full] if mode_steps.empty?
|
|
1696
|
-
state = @cmdline_completion
|
|
1697
|
-
before_text = cmd.text[0...ctx[:token_start]].to_s
|
|
1698
|
-
after_text = cmd.text[ctx[:token_end]..].to_s
|
|
1699
|
-
same = state &&
|
|
1700
|
-
state[:prefix] == cmd.prefix &&
|
|
1701
|
-
state[:kind] == ctx[:kind] &&
|
|
1702
|
-
state[:command] == ctx[:command] &&
|
|
1703
|
-
state[:arg_index] == ctx[:arg_index] &&
|
|
1704
|
-
state[:token_start] == ctx[:token_start] &&
|
|
1705
|
-
state[:before_text] == before_text &&
|
|
1706
|
-
state[:after_text] == after_text &&
|
|
1707
|
-
state[:matches] == matches
|
|
1708
|
-
unless same
|
|
1709
|
-
state = {
|
|
1710
|
-
prefix: cmd.prefix,
|
|
1711
|
-
kind: ctx[:kind],
|
|
1712
|
-
command: ctx[:command],
|
|
1713
|
-
arg_index: ctx[:arg_index],
|
|
1714
|
-
token_start: ctx[:token_start],
|
|
1715
|
-
before_text: before_text,
|
|
1716
|
-
after_text: after_text,
|
|
1717
|
-
matches: matches.dup,
|
|
1718
|
-
step_index: -1,
|
|
1719
|
-
full_index: nil
|
|
1720
|
-
}
|
|
1721
|
-
end
|
|
1722
|
-
|
|
1723
|
-
state[:step_index] += 1
|
|
1724
|
-
step = mode_steps[state[:step_index] % mode_steps.length]
|
|
1725
|
-
case step
|
|
1726
|
-
when :longest
|
|
1727
|
-
pref = common_prefix(matches)
|
|
1728
|
-
cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
|
|
1729
|
-
when :list
|
|
1730
|
-
show_command_line_completion_menu(matches, selected: state[:full_index], force: true)
|
|
1731
|
-
when :full
|
|
1732
|
-
state[:full_index] = state[:full_index] ? (state[:full_index] + 1) % matches.length : 0
|
|
1733
|
-
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches[state[:full_index]])
|
|
1734
|
-
show_command_line_completion_menu(matches, selected: state[:full_index], force: false)
|
|
1735
|
-
else
|
|
1736
|
-
pref = common_prefix(matches)
|
|
1737
|
-
cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
|
|
1738
|
-
end
|
|
1739
|
-
|
|
1740
|
-
@cmdline_completion = state
|
|
1741
|
-
end
|
|
1742
|
-
|
|
1743
|
-
def wildmode_steps
|
|
1744
|
-
raw = @editor.effective_option("wildmode").to_s
|
|
1745
|
-
return [:full] if raw.empty?
|
|
1746
|
-
|
|
1747
|
-
raw.split(",").flat_map do |tok|
|
|
1748
|
-
tok.to_s.split(":").map do |part|
|
|
1749
|
-
case part.strip.downcase
|
|
1750
|
-
when "longest" then :longest
|
|
1751
|
-
when "list" then :list
|
|
1752
|
-
when "full" then :full
|
|
1753
|
-
end
|
|
1754
|
-
end
|
|
1755
|
-
end.compact
|
|
1756
|
-
end
|
|
1757
|
-
|
|
1758
|
-
def show_command_line_completion_menu(matches, selected:, force:)
|
|
1759
|
-
return unless force || @editor.effective_option("wildmenu")
|
|
1760
|
-
|
|
1761
|
-
items = matches.each_with_index.map do |m, i|
|
|
1762
|
-
idx = i
|
|
1763
|
-
idx == selected ? "[#{m}]" : m
|
|
1764
|
-
end
|
|
1765
|
-
@editor.echo(compose_command_line_completion_menu(items))
|
|
1766
|
-
end
|
|
1767
|
-
|
|
1768
|
-
def compose_command_line_completion_menu(items)
|
|
1769
|
-
parts = Array(items).map(&:to_s)
|
|
1770
|
-
return "" if parts.empty?
|
|
1771
|
-
|
|
1772
|
-
width = command_line_completion_menu_width
|
|
1773
|
-
width = [width.to_i, 1].max
|
|
1774
|
-
out = +""
|
|
1775
|
-
shown = 0
|
|
1776
|
-
|
|
1777
|
-
parts.each_with_index do |item, idx|
|
|
1778
|
-
token = shown.zero? ? item : " #{item}"
|
|
1779
|
-
if out.empty? && token.length > width
|
|
1780
|
-
out = token[0, width]
|
|
1781
|
-
shown = 1
|
|
1782
|
-
break
|
|
1783
|
-
end
|
|
1784
|
-
break if out.length + token.length > width
|
|
1785
|
-
|
|
1786
|
-
out << token
|
|
1787
|
-
shown = idx + 1
|
|
1788
|
-
end
|
|
1789
|
-
|
|
1790
|
-
if shown < parts.length
|
|
1791
|
-
ellipsis = (out.empty? ? "..." : " ...")
|
|
1792
|
-
if out.length + ellipsis.length <= width
|
|
1793
|
-
out << ellipsis
|
|
1794
|
-
elsif width >= 3
|
|
1795
|
-
out = out[0, width - 3] + "..."
|
|
1796
|
-
else
|
|
1797
|
-
out = "." * width
|
|
1798
|
-
end
|
|
1799
|
-
end
|
|
1800
|
-
|
|
1801
|
-
out
|
|
1802
|
-
end
|
|
1803
|
-
|
|
1804
|
-
def command_line_completion_menu_width
|
|
1805
|
-
return 80 unless defined?(@terminal) && @terminal && @terminal.respond_to?(:winsize)
|
|
1806
|
-
|
|
1807
|
-
_rows, cols = @terminal.winsize
|
|
1808
|
-
[cols.to_i, 1].max
|
|
1809
|
-
rescue StandardError
|
|
1810
|
-
80
|
|
1811
|
-
end
|
|
1812
|
-
|
|
1813
|
-
def common_prefix(strings)
|
|
1814
|
-
return "" if strings.empty?
|
|
1815
|
-
|
|
1816
|
-
prefix = strings.first.dup
|
|
1817
|
-
strings[1..]&.each do |s|
|
|
1818
|
-
while !prefix.empty? && !s.start_with?(prefix)
|
|
1819
|
-
prefix = prefix[0...-1]
|
|
1820
|
-
end
|
|
1821
|
-
end
|
|
1822
|
-
prefix
|
|
1823
|
-
end
|
|
1824
|
-
|
|
1825
|
-
def clear_insert_completion
|
|
1826
|
-
@insert_completion = nil
|
|
1827
|
-
end
|
|
1828
|
-
|
|
1829
|
-
def insert_tab_in_insert_mode
|
|
1830
|
-
buf = @editor.current_buffer
|
|
1831
|
-
win = @editor.current_window
|
|
1832
|
-
if @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
1833
|
-
width = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
1834
|
-
width = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if width <= 0
|
|
1835
|
-
width = 2 if width <= 0
|
|
1836
|
-
line = buf.line_at(win.cursor_y)
|
|
1837
|
-
current_col = RuVim::TextMetrics.screen_col_for_char_index(line, win.cursor_x, tabstop: effective_tabstop(win, buf))
|
|
1838
|
-
spaces = width - (current_col % width)
|
|
1839
|
-
spaces = width if spaces <= 0
|
|
1840
|
-
_y, x = buf.insert_text(win.cursor_y, win.cursor_x, " " * spaces)
|
|
1841
|
-
win.cursor_x = x
|
|
1842
|
-
else
|
|
1843
|
-
buf.insert_char(win.cursor_y, win.cursor_x, "\t")
|
|
1844
|
-
win.cursor_x += 1
|
|
1845
|
-
end
|
|
1846
|
-
end
|
|
1847
|
-
|
|
1848
|
-
def apply_insert_autoindent(row, x, previous_row:)
|
|
1849
|
-
buf = @editor.current_buffer
|
|
1850
|
-
win = @editor.current_window
|
|
1851
|
-
return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
|
|
1852
|
-
return x if previous_row.negative?
|
|
1853
|
-
|
|
1854
|
-
prev = buf.line_at(previous_row)
|
|
1855
|
-
indent = prev[/\A[ \t]*/].to_s
|
|
1856
|
-
if @editor.effective_option("smartindent", window: win, buffer: buf)
|
|
1857
|
-
trimmed = prev.rstrip
|
|
1858
|
-
needs_indent = trimmed.end_with?("{", "[", "(")
|
|
1859
|
-
if !needs_indent
|
|
1860
|
-
needs_indent = buf.lang_module.indent_trigger?(trimmed)
|
|
1861
|
-
end
|
|
1862
|
-
if needs_indent
|
|
1863
|
-
sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
|
|
1864
|
-
sw = effective_tabstop(win, buf) if sw <= 0
|
|
1865
|
-
sw = 2 if sw <= 0
|
|
1866
|
-
indent += " " * sw
|
|
1867
|
-
end
|
|
1868
|
-
end
|
|
1869
|
-
return x if indent.empty?
|
|
1870
|
-
|
|
1871
|
-
_y, new_x = buf.insert_text(row, x, indent)
|
|
1872
|
-
new_x
|
|
1873
|
-
end
|
|
1874
|
-
|
|
1875
|
-
def maybe_showmatch_after_insert(key)
|
|
1876
|
-
return unless [")", "]", "}"].include?(key)
|
|
1877
|
-
return unless @editor.effective_option("showmatch")
|
|
1878
|
-
|
|
1879
|
-
mt = @editor.effective_option("matchtime").to_i
|
|
1880
|
-
mt = 5 if mt <= 0
|
|
1881
|
-
@editor.echo_temporary("match", duration_seconds: mt * 0.1)
|
|
1882
|
-
end
|
|
1883
|
-
|
|
1884
|
-
def maybe_dedent_after_insert(key)
|
|
1885
|
-
return unless @editor.effective_option("smartindent", window: @editor.current_window, buffer: @editor.current_buffer)
|
|
1886
|
-
|
|
1887
|
-
buf = @editor.current_buffer
|
|
1888
|
-
lang_mod = buf.lang_module
|
|
1889
|
-
|
|
1890
|
-
pattern = lang_mod.dedent_trigger(key)
|
|
1891
|
-
return unless pattern
|
|
1892
|
-
|
|
1893
|
-
row = @editor.current_window.cursor_y
|
|
1894
|
-
line = buf.line_at(row)
|
|
1895
|
-
m = line.match(pattern)
|
|
1896
|
-
return unless m
|
|
1897
|
-
|
|
1898
|
-
sw = @editor.effective_option("shiftwidth", buffer: buf).to_i
|
|
1899
|
-
sw = 2 if sw <= 0
|
|
1900
|
-
target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
|
|
1901
|
-
return unless target_indent
|
|
1902
|
-
|
|
1903
|
-
current_indent = m[1].length
|
|
1904
|
-
return if current_indent == target_indent
|
|
1905
|
-
|
|
1906
|
-
stripped = line.to_s.strip
|
|
1907
|
-
buf.delete_span(row, 0, row, current_indent) if current_indent > 0
|
|
1908
|
-
buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
|
|
1909
|
-
@editor.current_window.cursor_x = target_indent + stripped.length
|
|
1910
|
-
end
|
|
1911
|
-
|
|
1912
|
-
def clear_expired_transient_message_if_any
|
|
1913
|
-
@needs_redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
|
|
1914
|
-
end
|
|
1915
|
-
|
|
1916
|
-
def effective_tabstop(window = @editor.current_window, buffer = @editor.current_buffer)
|
|
1917
|
-
v = @editor.effective_option("tabstop", window:, buffer:).to_i
|
|
1918
|
-
v.positive? ? v : 2
|
|
1919
|
-
end
|
|
1920
|
-
|
|
1921
|
-
def insert_complete(direction)
|
|
1922
|
-
state = ensure_insert_completion_state
|
|
1923
|
-
return unless state
|
|
1924
|
-
|
|
1925
|
-
matches = state[:matches]
|
|
1926
|
-
if matches.empty?
|
|
1927
|
-
@editor.echo("No completion")
|
|
1928
|
-
return
|
|
1929
|
-
end
|
|
1930
|
-
|
|
1931
|
-
if state[:index].nil? && insert_completion_noselect? && matches.length > 1
|
|
1932
|
-
show_insert_completion_menu(matches, selected: nil)
|
|
1933
|
-
state[:index] = :pending_select
|
|
1934
|
-
return
|
|
1935
|
-
end
|
|
1936
|
-
|
|
1937
|
-
if state[:index].nil? && insert_completion_noinsert?
|
|
1938
|
-
preview_idx = direction.positive? ? 0 : matches.length - 1
|
|
1939
|
-
state[:index] = :pending_insert
|
|
1940
|
-
state[:pending_index] = preview_idx
|
|
1941
|
-
show_insert_completion_menu(matches, selected: preview_idx, current: matches[preview_idx])
|
|
1942
|
-
return
|
|
1943
|
-
end
|
|
1944
|
-
|
|
1945
|
-
idx = state[:index]
|
|
1946
|
-
idx = nil if idx == :pending_select
|
|
1947
|
-
if idx == :pending_insert
|
|
1948
|
-
idx = state.delete(:pending_index) || (direction.positive? ? 0 : matches.length - 1)
|
|
1949
|
-
else
|
|
1950
|
-
idx = idx.nil? ? (direction.positive? ? 0 : matches.length - 1) : (idx + direction) % matches.length
|
|
1951
|
-
end
|
|
1952
|
-
replacement = matches[idx]
|
|
1953
|
-
|
|
1954
|
-
end_col = state[:current_end_col]
|
|
1955
|
-
start_col = state[:start_col]
|
|
1956
|
-
@editor.current_buffer.delete_span(state[:row], start_col, state[:row], end_col)
|
|
1957
|
-
_y, new_x = @editor.current_buffer.insert_text(state[:row], start_col, replacement)
|
|
1958
|
-
@editor.current_window.cursor_y = state[:row]
|
|
1959
|
-
@editor.current_window.cursor_x = new_x
|
|
1960
|
-
state[:index] = idx
|
|
1961
|
-
state[:current_end_col] = start_col + replacement.length
|
|
1962
|
-
if matches.length == 1
|
|
1963
|
-
@editor.echo(replacement)
|
|
1964
|
-
else
|
|
1965
|
-
show_insert_completion_menu(matches, selected: idx, current: replacement)
|
|
1966
|
-
end
|
|
1967
|
-
rescue StandardError => e
|
|
1968
|
-
@editor.echo_error("Completion error: #{e.message}")
|
|
1969
|
-
clear_insert_completion
|
|
1970
|
-
end
|
|
1971
|
-
|
|
1972
|
-
def insert_completion_noselect?
|
|
1973
|
-
@editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noselect")
|
|
1974
|
-
end
|
|
1975
|
-
|
|
1976
|
-
def insert_completion_noinsert?
|
|
1977
|
-
@editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noinsert")
|
|
1978
|
-
end
|
|
1979
|
-
|
|
1980
|
-
def insert_completion_menu_enabled?
|
|
1981
|
-
opts = @editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }
|
|
1982
|
-
opts.include?("menu") || opts.include?("menuone")
|
|
1983
|
-
end
|
|
1984
|
-
|
|
1985
|
-
def show_insert_completion_menu(matches, selected:, current: nil)
|
|
1986
|
-
if insert_completion_menu_enabled?
|
|
1987
|
-
limit = [@editor.effective_option("pumheight").to_i, 1].max
|
|
1988
|
-
items = matches.first(limit).each_with_index.map do |m, i|
|
|
1989
|
-
i == selected ? "[#{m}]" : m
|
|
1990
|
-
end
|
|
1991
|
-
items << "..." if matches.length > limit
|
|
1992
|
-
if current
|
|
1993
|
-
@editor.echo("#{current} (#{selected + 1}/#{matches.length}) | #{items.join(' ')}")
|
|
1994
|
-
else
|
|
1995
|
-
@editor.echo(items.join(" "))
|
|
1996
|
-
end
|
|
1997
|
-
elsif current
|
|
1998
|
-
@editor.echo("#{current} (#{selected + 1}/#{matches.length})")
|
|
1999
|
-
end
|
|
2000
|
-
end
|
|
2001
|
-
|
|
2002
|
-
def ensure_insert_completion_state
|
|
2003
|
-
row = @editor.current_window.cursor_y
|
|
2004
|
-
col = @editor.current_window.cursor_x
|
|
2005
|
-
line = @editor.current_buffer.line_at(row)
|
|
2006
|
-
prefix = trailing_keyword_fragment(line[0...col].to_s, @editor.current_window, @editor.current_buffer)
|
|
2007
|
-
return nil if prefix.nil? || prefix.empty?
|
|
2008
|
-
|
|
2009
|
-
start_col = col - prefix.length
|
|
2010
|
-
current_token = line[start_col...col].to_s
|
|
2011
|
-
state = @insert_completion
|
|
2012
|
-
|
|
2013
|
-
if state &&
|
|
2014
|
-
state[:row] == row &&
|
|
2015
|
-
state[:start_col] == start_col &&
|
|
2016
|
-
state[:prefix] == prefix &&
|
|
2017
|
-
col == state[:current_end_col]
|
|
2018
|
-
return state
|
|
2019
|
-
end
|
|
2020
|
-
|
|
2021
|
-
matches = collect_buffer_word_completions(prefix, current_word: current_token)
|
|
2022
|
-
@insert_completion = {
|
|
2023
|
-
row: row,
|
|
2024
|
-
start_col: start_col,
|
|
2025
|
-
prefix: prefix,
|
|
2026
|
-
matches: matches,
|
|
2027
|
-
index: nil,
|
|
2028
|
-
current_end_col: col
|
|
2029
|
-
}
|
|
2030
|
-
end
|
|
2031
|
-
|
|
2032
|
-
def collect_buffer_word_completions(prefix, current_word:)
|
|
2033
|
-
words = []
|
|
2034
|
-
seen = {}
|
|
2035
|
-
rx = keyword_scan_regex(@editor.current_window, @editor.current_buffer)
|
|
2036
|
-
@editor.buffers.values.each do |buf|
|
|
2037
|
-
buf.lines.each do |line|
|
|
2038
|
-
line.scan(rx) do |w|
|
|
2039
|
-
next unless w.start_with?(prefix)
|
|
2040
|
-
next if w == current_word
|
|
2041
|
-
next if seen[w]
|
|
2042
|
-
|
|
2043
|
-
seen[w] = true
|
|
2044
|
-
words << w
|
|
2045
|
-
end
|
|
2046
|
-
end
|
|
2047
|
-
end
|
|
2048
|
-
words.sort
|
|
2049
|
-
end
|
|
2050
|
-
|
|
2051
|
-
def track_mode_transition(mode_before)
|
|
2052
|
-
mode_after = @editor.mode
|
|
2053
|
-
if mode_before != :insert && mode_after == :insert
|
|
2054
|
-
@insert_start_location = @editor.current_location
|
|
2055
|
-
elsif mode_before == :insert && mode_after != :insert
|
|
2056
|
-
@insert_start_location = nil
|
|
2057
|
-
end
|
|
2058
|
-
|
|
2059
|
-
if mode_before != :command_line && mode_after == :command_line
|
|
2060
|
-
@incsearch_preview = nil
|
|
2061
|
-
elsif mode_before == :command_line && mode_after != :command_line
|
|
2062
|
-
@incsearch_preview = nil
|
|
2063
|
-
end
|
|
2064
|
-
end
|
|
2065
|
-
|
|
2066
|
-
def insert_backspace_allowed?
|
|
2067
|
-
buf = @editor.current_buffer
|
|
2068
|
-
win = @editor.current_window
|
|
2069
|
-
row = win.cursor_y
|
|
2070
|
-
col = win.cursor_x
|
|
2071
|
-
return false if row.zero? && col.zero?
|
|
2072
|
-
|
|
2073
|
-
opt = @editor.effective_option("backspace", window: win, buffer: buf).to_s
|
|
2074
|
-
allow = opt.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
2075
|
-
allow_all = allow.include?("2")
|
|
2076
|
-
allow_indent = allow_all || allow.include?("indent")
|
|
2077
|
-
|
|
2078
|
-
if col.zero? && row.positive?
|
|
2079
|
-
return true if allow_all || allow.include?("eol")
|
|
2080
|
-
|
|
2081
|
-
@editor.echo_error("backspace=eol required")
|
|
2082
|
-
return false
|
|
2083
|
-
end
|
|
2084
|
-
|
|
2085
|
-
if @insert_start_location
|
|
2086
|
-
same_buf = @insert_start_location[:buffer_id] == buf.id
|
|
2087
|
-
if same_buf && (row < @insert_start_location[:row] || (row == @insert_start_location[:row] && col <= @insert_start_location[:col]))
|
|
2088
|
-
if allow_all || allow.include?("start")
|
|
2089
|
-
return true
|
|
2090
|
-
end
|
|
2091
|
-
|
|
2092
|
-
if allow_indent && same_row_autoindent_backspace?(buf, row, col)
|
|
2093
|
-
return true
|
|
2094
|
-
end
|
|
2095
|
-
|
|
2096
|
-
@editor.echo_error("backspace=start required")
|
|
2097
|
-
return false
|
|
2098
|
-
end
|
|
2099
|
-
end
|
|
2100
|
-
|
|
2101
|
-
true
|
|
2102
|
-
end
|
|
2103
|
-
|
|
2104
|
-
def insert_backspace_in_insert_mode
|
|
2105
|
-
buf = @editor.current_buffer
|
|
2106
|
-
win = @editor.current_window
|
|
2107
|
-
row = win.cursor_y
|
|
2108
|
-
col = win.cursor_x
|
|
2109
|
-
|
|
2110
|
-
if row >= 0 && col.positive? && try_softtabstop_backspace(buf, win)
|
|
2111
|
-
return
|
|
2112
|
-
end
|
|
2113
|
-
|
|
2114
|
-
y, x = buf.backspace(row, col)
|
|
2115
|
-
win.cursor_y = y
|
|
2116
|
-
win.cursor_x = x
|
|
2117
|
-
end
|
|
2118
|
-
|
|
2119
|
-
def dispatch_insert_cursor_motion(id)
|
|
2120
|
-
@dispatcher.dispatch(@editor, CommandInvocation.new(id: id, count: 1))
|
|
2121
|
-
rescue StandardError => e
|
|
2122
|
-
@editor.echo_error("Motion error: #{e.message}")
|
|
2123
|
-
end
|
|
2124
|
-
|
|
2125
|
-
def try_softtabstop_backspace(buf, win)
|
|
2126
|
-
row = win.cursor_y
|
|
2127
|
-
col = win.cursor_x
|
|
2128
|
-
line = buf.line_at(row)
|
|
2129
|
-
return false unless line
|
|
2130
|
-
return false unless @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
2131
|
-
|
|
2132
|
-
sts = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
2133
|
-
sts = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if sts <= 0
|
|
2134
|
-
return false if sts <= 0
|
|
2135
|
-
|
|
2136
|
-
prefix = line[0...col].to_s
|
|
2137
|
-
m = prefix.match(/ +\z/)
|
|
2138
|
-
return false unless m
|
|
2139
|
-
|
|
2140
|
-
run = m[0].length
|
|
2141
|
-
return false if run <= 1
|
|
2142
|
-
|
|
2143
|
-
tabstop = effective_tabstop(win, buf)
|
|
2144
|
-
cur_screen = RuVim::TextMetrics.screen_col_for_char_index(line, col, tabstop:)
|
|
2145
|
-
target_screen = [cur_screen - sts, 0].max
|
|
2146
|
-
target_col = RuVim::TextMetrics.char_index_for_screen_col(line, target_screen, tabstop:, align: :floor)
|
|
2147
|
-
delete_cols = col - target_col
|
|
2148
|
-
delete_cols = [delete_cols, run, sts].min
|
|
2149
|
-
return false if delete_cols <= 1
|
|
2150
|
-
|
|
2151
|
-
# Only collapse whitespace run; if target lands before the run, clamp to run start.
|
|
2152
|
-
run_start = col - run
|
|
2153
|
-
target_col = [target_col, run_start].max
|
|
2154
|
-
delete_cols = col - target_col
|
|
2155
|
-
return false if delete_cols <= 1
|
|
2156
|
-
|
|
2157
|
-
buf.delete_span(row, target_col, row, col)
|
|
2158
|
-
win.cursor_x = target_col
|
|
2159
|
-
true
|
|
2160
|
-
rescue StandardError
|
|
2161
|
-
false
|
|
2162
|
-
end
|
|
2163
|
-
|
|
2164
|
-
def same_row_autoindent_backspace?(buf, row, col)
|
|
2165
|
-
return false unless @insert_start_location
|
|
2166
|
-
return false unless row == @insert_start_location[:row]
|
|
2167
|
-
return false unless col <= @insert_start_location[:col]
|
|
2168
|
-
|
|
2169
|
-
line = buf.line_at(row)
|
|
2170
|
-
line[0...@insert_start_location[:col]].to_s.match?(/\A[ \t]*\z/)
|
|
2171
|
-
rescue StandardError
|
|
2172
|
-
false
|
|
2173
|
-
end
|
|
2174
|
-
|
|
2175
|
-
def incsearch_enabled?
|
|
2176
|
-
return false unless @editor.command_line_active?
|
|
2177
|
-
return false unless ["/", "?"].include?(@editor.command_line.prefix)
|
|
2178
|
-
|
|
2179
|
-
!!@editor.effective_option("incsearch")
|
|
2180
|
-
end
|
|
2181
|
-
|
|
2182
|
-
def update_incsearch_preview_if_needed
|
|
2183
|
-
return unless incsearch_enabled?
|
|
2184
|
-
|
|
2185
|
-
cmd = @editor.command_line
|
|
2186
|
-
ensure_incsearch_preview_origin!(direction: (cmd.prefix == "/" ? :forward : :backward))
|
|
2187
|
-
pattern = cmd.text.to_s
|
|
2188
|
-
if pattern.empty?
|
|
2189
|
-
clear_incsearch_preview_state(apply: false)
|
|
2190
|
-
return
|
|
2191
|
-
end
|
|
2192
|
-
|
|
2193
|
-
buf = @editor.current_buffer
|
|
2194
|
-
win = @editor.current_window
|
|
2195
|
-
origin = @incsearch_preview[:origin]
|
|
2196
|
-
tmp_window = RuVim::Window.new(id: -1, buffer_id: buf.id)
|
|
2197
|
-
tmp_window.cursor_y = origin[:row]
|
|
2198
|
-
tmp_window.cursor_x = origin[:col]
|
|
2199
|
-
regex = GlobalCommands.instance.send(:compile_search_regex, pattern, editor: @editor, window: win, buffer: buf)
|
|
2200
|
-
match = GlobalCommands.instance.send(:find_next_match, buf, tmp_window, regex, direction: @incsearch_preview[:direction])
|
|
2201
|
-
if match
|
|
2202
|
-
win.cursor_y = match[:row]
|
|
2203
|
-
win.cursor_x = match[:col]
|
|
2204
|
-
win.clamp_to_buffer(buf)
|
|
2205
|
-
end
|
|
2206
|
-
@incsearch_preview[:active] = true
|
|
2207
|
-
rescue RuVim::CommandError, RegexpError
|
|
2208
|
-
# Keep editing command-line without forcing an error flash on every keystroke.
|
|
2209
|
-
end
|
|
2210
|
-
|
|
2211
|
-
def ensure_incsearch_preview_origin!(direction:)
|
|
2212
|
-
return if @incsearch_preview
|
|
2213
|
-
|
|
2214
|
-
@incsearch_preview = {
|
|
2215
|
-
origin: @editor.current_location,
|
|
2216
|
-
direction: direction,
|
|
2217
|
-
active: false
|
|
2218
|
-
}
|
|
2219
|
-
end
|
|
2220
|
-
|
|
2221
|
-
def cancel_incsearch_preview_if_any
|
|
2222
|
-
clear_incsearch_preview_state(apply: false)
|
|
2223
|
-
end
|
|
2224
|
-
|
|
2225
|
-
def clear_incsearch_preview_state(apply:)
|
|
2226
|
-
return unless @incsearch_preview
|
|
2227
|
-
|
|
2228
|
-
if !apply && @incsearch_preview[:origin]
|
|
2229
|
-
@editor.jump_to_location(@incsearch_preview[:origin])
|
|
2230
|
-
end
|
|
2231
|
-
@incsearch_preview = nil
|
|
2232
|
-
end
|
|
2233
|
-
|
|
2234
|
-
def trailing_keyword_fragment(prefix_text, window, buffer)
|
|
2235
|
-
cls = keyword_char_class(window, buffer)
|
|
2236
|
-
prefix_text.to_s[/[#{cls}]+\z/]
|
|
2237
|
-
rescue RegexpError
|
|
2238
|
-
prefix_text.to_s[/[[:alnum:]_]+\z/]
|
|
2239
|
-
end
|
|
2240
|
-
|
|
2241
|
-
def keyword_scan_regex(window, buffer)
|
|
2242
|
-
cls = keyword_char_class(window, buffer)
|
|
2243
|
-
/[#{cls}]+/
|
|
2244
|
-
rescue RegexpError
|
|
2245
|
-
/[[:alnum:]_]+/
|
|
2246
|
-
end
|
|
2247
|
-
|
|
2248
|
-
def keyword_char_class(window, buffer)
|
|
2249
|
-
raw = @editor.effective_option("iskeyword", window:, buffer:).to_s
|
|
2250
|
-
RuVim::KeywordChars.char_class(raw)
|
|
2251
|
-
rescue StandardError
|
|
2252
|
-
"[:alnum:]_"
|
|
2253
|
-
end
|
|
2254
|
-
|
|
2255
|
-
def ex_completion_context(cmd)
|
|
2256
|
-
text = cmd.text
|
|
2257
|
-
cursor = cmd.cursor
|
|
2258
|
-
token_start = token_start_index(text, cursor)
|
|
2259
|
-
token_end = token_end_index(text, cursor)
|
|
2260
|
-
prefix = text[token_start...cursor].to_s
|
|
2261
|
-
before = text[0...token_start].to_s
|
|
2262
|
-
argv_before = before.split(/\s+/).reject(&:empty?)
|
|
2263
|
-
|
|
2264
|
-
if argv_before.empty?
|
|
2265
|
-
{
|
|
2266
|
-
kind: :command,
|
|
2267
|
-
token_start: token_start,
|
|
2268
|
-
token_end: token_end,
|
|
2269
|
-
prefix: prefix
|
|
2270
|
-
}
|
|
2271
|
-
else
|
|
2272
|
-
{
|
|
2273
|
-
kind: :arg,
|
|
2274
|
-
command: argv_before.first,
|
|
2275
|
-
arg_index: argv_before.length - 1,
|
|
2276
|
-
token_start: token_start,
|
|
2277
|
-
token_end: token_end,
|
|
2278
|
-
prefix: prefix
|
|
2279
|
-
}
|
|
2280
|
-
end
|
|
2281
|
-
end
|
|
2282
|
-
|
|
2283
|
-
def ex_completion_candidates(ctx)
|
|
2284
|
-
case ctx[:kind]
|
|
2285
|
-
when :command
|
|
2286
|
-
ExCommandRegistry.instance.all.flat_map { |spec| [spec.name, *spec.aliases] }.uniq.sort.select { |n| n.start_with?(ctx[:prefix]) }
|
|
2287
|
-
when :arg
|
|
2288
|
-
ex_arg_completion_candidates(ctx[:command], ctx[:arg_index], ctx[:prefix])
|
|
2289
|
-
else
|
|
2290
|
-
[]
|
|
2291
|
-
end
|
|
2292
|
-
end
|
|
2293
|
-
|
|
2294
|
-
def ex_arg_completion_candidates(command_name, arg_index, prefix)
|
|
2295
|
-
cmd = command_name.to_s
|
|
2296
|
-
return [] unless arg_index.zero?
|
|
2297
|
-
|
|
2298
|
-
if %w[e edit w write tabnew].include?(cmd)
|
|
2299
|
-
return path_completion_candidates(prefix)
|
|
2300
|
-
end
|
|
2301
|
-
|
|
2302
|
-
if %w[buffer b].include?(cmd)
|
|
2303
|
-
return buffer_completion_candidates(prefix)
|
|
2304
|
-
end
|
|
2305
|
-
|
|
2306
|
-
if %w[set setlocal setglobal].include?(cmd)
|
|
2307
|
-
return option_completion_candidates(prefix)
|
|
2308
|
-
end
|
|
2309
|
-
|
|
2310
|
-
[]
|
|
2311
|
-
end
|
|
2312
|
-
|
|
2313
|
-
def path_completion_candidates(prefix)
|
|
2314
|
-
input = prefix.to_s
|
|
2315
|
-
base_dir =
|
|
2316
|
-
if input.empty?
|
|
2317
|
-
"."
|
|
2318
|
-
elsif input.end_with?("/")
|
|
2319
|
-
input
|
|
2320
|
-
else
|
|
2321
|
-
File.dirname(input)
|
|
2322
|
-
end
|
|
2323
|
-
partial = input.end_with?("/") ? "" : File.basename(input)
|
|
2324
|
-
pattern =
|
|
2325
|
-
if input.empty?
|
|
2326
|
-
"*"
|
|
2327
|
-
elsif base_dir == "."
|
|
2328
|
-
"#{partial}*"
|
|
2329
|
-
else
|
|
2330
|
-
File.join(base_dir, "#{partial}*")
|
|
2331
|
-
end
|
|
2332
|
-
partial_starts_with_dot = partial.start_with?(".")
|
|
2333
|
-
entries = Dir.glob(pattern, File::FNM_DOTMATCH).filter_map do |p|
|
|
2334
|
-
next if [".", ".."].include?(File.basename(p))
|
|
2335
|
-
next unless p.start_with?(input) || input.empty?
|
|
2336
|
-
next if wildignore_path?(p)
|
|
2337
|
-
File.directory?(p) ? "#{p}/" : p
|
|
2338
|
-
end
|
|
2339
|
-
entries.sort_by do |p|
|
|
2340
|
-
base = File.basename(p.to_s.sub(%r{/\z}, ""))
|
|
2341
|
-
hidden_rank = (!partial_starts_with_dot && base.start_with?(".")) ? 1 : 0
|
|
2342
|
-
[hidden_rank, p]
|
|
2343
|
-
end
|
|
2344
|
-
rescue StandardError
|
|
2345
|
-
[]
|
|
2346
|
-
end
|
|
2347
|
-
|
|
2348
|
-
def wildignore_path?(path)
|
|
2349
|
-
spec = @editor.global_options["wildignore"].to_s
|
|
2350
|
-
return false if spec.empty?
|
|
2351
|
-
|
|
2352
|
-
flags = @editor.global_options["wildignorecase"] ? File::FNM_CASEFOLD : 0
|
|
2353
|
-
name = path.to_s
|
|
2354
|
-
base = File.basename(name)
|
|
2355
|
-
spec.split(",").map(&:strip).reject(&:empty?).any? do |pat|
|
|
2356
|
-
File.fnmatch?(pat, name, flags) || File.fnmatch?(pat, base, flags)
|
|
2357
|
-
end
|
|
2358
|
-
rescue StandardError
|
|
2359
|
-
false
|
|
2360
|
-
end
|
|
2361
|
-
|
|
2362
|
-
def buffer_completion_candidates(prefix)
|
|
2363
|
-
pfx = prefix.to_s
|
|
2364
|
-
items = @editor.buffers.values.flat_map do |b|
|
|
2365
|
-
path = b.path.to_s
|
|
2366
|
-
base = path.empty? ? nil : File.basename(path)
|
|
2367
|
-
[b.id.to_s, path, base].compact
|
|
2368
|
-
end.uniq.sort
|
|
2369
|
-
items.select { |s| s.start_with?(pfx) }
|
|
2370
|
-
end
|
|
2371
|
-
|
|
2372
|
-
def option_completion_candidates(prefix)
|
|
2373
|
-
pfx = prefix.to_s
|
|
2374
|
-
names = RuVim::Editor::OPTION_DEFS.keys
|
|
2375
|
-
tokens = names + names.map { |n| "no#{n}" } + names.map { |n| "inv#{n}" } + names.map { |n| "#{n}?" }
|
|
2376
|
-
tokens.uniq.sort.select { |s| s.start_with?(pfx) }
|
|
2377
|
-
end
|
|
2378
|
-
|
|
2379
|
-
def token_start_index(text, cursor)
|
|
2380
|
-
i = [[cursor, 0].max, text.length].min
|
|
2381
|
-
i -= 1 while i.positive? && !whitespace_char?(text[i - 1])
|
|
2382
|
-
i
|
|
2383
|
-
end
|
|
2384
|
-
|
|
2385
|
-
def token_end_index(text, cursor)
|
|
2386
|
-
i = [[cursor, 0].max, text.length].min
|
|
2387
|
-
i += 1 while i < text.length && !whitespace_char?(text[i])
|
|
2388
|
-
i
|
|
2389
|
-
end
|
|
2390
|
-
|
|
2391
|
-
def whitespace_char?(ch)
|
|
2392
|
-
ch && ch.match?(/\s/)
|
|
2393
|
-
end
|
|
2394
|
-
|
|
2395
|
-
def install_signal_handlers
|
|
2396
|
-
Signal.trap("WINCH") do
|
|
2397
|
-
@screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
|
|
2398
|
-
@needs_redraw = true
|
|
2399
|
-
notify_signal_wakeup
|
|
2400
|
-
end
|
|
2401
|
-
rescue ArgumentError
|
|
2402
|
-
nil
|
|
2403
|
-
end
|
|
2404
|
-
|
|
2405
|
-
def init_config_loader!
|
|
2406
|
-
@config_loader = ConfigLoader.new(
|
|
2407
|
-
command_registry: CommandRegistry.instance,
|
|
2408
|
-
ex_registry: ExCommandRegistry.instance,
|
|
2409
|
-
keymaps: @keymaps,
|
|
2410
|
-
command_host: GlobalCommands.instance
|
|
2411
|
-
)
|
|
2412
|
-
end
|
|
2413
|
-
|
|
2414
|
-
def load_user_config!
|
|
2415
|
-
return if @clean_mode || @restricted_mode
|
|
2416
|
-
return if @skip_user_config
|
|
2417
|
-
|
|
2418
|
-
if @config_path
|
|
2419
|
-
@config_loader.load_file(@config_path)
|
|
2420
|
-
else
|
|
2421
|
-
@config_loader.load_default!
|
|
2422
|
-
end
|
|
2423
|
-
rescue StandardError => e
|
|
2424
|
-
@editor.echo_error("config error: #{e.message}")
|
|
2425
|
-
end
|
|
2426
|
-
|
|
2427
|
-
def load_current_ftplugin!
|
|
2428
|
-
return if @clean_mode || @restricted_mode
|
|
2429
|
-
return unless @config_loader
|
|
2430
|
-
|
|
2431
|
-
@config_loader.load_ftplugin!(@editor, @editor.current_buffer)
|
|
2432
|
-
rescue StandardError => e
|
|
2433
|
-
@editor.echo_error("ftplugin error: #{e.message}")
|
|
2434
|
-
end
|
|
2435
|
-
|
|
2436
|
-
def run_startup_action!(action, log_prefix: "startup")
|
|
2437
|
-
case action[:type]
|
|
2438
|
-
when :ex
|
|
2439
|
-
verbose_log(2, "#{log_prefix} ex: #{action[:value]}")
|
|
2440
|
-
@dispatcher.dispatch_ex(@editor, action[:value].to_s)
|
|
2441
|
-
when :line
|
|
2442
|
-
verbose_log(2, "#{log_prefix} line: #{action[:value]}")
|
|
2443
|
-
move_cursor_to_line(action[:value].to_i)
|
|
2444
|
-
when :line_end
|
|
2445
|
-
verbose_log(2, "#{log_prefix} line_end")
|
|
2446
|
-
move_cursor_to_line(@editor.current_buffer.line_count)
|
|
2447
|
-
end
|
|
2448
|
-
end
|
|
2449
|
-
|
|
2450
|
-
def verbose_log(level, message)
|
|
2451
|
-
return if @verbose_level.to_i < level.to_i
|
|
2452
|
-
return unless @verbose_io
|
|
2453
|
-
|
|
2454
|
-
@verbose_io.puts("[ruvim:v#{@verbose_level}] #{message}")
|
|
2455
|
-
@verbose_io.flush if @verbose_io.respond_to?(:flush)
|
|
2456
|
-
rescue StandardError
|
|
2457
|
-
nil
|
|
2458
|
-
end
|
|
2459
|
-
|
|
2460
|
-
def startup_mark(label)
|
|
2461
|
-
return unless @startup_time_path
|
|
2462
|
-
|
|
2463
|
-
@startup_timeline << [label.to_s, monotonic_now]
|
|
2464
|
-
end
|
|
2465
|
-
|
|
2466
|
-
def write_startuptime_log!
|
|
2467
|
-
return unless @startup_time_path
|
|
2468
|
-
|
|
2469
|
-
prev = @startup_time_origin
|
|
2470
|
-
lines = @startup_timeline.map do |label, t|
|
|
2471
|
-
total_ms = ((t - @startup_time_origin) * 1000.0)
|
|
2472
|
-
delta_ms = ((t - prev) * 1000.0)
|
|
2473
|
-
prev = t
|
|
2474
|
-
format("%9.3f %9.3f %s", total_ms, delta_ms, label)
|
|
2475
|
-
end
|
|
2476
|
-
File.write(@startup_time_path, lines.join("\n") + "\n")
|
|
2477
|
-
rescue StandardError => e
|
|
2478
|
-
verbose_log(1, "startuptime write error: #{e.message}")
|
|
2479
|
-
end
|
|
2480
|
-
|
|
2481
|
-
def monotonic_now
|
|
2482
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
2483
|
-
rescue StandardError
|
|
2484
|
-
Time.now.to_f
|
|
2485
|
-
end
|
|
2486
|
-
|
|
2487
|
-
def apply_startup_readonly!
|
|
2488
|
-
buf = @editor.current_buffer
|
|
2489
|
-
return unless buf&.file_buffer?
|
|
2490
|
-
|
|
2491
|
-
buf.readonly = true
|
|
2492
|
-
@editor.echo("readonly: #{buf.display_name}")
|
|
2493
|
-
end
|
|
2494
|
-
|
|
2495
|
-
def apply_startup_nomodifiable!
|
|
2496
|
-
buf = @editor.current_buffer
|
|
2497
|
-
return unless buf&.file_buffer?
|
|
2498
|
-
|
|
2499
|
-
buf.modifiable = false
|
|
2500
|
-
buf.readonly = true
|
|
2501
|
-
@editor.echo("nomodifiable: #{buf.display_name}")
|
|
2502
|
-
end
|
|
2503
|
-
|
|
2504
|
-
def apply_startup_compat_mode_messages!
|
|
2505
|
-
if @startup_diff_mode
|
|
2506
|
-
verbose_log(1, "startup: -d requested (diff mode placeholder)")
|
|
2507
|
-
@editor.echo("diff mode (-d) is not implemented yet")
|
|
2508
|
-
end
|
|
2509
|
-
|
|
2510
|
-
if @startup_quickfix_errorfile
|
|
2511
|
-
verbose_log(1, "startup: -q #{@startup_quickfix_errorfile} requested (quickfix placeholder)")
|
|
2512
|
-
@editor.echo("quickfix startup (-q #{@startup_quickfix_errorfile}) is not implemented yet")
|
|
2513
|
-
end
|
|
2514
|
-
|
|
2515
|
-
if @startup_session_file
|
|
2516
|
-
verbose_log(1, "startup: -S #{@startup_session_file} requested (session placeholder)")
|
|
2517
|
-
@editor.echo("session startup (-S #{@startup_session_file}) is not implemented yet")
|
|
2518
|
-
end
|
|
2519
|
-
end
|
|
2520
|
-
|
|
2521
|
-
def open_startup_paths!(paths)
|
|
2522
|
-
list = Array(paths).compact
|
|
2523
|
-
return if list.empty?
|
|
2524
|
-
|
|
2525
|
-
# Remove the bootstrap empty buffer and reset the ID counter
|
|
2526
|
-
# so the first file gets buffer id 1 (Vim-like behavior).
|
|
2527
|
-
evict_bootstrap_buffer!
|
|
2528
|
-
|
|
2529
|
-
# Initialize arglist with all paths
|
|
2530
|
-
@editor.set_arglist(list)
|
|
2531
|
-
|
|
2532
|
-
first, *rest = list
|
|
2533
|
-
@editor.open_path(first)
|
|
2534
|
-
apply_startup_readonly! if @startup_readonly
|
|
2535
|
-
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
2536
|
-
|
|
2537
|
-
case @startup_open_layout
|
|
2538
|
-
when :horizontal
|
|
2539
|
-
first_win_id = @editor.current_window_id
|
|
2540
|
-
rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
|
|
2541
|
-
@editor.focus_window(first_win_id)
|
|
2542
|
-
when :vertical
|
|
2543
|
-
first_win_id = @editor.current_window_id
|
|
2544
|
-
rest.each { |p| open_path_in_split!(p, layout: :vertical) }
|
|
2545
|
-
@editor.focus_window(first_win_id)
|
|
2546
|
-
when :tab
|
|
2547
|
-
rest.each { |p| open_path_in_tab!(p) }
|
|
2548
|
-
@editor.tabnext(-(@editor.tabpage_count - 1))
|
|
2549
|
-
else
|
|
2550
|
-
# Load remaining files as buffers (Vim-like behavior).
|
|
2551
|
-
rest.each { |p| @editor.add_buffer_from_file(p) }
|
|
2552
|
-
end
|
|
2553
|
-
end
|
|
2554
|
-
|
|
2555
|
-
# Remove the bootstrap empty buffer before opening real files,
|
|
2556
|
-
# resetting the buffer ID counter so the first file gets id 1.
|
|
2557
|
-
def evict_bootstrap_buffer!
|
|
2558
|
-
bid = @editor.buffer_ids.find do |id|
|
|
2559
|
-
b = @editor.buffers[id]
|
|
2560
|
-
b.path.nil? && !b.modified? && b.line_count <= 1 && b.kind == :file
|
|
2561
|
-
end
|
|
2562
|
-
return unless bid
|
|
2563
|
-
|
|
2564
|
-
@editor.buffers.delete(bid)
|
|
2565
|
-
@editor.instance_variable_set(:@next_buffer_id, 1)
|
|
2566
|
-
end
|
|
2567
|
-
|
|
2568
|
-
def open_path_in_split!(path, layout:)
|
|
2569
|
-
@editor.split_current_window(layout:)
|
|
2570
|
-
@editor.open_path(path)
|
|
2571
|
-
apply_startup_readonly! if @startup_readonly
|
|
2572
|
-
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
653
|
+
def open_path_in_split!(path, layout:)
|
|
654
|
+
@editor.split_current_window(layout:)
|
|
655
|
+
@editor.open_path(path)
|
|
656
|
+
apply_startup_buffer_flags!
|
|
2573
657
|
end
|
|
2574
658
|
|
|
2575
659
|
def open_path_in_tab!(path)
|
|
2576
660
|
@editor.tabnew(path:)
|
|
2577
|
-
|
|
2578
|
-
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
2579
|
-
end
|
|
2580
|
-
|
|
2581
|
-
def open_path_with_large_file_support(path)
|
|
2582
|
-
return @editor.open_path_sync(path) unless should_open_path_async?(path)
|
|
2583
|
-
return @editor.open_path_sync(path) unless can_start_async_file_load?
|
|
2584
|
-
|
|
2585
|
-
open_path_asynchronously!(path)
|
|
2586
|
-
end
|
|
2587
|
-
|
|
2588
|
-
def should_open_path_async?(path)
|
|
2589
|
-
p = path.to_s
|
|
2590
|
-
return false if p.empty?
|
|
2591
|
-
return false unless File.file?(p)
|
|
2592
|
-
|
|
2593
|
-
File.size(p) >= large_file_async_threshold_bytes
|
|
2594
|
-
rescue StandardError
|
|
2595
|
-
false
|
|
2596
|
-
end
|
|
2597
|
-
|
|
2598
|
-
def can_start_async_file_load?
|
|
2599
|
-
@async_file_loads.empty?
|
|
2600
|
-
end
|
|
2601
|
-
|
|
2602
|
-
def large_file_async_threshold_bytes
|
|
2603
|
-
raw = ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"]
|
|
2604
|
-
n = raw.to_i if raw
|
|
2605
|
-
return n if n && n.positive?
|
|
2606
|
-
|
|
2607
|
-
LARGE_FILE_ASYNC_THRESHOLD_BYTES
|
|
2608
|
-
end
|
|
2609
|
-
|
|
2610
|
-
def open_path_asynchronously!(path)
|
|
2611
|
-
file_size = File.size(path)
|
|
2612
|
-
buf = @editor.add_empty_buffer(path: path)
|
|
2613
|
-
@editor.switch_to_buffer(buf.id)
|
|
2614
|
-
buf.loading_state = :live
|
|
2615
|
-
buf.modified = false
|
|
2616
|
-
|
|
2617
|
-
ensure_stream_event_queue!
|
|
2618
|
-
io = File.open(path, "rb")
|
|
2619
|
-
state = { path: path, io: io, thread: nil, ended_with_newline: false }
|
|
2620
|
-
staged_prefix_bytes = async_file_staged_prefix_bytes
|
|
2621
|
-
staged_mode = file_size > staged_prefix_bytes
|
|
2622
|
-
if staged_mode
|
|
2623
|
-
prefix = io.read(staged_prefix_bytes) || "".b
|
|
2624
|
-
unless prefix.empty?
|
|
2625
|
-
buf.append_stream_text!(Buffer.decode_text(prefix))
|
|
2626
|
-
state[:ended_with_newline] = prefix.end_with?("\n")
|
|
2627
|
-
end
|
|
2628
|
-
end
|
|
2629
|
-
|
|
2630
|
-
if io.eof?
|
|
2631
|
-
buf.finalize_async_file_load!(ended_with_newline: state[:ended_with_newline])
|
|
2632
|
-
buf.loading_state = :closed
|
|
2633
|
-
io.close unless io.closed?
|
|
2634
|
-
return buf
|
|
2635
|
-
end
|
|
2636
|
-
|
|
2637
|
-
@async_file_loads[buf.id] = state
|
|
2638
|
-
state[:thread] = start_async_file_loader_thread(buf.id, io, bulk_once: staged_mode)
|
|
2639
|
-
|
|
2640
|
-
size_mb = file_size.fdiv(1024 * 1024)
|
|
2641
|
-
if staged_mode
|
|
2642
|
-
@editor.echo(format("\"%s\" loading... (showing first %.0fMB of %.1fMB)", path, staged_prefix_bytes.fdiv(1024 * 1024), size_mb))
|
|
2643
|
-
else
|
|
2644
|
-
@editor.echo(format("\"%s\" loading... (%.1fMB)", path, size_mb))
|
|
2645
|
-
end
|
|
2646
|
-
buf
|
|
2647
|
-
rescue StandardError
|
|
2648
|
-
@async_file_loads.delete(buf.id) if buf
|
|
2649
|
-
raise
|
|
2650
|
-
end
|
|
2651
|
-
|
|
2652
|
-
def async_file_staged_prefix_bytes
|
|
2653
|
-
raw = ENV["RUVIM_ASYNC_FILE_PREFIX_BYTES"]
|
|
2654
|
-
n = raw.to_i if raw
|
|
2655
|
-
return n if n && n.positive?
|
|
2656
|
-
|
|
2657
|
-
LARGE_FILE_STAGED_PREFIX_BYTES
|
|
2658
|
-
end
|
|
2659
|
-
|
|
2660
|
-
def start_async_file_loader_thread(buffer_id, io, bulk_once: false)
|
|
2661
|
-
Thread.new do
|
|
2662
|
-
if bulk_once
|
|
2663
|
-
rest = io.read || "".b
|
|
2664
|
-
unless rest.empty?
|
|
2665
|
-
@stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: Buffer.decode_text(rest) }
|
|
2666
|
-
notify_signal_wakeup
|
|
2667
|
-
end
|
|
2668
|
-
@stream_event_queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: rest.end_with?("\n") }
|
|
2669
|
-
notify_signal_wakeup
|
|
2670
|
-
next
|
|
2671
|
-
end
|
|
2672
|
-
|
|
2673
|
-
ended_with_newline = false
|
|
2674
|
-
pending_text = +""
|
|
2675
|
-
loop do
|
|
2676
|
-
chunk = io.readpartial(ASYNC_FILE_READ_CHUNK_BYTES)
|
|
2677
|
-
next if chunk.nil? || chunk.empty?
|
|
2678
|
-
|
|
2679
|
-
ended_with_newline = chunk.end_with?("\n")
|
|
2680
|
-
pending_text << Buffer.decode_text(chunk)
|
|
2681
|
-
next if pending_text.bytesize < ASYNC_FILE_EVENT_FLUSH_BYTES
|
|
2682
|
-
|
|
2683
|
-
@stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: pending_text }
|
|
2684
|
-
pending_text = +""
|
|
2685
|
-
notify_signal_wakeup
|
|
2686
|
-
end
|
|
2687
|
-
rescue EOFError
|
|
2688
|
-
unless pending_text.empty?
|
|
2689
|
-
@stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: pending_text }
|
|
2690
|
-
notify_signal_wakeup
|
|
2691
|
-
end
|
|
2692
|
-
@stream_event_queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: ended_with_newline }
|
|
2693
|
-
notify_signal_wakeup
|
|
2694
|
-
rescue StandardError => e
|
|
2695
|
-
@stream_event_queue << { type: :file_error, buffer_id: buffer_id, error: e.message.to_s }
|
|
2696
|
-
notify_signal_wakeup
|
|
2697
|
-
ensure
|
|
2698
|
-
begin
|
|
2699
|
-
io.close unless io.closed?
|
|
2700
|
-
rescue StandardError
|
|
2701
|
-
nil
|
|
2702
|
-
end
|
|
2703
|
-
end
|
|
2704
|
-
end
|
|
2705
|
-
|
|
2706
|
-
def prepare_stdin_stream_buffer!
|
|
2707
|
-
buf = @editor.current_buffer
|
|
2708
|
-
if buf.intro_buffer?
|
|
2709
|
-
@editor.materialize_intro_buffer!
|
|
2710
|
-
buf = @editor.current_buffer
|
|
2711
|
-
end
|
|
2712
|
-
|
|
2713
|
-
buf.replace_all_lines!([""])
|
|
2714
|
-
buf.configure_special!(kind: :stream, name: "[stdin]", readonly: true, modifiable: false)
|
|
2715
|
-
buf.modified = false
|
|
2716
|
-
buf.stream_state = :live
|
|
2717
|
-
buf.options["filetype"] = "text"
|
|
2718
|
-
@stream_stop_requested = false
|
|
2719
|
-
ensure_stream_event_queue!
|
|
2720
|
-
@stream_buffer_id = buf.id
|
|
2721
|
-
move_window_to_stream_end!(@editor.current_window, buf)
|
|
2722
|
-
@editor.echo("[stdin] follow")
|
|
2723
|
-
end
|
|
2724
|
-
|
|
2725
|
-
def stdin_stream_stop_command
|
|
2726
|
-
return if stop_stdin_stream!
|
|
2727
|
-
|
|
2728
|
-
handle_normal_ctrl_c
|
|
2729
|
-
end
|
|
2730
|
-
|
|
2731
|
-
def stop_stdin_stream!
|
|
2732
|
-
buf = @editor.buffers[@stream_buffer_id]
|
|
2733
|
-
return false unless buf&.kind == :stream
|
|
2734
|
-
return false unless (buf.stream_state || :live) == :live
|
|
2735
|
-
|
|
2736
|
-
@stream_stop_requested = true
|
|
2737
|
-
io = @stdin_stream_source
|
|
2738
|
-
@stdin_stream_source = nil
|
|
2739
|
-
begin
|
|
2740
|
-
io.close if io && io.respond_to?(:close) && !(io.respond_to?(:closed?) && io.closed?)
|
|
2741
|
-
rescue StandardError
|
|
2742
|
-
nil
|
|
2743
|
-
end
|
|
2744
|
-
if @stream_reader_thread&.alive?
|
|
2745
|
-
@stream_reader_thread.kill
|
|
2746
|
-
@stream_reader_thread.join(0.05)
|
|
2747
|
-
end
|
|
2748
|
-
@stream_reader_thread = nil
|
|
2749
|
-
|
|
2750
|
-
buf.stream_state = :closed
|
|
2751
|
-
@editor.echo("[stdin] closed")
|
|
2752
|
-
notify_signal_wakeup
|
|
2753
|
-
true
|
|
2754
|
-
end
|
|
2755
|
-
|
|
2756
|
-
def start_stdin_stream_reader!
|
|
2757
|
-
return unless @stdin_stream_source
|
|
2758
|
-
ensure_stream_event_queue!
|
|
2759
|
-
return if @stream_reader_thread&.alive?
|
|
2760
|
-
|
|
2761
|
-
@stream_stop_requested = false
|
|
2762
|
-
io = @stdin_stream_source
|
|
2763
|
-
@stream_reader_thread = Thread.new do
|
|
2764
|
-
loop do
|
|
2765
|
-
chunk = io.readpartial(4096)
|
|
2766
|
-
next if chunk.nil? || chunk.empty?
|
|
2767
|
-
|
|
2768
|
-
@stream_event_queue << { type: :data, data: Buffer.decode_text(chunk) }
|
|
2769
|
-
notify_signal_wakeup
|
|
2770
|
-
end
|
|
2771
|
-
rescue EOFError
|
|
2772
|
-
unless @stream_stop_requested
|
|
2773
|
-
@stream_event_queue << { type: :eof }
|
|
2774
|
-
notify_signal_wakeup
|
|
2775
|
-
end
|
|
2776
|
-
rescue IOError => e
|
|
2777
|
-
unless @stream_stop_requested
|
|
2778
|
-
@stream_event_queue << { type: :error, error: e.message.to_s }
|
|
2779
|
-
notify_signal_wakeup
|
|
2780
|
-
end
|
|
2781
|
-
rescue StandardError => e
|
|
2782
|
-
unless @stream_stop_requested
|
|
2783
|
-
@stream_event_queue << { type: :error, error: e.message.to_s }
|
|
2784
|
-
notify_signal_wakeup
|
|
2785
|
-
end
|
|
2786
|
-
end
|
|
2787
|
-
end
|
|
2788
|
-
|
|
2789
|
-
def drain_stream_events!
|
|
2790
|
-
return false unless @stream_event_queue
|
|
2791
|
-
|
|
2792
|
-
changed = false
|
|
2793
|
-
loop do
|
|
2794
|
-
event = @stream_event_queue.pop(true)
|
|
2795
|
-
case event[:type]
|
|
2796
|
-
when :data
|
|
2797
|
-
changed = apply_stream_chunk!(event[:data]) || changed
|
|
2798
|
-
when :eof
|
|
2799
|
-
if (buf = @editor.buffers[@stream_buffer_id])
|
|
2800
|
-
buf.stream_state = :closed
|
|
2801
|
-
end
|
|
2802
|
-
@editor.echo("[stdin] EOF")
|
|
2803
|
-
changed = true
|
|
2804
|
-
when :error
|
|
2805
|
-
next if ignore_stream_shutdown_error?(event[:error])
|
|
2806
|
-
if (buf = @editor.buffers[@stream_buffer_id])
|
|
2807
|
-
buf.stream_state = :error
|
|
2808
|
-
end
|
|
2809
|
-
@editor.echo_error("[stdin] stream error: #{event[:error]}")
|
|
2810
|
-
changed = true
|
|
2811
|
-
when :file_data
|
|
2812
|
-
changed = apply_async_file_chunk!(event[:buffer_id], event[:data]) || changed
|
|
2813
|
-
when :file_eof
|
|
2814
|
-
changed = finish_async_file_load!(event[:buffer_id], ended_with_newline: event[:ended_with_newline]) || changed
|
|
2815
|
-
when :file_error
|
|
2816
|
-
changed = fail_async_file_load!(event[:buffer_id], event[:error]) || changed
|
|
2817
|
-
end
|
|
2818
|
-
end
|
|
2819
|
-
rescue ThreadError
|
|
2820
|
-
changed
|
|
2821
|
-
end
|
|
2822
|
-
|
|
2823
|
-
def apply_stream_chunk!(text)
|
|
2824
|
-
return false if text.to_s.empty?
|
|
2825
|
-
|
|
2826
|
-
buf = @editor.buffers[@stream_buffer_id]
|
|
2827
|
-
return false unless buf
|
|
2828
|
-
|
|
2829
|
-
follow_window_ids = @editor.windows.values.filter_map do |win|
|
|
2830
|
-
next unless win.buffer_id == buf.id
|
|
2831
|
-
next unless stream_window_following_end?(win, buf)
|
|
2832
|
-
|
|
2833
|
-
win.id
|
|
2834
|
-
end
|
|
2835
|
-
|
|
2836
|
-
buf.append_stream_text!(text)
|
|
2837
|
-
|
|
2838
|
-
follow_window_ids.each do |win_id|
|
|
2839
|
-
win = @editor.windows[win_id]
|
|
2840
|
-
move_window_to_stream_end!(win, buf) if win
|
|
2841
|
-
end
|
|
2842
|
-
|
|
2843
|
-
true
|
|
2844
|
-
end
|
|
2845
|
-
|
|
2846
|
-
def apply_async_file_chunk!(buffer_id, text)
|
|
2847
|
-
return false if text.to_s.empty?
|
|
2848
|
-
|
|
2849
|
-
buf = @editor.buffers[buffer_id]
|
|
2850
|
-
return false unless buf
|
|
2851
|
-
|
|
2852
|
-
buf.append_stream_text!(text)
|
|
2853
|
-
true
|
|
2854
|
-
end
|
|
2855
|
-
|
|
2856
|
-
def finish_async_file_load!(buffer_id, ended_with_newline:)
|
|
2857
|
-
@async_file_loads.delete(buffer_id)
|
|
2858
|
-
buf = @editor.buffers[buffer_id]
|
|
2859
|
-
return false unless buf
|
|
2860
|
-
|
|
2861
|
-
buf.finalize_async_file_load!(ended_with_newline: !!ended_with_newline)
|
|
2862
|
-
buf.loading_state = :closed
|
|
2863
|
-
true
|
|
2864
|
-
end
|
|
2865
|
-
|
|
2866
|
-
def fail_async_file_load!(buffer_id, error)
|
|
2867
|
-
state = @async_file_loads.delete(buffer_id)
|
|
2868
|
-
buf = @editor.buffers[buffer_id]
|
|
2869
|
-
if buf
|
|
2870
|
-
buf.loading_state = :error
|
|
2871
|
-
end
|
|
2872
|
-
@editor.echo_error("\"#{(state && state[:path]) || (buf && buf.display_name) || buffer_id}\" load error: #{error}")
|
|
2873
|
-
true
|
|
2874
|
-
end
|
|
2875
|
-
|
|
2876
|
-
def stream_window_following_end?(win, buf)
|
|
2877
|
-
return false unless win
|
|
2878
|
-
|
|
2879
|
-
last_row = buf.line_count - 1
|
|
2880
|
-
win.cursor_y >= last_row
|
|
2881
|
-
end
|
|
2882
|
-
|
|
2883
|
-
def move_window_to_stream_end!(win, buf)
|
|
2884
|
-
return unless win && buf
|
|
2885
|
-
|
|
2886
|
-
last_row = buf.line_count - 1
|
|
2887
|
-
win.cursor_y = last_row
|
|
2888
|
-
win.cursor_x = buf.line_length(last_row)
|
|
2889
|
-
win.clamp_to_buffer(buf)
|
|
2890
|
-
end
|
|
2891
|
-
|
|
2892
|
-
def shutdown_stream_reader!
|
|
2893
|
-
thread = @stream_reader_thread
|
|
2894
|
-
@stream_reader_thread = nil
|
|
2895
|
-
@stream_stop_requested = true
|
|
2896
|
-
return unless thread
|
|
2897
|
-
return unless thread.alive?
|
|
2898
|
-
|
|
2899
|
-
thread.kill
|
|
2900
|
-
thread.join(0.05)
|
|
2901
|
-
rescue StandardError
|
|
2902
|
-
nil
|
|
2903
|
-
end
|
|
2904
|
-
|
|
2905
|
-
def shutdown_async_file_loaders!
|
|
2906
|
-
loaders = @async_file_loads
|
|
2907
|
-
@async_file_loads = {}
|
|
2908
|
-
loaders.each_value do |state|
|
|
2909
|
-
io = state[:io]
|
|
2910
|
-
thread = state[:thread]
|
|
2911
|
-
begin
|
|
2912
|
-
io.close if io && !io.closed?
|
|
2913
|
-
rescue StandardError
|
|
2914
|
-
nil
|
|
2915
|
-
end
|
|
2916
|
-
next unless thread&.alive?
|
|
2917
|
-
|
|
2918
|
-
thread.kill
|
|
2919
|
-
thread.join(0.05)
|
|
2920
|
-
rescue StandardError
|
|
2921
|
-
nil
|
|
2922
|
-
end
|
|
2923
|
-
end
|
|
2924
|
-
|
|
2925
|
-
def shutdown_background_readers!
|
|
2926
|
-
shutdown_stream_reader!
|
|
2927
|
-
shutdown_async_file_loaders!
|
|
2928
|
-
end
|
|
2929
|
-
|
|
2930
|
-
def ignore_stream_shutdown_error?(message)
|
|
2931
|
-
buf = @editor.buffers[@stream_buffer_id]
|
|
2932
|
-
return false unless buf&.kind == :stream
|
|
2933
|
-
return false unless (buf.stream_state || :live) == :closed
|
|
2934
|
-
|
|
2935
|
-
msg = message.to_s.downcase
|
|
2936
|
-
msg.include?("stream closed") || msg.include?("closed in another thread")
|
|
2937
|
-
end
|
|
2938
|
-
|
|
2939
|
-
def ensure_stream_event_queue!
|
|
2940
|
-
@stream_event_queue ||= Queue.new
|
|
661
|
+
apply_startup_buffer_flags!
|
|
2941
662
|
end
|
|
2942
663
|
|
|
2943
664
|
def move_cursor_to_line(line_number)
|
|
@@ -2945,7 +666,7 @@ module RuVim
|
|
|
2945
666
|
buf = @editor.current_buffer
|
|
2946
667
|
return unless win && buf
|
|
2947
668
|
|
|
2948
|
-
target = [[line_number
|
|
669
|
+
target = [[line_number - 1, 0].max, buf.line_count - 1].min
|
|
2949
670
|
win.cursor_y = target
|
|
2950
671
|
win.clamp_to_buffer(buf)
|
|
2951
672
|
end
|