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