ruvim 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +109 -0
- data/lib/ruvim/screen.rb +503 -109
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +165 -0
- data/test/indent_test.rb +287 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -2
data/lib/ruvim/app.rb
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "file_watcher"
|
|
6
|
+
|
|
1
7
|
module RuVim
|
|
2
8
|
class App
|
|
3
|
-
|
|
9
|
+
LARGE_FILE_ASYNC_THRESHOLD_BYTES = 64 * 1024 * 1024
|
|
10
|
+
LARGE_FILE_STAGED_PREFIX_BYTES = 8 * 1024 * 1024
|
|
11
|
+
ASYNC_FILE_READ_CHUNK_BYTES = 1 * 1024 * 1024
|
|
12
|
+
ASYNC_FILE_EVENT_FLUSH_BYTES = 4 * 1024 * 1024
|
|
13
|
+
|
|
14
|
+
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
|
+
startup_paths = Array(paths || path).compact
|
|
16
|
+
@ui_stdin = ui_stdin || stdin
|
|
17
|
+
@stdin_stream_mode = !!stdin_stream_mode
|
|
18
|
+
@stdin_stream_source = @stdin_stream_mode ? stdin : nil
|
|
4
19
|
@editor = Editor.new
|
|
5
|
-
@terminal = Terminal.new(stdin
|
|
6
|
-
@input = Input.new(
|
|
20
|
+
@terminal = Terminal.new(stdin: @ui_stdin, stdout:)
|
|
21
|
+
@input = Input.new(@ui_stdin)
|
|
7
22
|
@screen = Screen.new(terminal: @terminal)
|
|
8
23
|
@dispatcher = Dispatcher.new
|
|
9
24
|
@keymaps = KeymapManager.new
|
|
10
25
|
@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 = {}
|
|
11
32
|
@cmdline_history = Hash.new { |h, k| h[k] = [] }
|
|
12
33
|
@cmdline_history_index = nil
|
|
13
34
|
@cmdline_completion = nil
|
|
@@ -24,6 +45,7 @@ module RuVim
|
|
|
24
45
|
@startup_quickfix_errorfile = quickfix_errorfile
|
|
25
46
|
@startup_session_file = session_file
|
|
26
47
|
@startup_nomodifiable = nomodifiable
|
|
48
|
+
@startup_follow = follow
|
|
27
49
|
@restricted_mode = restricted
|
|
28
50
|
@verbose_level = verbose_level.to_i
|
|
29
51
|
@verbose_io = verbose_io
|
|
@@ -33,6 +55,13 @@ module RuVim
|
|
|
33
55
|
@startup_open_layout = startup_open_layout
|
|
34
56
|
@startup_open_count = startup_open_count
|
|
35
57
|
@editor.restricted_mode = @restricted_mode
|
|
58
|
+
@editor.stdin_stream_stop_handler = method(:stdin_stream_stop_command)
|
|
59
|
+
@editor.open_path_handler = method(:open_path_with_large_file_support)
|
|
60
|
+
@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
|
+
load_command_line_history!
|
|
36
65
|
|
|
37
66
|
startup_mark("init.start")
|
|
38
67
|
register_builtins!
|
|
@@ -48,8 +77,10 @@ module RuVim
|
|
|
48
77
|
install_signal_handlers
|
|
49
78
|
startup_mark("signals.installed")
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
if @stdin_stream_mode && startup_paths.empty?
|
|
81
|
+
verbose_log(1, "startup: stdin stream buffer")
|
|
82
|
+
prepare_stdin_stream_buffer!
|
|
83
|
+
elsif startup_paths.empty?
|
|
53
84
|
verbose_log(1, "startup: intro")
|
|
54
85
|
@editor.show_intro_buffer_if_applicable!
|
|
55
86
|
else
|
|
@@ -64,12 +95,14 @@ module RuVim
|
|
|
64
95
|
verbose_log(1, "startup: run_startup_actions count=#{Array(startup_actions).length}")
|
|
65
96
|
run_startup_actions!(startup_actions)
|
|
66
97
|
startup_mark("startup_actions.done")
|
|
98
|
+
start_stdin_stream_reader! if @stream_buffer_id
|
|
67
99
|
write_startuptime_log!
|
|
68
100
|
end
|
|
69
101
|
|
|
70
102
|
def run
|
|
71
103
|
@terminal.with_ui do
|
|
72
104
|
loop do
|
|
105
|
+
@needs_redraw = true if drain_stream_events!
|
|
73
106
|
if @needs_redraw
|
|
74
107
|
@screen.render(@editor)
|
|
75
108
|
@needs_redraw = false
|
|
@@ -89,8 +122,25 @@ module RuVim
|
|
|
89
122
|
|
|
90
123
|
handle_key(key)
|
|
91
124
|
@needs_redraw = true
|
|
125
|
+
|
|
126
|
+
# Batch insert-mode keystrokes to avoid per-char rendering during paste
|
|
127
|
+
if @editor.mode == :insert && @input.has_pending_input?
|
|
128
|
+
@paste_batch = true
|
|
129
|
+
begin
|
|
130
|
+
while @editor.mode == :insert && @input.has_pending_input?
|
|
131
|
+
batch_key = @input.read_key(timeout: 0, esc_timeout: 0)
|
|
132
|
+
break unless batch_key
|
|
133
|
+
handle_key(batch_key)
|
|
134
|
+
end
|
|
135
|
+
ensure
|
|
136
|
+
@paste_batch = false
|
|
137
|
+
end
|
|
138
|
+
end
|
|
92
139
|
end
|
|
93
140
|
end
|
|
141
|
+
ensure
|
|
142
|
+
shutdown_background_readers!
|
|
143
|
+
save_command_line_history!
|
|
94
144
|
end
|
|
95
145
|
|
|
96
146
|
def run_startup_actions!(actions, log_prefix: "startup")
|
|
@@ -173,6 +223,9 @@ module RuVim
|
|
|
173
223
|
register_internal_unless(cmd, "cursor.page_down.half", call: :cursor_page_down_half, desc: "Move half page down")
|
|
174
224
|
register_internal_unless(cmd, "window.scroll_up.line", call: :window_scroll_up_line, desc: "Scroll window up one line")
|
|
175
225
|
register_internal_unless(cmd, "window.scroll_down.line", call: :window_scroll_down_line, desc: "Scroll window down one line")
|
|
226
|
+
register_internal_unless(cmd, "window.cursor_line_top", call: :window_cursor_line_top, desc: "Put cursor line at top")
|
|
227
|
+
register_internal_unless(cmd, "window.cursor_line_center", call: :window_cursor_line_center, desc: "Put cursor line at center")
|
|
228
|
+
register_internal_unless(cmd, "window.cursor_line_bottom", call: :window_cursor_line_bottom, desc: "Put cursor line at bottom")
|
|
176
229
|
register_internal_unless(cmd, "cursor.line_start", call: :cursor_line_start, desc: "Move to column 1")
|
|
177
230
|
register_internal_unless(cmd, "cursor.line_end", call: :cursor_line_end, desc: "Move to end of line")
|
|
178
231
|
register_internal_unless(cmd, "cursor.first_nonblank", call: :cursor_first_nonblank, desc: "Move to first nonblank")
|
|
@@ -198,10 +251,17 @@ module RuVim
|
|
|
198
251
|
register_internal_unless(cmd, "window.focus_right", call: :window_focus_right, desc: "Focus right window")
|
|
199
252
|
register_internal_unless(cmd, "window.focus_up", call: :window_focus_up, desc: "Focus upper window")
|
|
200
253
|
register_internal_unless(cmd, "window.focus_down", call: :window_focus_down, desc: "Focus lower window")
|
|
254
|
+
register_internal_unless(cmd, "window.focus_or_split_left", call: :window_focus_or_split_left, desc: "Focus left window or split")
|
|
255
|
+
register_internal_unless(cmd, "window.focus_or_split_right", call: :window_focus_or_split_right, desc: "Focus right window or split")
|
|
256
|
+
register_internal_unless(cmd, "window.focus_or_split_up", call: :window_focus_or_split_up, desc: "Focus upper window or split")
|
|
257
|
+
register_internal_unless(cmd, "window.focus_or_split_down", call: :window_focus_or_split_down, desc: "Focus lower window or split")
|
|
201
258
|
register_internal_unless(cmd, "mode.command_line", call: :enter_command_line_mode, desc: "Enter command-line mode")
|
|
202
259
|
register_internal_unless(cmd, "mode.search_forward", call: :enter_search_forward_mode, desc: "Enter / search")
|
|
203
260
|
register_internal_unless(cmd, "mode.search_backward", call: :enter_search_backward_mode, desc: "Enter ? search")
|
|
204
261
|
register_internal_unless(cmd, "buffer.delete_char", call: :delete_char, desc: "Delete char under cursor")
|
|
262
|
+
register_internal_unless(cmd, "buffer.substitute_char", call: :substitute_char, desc: "Substitute char(s)")
|
|
263
|
+
register_internal_unless(cmd, "buffer.swapcase_char", call: :swapcase_char, desc: "Swap case under cursor")
|
|
264
|
+
register_internal_unless(cmd, "buffer.join_lines", call: :join_lines, desc: "Join lines")
|
|
205
265
|
register_internal_unless(cmd, "buffer.delete_line", call: :delete_line, desc: "Delete current line")
|
|
206
266
|
register_internal_unless(cmd, "buffer.delete_motion", call: :delete_motion, desc: "Delete by motion")
|
|
207
267
|
register_internal_unless(cmd, "buffer.change_motion", call: :change_motion, desc: "Change by motion")
|
|
@@ -231,10 +291,39 @@ module RuVim
|
|
|
231
291
|
register_internal_unless(cmd, "buffer.replace_char", call: :replace_char, desc: "Replace single char")
|
|
232
292
|
register_internal_unless(cmd, "file.goto_under_cursor", call: :file_goto_under_cursor, desc: "Open file under cursor")
|
|
233
293
|
register_internal_unless(cmd, "ui.clear_message", call: :clear_message, desc: "Clear message")
|
|
294
|
+
register_internal_unless(cmd, "normal.register_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_register_pending_start) }, desc: "Select register for next operation")
|
|
295
|
+
register_internal_unless(cmd, "normal.operator_delete_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :delete) }, desc: "Start delete operator")
|
|
296
|
+
register_internal_unless(cmd, "normal.operator_yank_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :yank) }, desc: "Start yank operator")
|
|
297
|
+
register_internal_unless(cmd, "normal.operator_change_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :change) }, desc: "Start change operator")
|
|
298
|
+
register_internal_unless(cmd, "normal.operator_indent_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :indent) }, desc: "Start indent operator")
|
|
299
|
+
register_internal_unless(cmd, "buffer.indent_lines", call: :indent_lines, desc: "Auto-indent lines")
|
|
300
|
+
register_internal_unless(cmd, "buffer.indent_motion", call: :indent_motion, desc: "Auto-indent motion range")
|
|
301
|
+
register_internal_unless(cmd, "buffer.visual_indent", call: :visual_indent, desc: "Auto-indent visual selection")
|
|
302
|
+
register_internal_unless(cmd, "normal.replace_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_replace_pending_start) }, desc: "Start replace-char pending")
|
|
303
|
+
register_internal_unless(cmd, "normal.find_char_forward_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_find_pending_start, token: "f") }, desc: "Start char find forward")
|
|
304
|
+
register_internal_unless(cmd, "normal.find_char_backward_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_find_pending_start, token: "F") }, desc: "Start char find backward")
|
|
305
|
+
register_internal_unless(cmd, "normal.find_till_forward_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_find_pending_start, token: "t") }, desc: "Start till-char find forward")
|
|
306
|
+
register_internal_unless(cmd, "normal.find_till_backward_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_find_pending_start, token: "T") }, desc: "Start till-char find backward")
|
|
307
|
+
register_internal_unless(cmd, "normal.find_repeat", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_find_repeat, reverse: false) }, desc: "Repeat last f/t/F/T")
|
|
308
|
+
register_internal_unless(cmd, "normal.find_repeat_reverse", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_find_repeat, reverse: true) }, desc: "Repeat last f/t/F/T in reverse")
|
|
309
|
+
register_internal_unless(cmd, "normal.change_repeat", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_change_repeat) }, desc: "Repeat last change")
|
|
310
|
+
register_internal_unless(cmd, "normal.macro_record_toggle", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_macro_record_toggle) }, desc: "Start/stop macro recording")
|
|
311
|
+
register_internal_unless(cmd, "normal.macro_play_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_macro_play_pending_start) }, desc: "Start macro play pending")
|
|
312
|
+
register_internal_unless(cmd, "normal.mark_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_mark_pending_start) }, desc: "Start mark set pending")
|
|
313
|
+
register_internal_unless(cmd, "normal.jump_mark_linewise_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_jump_pending_start, linewise: true, repeat_token: "'") }, desc: "Start linewise mark jump pending")
|
|
314
|
+
register_internal_unless(cmd, "normal.jump_mark_exact_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_jump_pending_start, linewise: false, repeat_token: "`") }, desc: "Start exact mark jump pending")
|
|
315
|
+
register_internal_unless(
|
|
316
|
+
cmd,
|
|
317
|
+
"stdin.stream_stop",
|
|
318
|
+
call: ->(ctx, **) { ctx.editor.stdin_stream_stop_or_cancel! },
|
|
319
|
+
desc: "Stop stdin follow stream (or cancel pending state)"
|
|
320
|
+
)
|
|
234
321
|
|
|
235
322
|
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :maybe_one, bang: true)
|
|
236
323
|
register_ex_unless(ex, "q", call: :app_quit, aliases: %w[quit], desc: "Quit", nargs: 0, bang: true)
|
|
324
|
+
register_ex_unless(ex, "qa", call: :app_quit_all, aliases: %w[qall], desc: "Quit all", nargs: 0, bang: true)
|
|
237
325
|
register_ex_unless(ex, "wq", call: :file_write_quit, desc: "Write and quit", nargs: :maybe_one, bang: true)
|
|
326
|
+
register_ex_unless(ex, "wqa", call: :file_write_quit_all, aliases: %w[wqall xa xall], desc: "Write all and quit", nargs: 0, bang: true)
|
|
238
327
|
register_ex_unless(ex, "e", call: :file_edit, aliases: %w[edit], desc: "Edit file / reload", nargs: :maybe_one, bang: true)
|
|
239
328
|
register_ex_unless(ex, "help", call: :ex_help, desc: "Show help / topics", nargs: :any)
|
|
240
329
|
register_ex_unless(ex, "command", call: :ex_define_command, desc: "Define user command", nargs: :any, bang: true)
|
|
@@ -244,7 +333,13 @@ module RuVim
|
|
|
244
333
|
register_ex_unless(ex, "bprev", call: :buffer_prev, aliases: %w[bp], desc: "Previous buffer", nargs: 0, bang: true)
|
|
245
334
|
register_ex_unless(ex, "buffer", call: :buffer_switch, aliases: %w[b], desc: "Switch buffer", nargs: 1, bang: true)
|
|
246
335
|
register_ex_unless(ex, "bdelete", call: :buffer_delete, aliases: %w[bd], desc: "Delete buffer", nargs: :maybe_one, bang: true)
|
|
336
|
+
register_ex_unless(ex, "args", call: :arglist_show, desc: "Show argument list", nargs: 0)
|
|
337
|
+
register_ex_unless(ex, "next", call: :arglist_next, desc: "Next argument", nargs: 0)
|
|
338
|
+
register_ex_unless(ex, "prev", call: :arglist_prev, desc: "Previous argument", nargs: 0)
|
|
339
|
+
register_ex_unless(ex, "first", call: :arglist_first, desc: "First argument", nargs: 0)
|
|
340
|
+
register_ex_unless(ex, "last", call: :arglist_last, desc: "Last argument", nargs: 0)
|
|
247
341
|
register_ex_unless(ex, "commands", call: :ex_commands, desc: "List Ex commands", nargs: 0)
|
|
342
|
+
register_ex_unless(ex, "bindings", call: :ex_bindings, desc: "List active key bindings", nargs: :any)
|
|
248
343
|
register_ex_unless(ex, "set", call: :ex_set, desc: "Set options", nargs: :any)
|
|
249
344
|
register_ex_unless(ex, "setlocal", call: :ex_setlocal, desc: "Set window/buffer local option", nargs: :any)
|
|
250
345
|
register_ex_unless(ex, "setglobal", call: :ex_setglobal, desc: "Set global option", nargs: :any)
|
|
@@ -253,6 +348,7 @@ module RuVim
|
|
|
253
348
|
register_ex_unless(ex, "tabnew", call: :tab_new, desc: "New tab", nargs: :maybe_one)
|
|
254
349
|
register_ex_unless(ex, "tabnext", call: :tab_next, aliases: %w[tabn], desc: "Next tab", nargs: 0)
|
|
255
350
|
register_ex_unless(ex, "tabprev", call: :tab_prev, aliases: %w[tabp], desc: "Prev tab", nargs: 0)
|
|
351
|
+
register_ex_unless(ex, "tabs", call: :tab_list, desc: "List tabs", nargs: 0)
|
|
256
352
|
register_ex_unless(ex, "vimgrep", call: :ex_vimgrep, desc: "Populate quickfix from regex (minimal)", nargs: :any)
|
|
257
353
|
register_ex_unless(ex, "lvimgrep", call: :ex_lvimgrep, desc: "Populate location list from regex (minimal)", nargs: :any)
|
|
258
354
|
register_ex_unless(ex, "copen", call: :ex_copen, desc: "Open quickfix list", nargs: 0)
|
|
@@ -263,6 +359,32 @@ module RuVim
|
|
|
263
359
|
register_ex_unless(ex, "lclose", call: :ex_lclose, desc: "Close location list window", nargs: 0)
|
|
264
360
|
register_ex_unless(ex, "lnext", call: :ex_lnext, aliases: %w[ln], desc: "Next location item", nargs: 0)
|
|
265
361
|
register_ex_unless(ex, "lprev", call: :ex_lprev, aliases: %w[lp], desc: "Prev location item", nargs: 0)
|
|
362
|
+
register_ex_unless(ex, "grep", call: :ex_grep, desc: "Search with external grep", nargs: :any)
|
|
363
|
+
register_ex_unless(ex, "lgrep", call: :ex_lgrep, desc: "Search with external grep (location list)", nargs: :any)
|
|
364
|
+
register_ex_unless(ex, "d", call: :ex_delete_lines, aliases: %w[delete], desc: "Delete lines", nargs: :any)
|
|
365
|
+
register_ex_unless(ex, "y", call: :ex_yank_lines, aliases: %w[yank], desc: "Yank lines", nargs: :any)
|
|
366
|
+
register_ex_unless(ex, "rich", call: :ex_rich, desc: "Open/close Rich View", nargs: :maybe_one)
|
|
367
|
+
register_ex_unless(ex, "follow", call: ->(ctx, **) { ctx.editor.invoke_app_action(:follow_toggle) }, desc: "Toggle file follow mode", nargs: 0)
|
|
368
|
+
register_ex_unless(ex, "nohlsearch", call: ->(ctx, **) { ctx.editor.suppress_hlsearch! }, aliases: %w[noh nohl nohlsearc nohlsear nohlsea nohlse nohls], desc: "Temporarily clear search highlight", nargs: 0)
|
|
369
|
+
register_ex_unless(ex, "filter", call: :ex_filter, desc: "Filter lines matching search pattern", nargs: :any)
|
|
370
|
+
register_internal_unless(cmd, "search.filter", call: :search_filter, desc: "Filter lines matching search pattern")
|
|
371
|
+
register_internal_unless(cmd, "rich.toggle", call: :rich_toggle, desc: "Toggle Rich View")
|
|
372
|
+
register_internal_unless(cmd, "rich.close_buffer", call: :rich_view_close_buffer, desc: "Close rich view buffer")
|
|
373
|
+
register_internal_unless(cmd, "quickfix.next", call: :ex_cnext, desc: "Next quickfix item")
|
|
374
|
+
register_internal_unless(cmd, "quickfix.prev", call: :ex_cprev, desc: "Prev quickfix item")
|
|
375
|
+
register_internal_unless(cmd, "quickfix.open", call: :ex_copen, desc: "Open quickfix list")
|
|
376
|
+
|
|
377
|
+
register_internal_unless(cmd, "git.blame", call: :git_blame, desc: "Open git blame buffer")
|
|
378
|
+
register_internal_unless(cmd, "git.blame.prev", call: :git_blame_prev, desc: "Blame at parent commit")
|
|
379
|
+
register_internal_unless(cmd, "git.blame.back", call: :git_blame_back, desc: "Restore previous blame")
|
|
380
|
+
register_internal_unless(cmd, "git.blame.commit", call: :git_blame_commit, desc: "Show commit details")
|
|
381
|
+
register_internal_unless(cmd, "git.command_mode", call: :enter_git_command_mode, desc: "Enter Git command-line mode")
|
|
382
|
+
register_internal_unless(cmd, "git.close_buffer", call: :git_close_buffer, desc: "Close git buffer")
|
|
383
|
+
register_internal_unless(cmd, "git.status.open_file", call: :git_status_open_file, desc: "Open file from git status")
|
|
384
|
+
register_internal_unless(cmd, "git.diff.open_file", call: :git_diff_open_file, desc: "Open file from git diff")
|
|
385
|
+
register_internal_unless(cmd, "git.branch.checkout", call: :git_branch_checkout, desc: "Checkout branch under cursor")
|
|
386
|
+
register_internal_unless(cmd, "git.commit.execute", call: :git_commit_execute, desc: "Execute git commit")
|
|
387
|
+
register_ex_unless(ex, "git", call: :ex_git, desc: "Git subcommand dispatcher", nargs: :any)
|
|
266
388
|
end
|
|
267
389
|
|
|
268
390
|
def bind_default_keys!
|
|
@@ -297,10 +419,40 @@ module RuVim
|
|
|
297
419
|
@keymaps.bind(:normal, ["<C-w>", "j"], "window.focus_down")
|
|
298
420
|
@keymaps.bind(:normal, ["<C-w>", "k"], "window.focus_up")
|
|
299
421
|
@keymaps.bind(:normal, ["<C-w>", "l"], "window.focus_right")
|
|
422
|
+
@keymaps.bind(:normal, ["<S-Left>"], "window.focus_or_split_left")
|
|
423
|
+
@keymaps.bind(:normal, ["<S-Right>"], "window.focus_or_split_right")
|
|
424
|
+
@keymaps.bind(:normal, ["<S-Up>"], "window.focus_or_split_up")
|
|
425
|
+
@keymaps.bind(:normal, ["<S-Down>"], "window.focus_or_split_down")
|
|
300
426
|
@keymaps.bind(:normal, ":", "mode.command_line")
|
|
301
427
|
@keymaps.bind(:normal, "/", "mode.search_forward")
|
|
302
428
|
@keymaps.bind(:normal, "?", "mode.search_backward")
|
|
303
429
|
@keymaps.bind(:normal, "x", "buffer.delete_char")
|
|
430
|
+
@keymaps.bind(:normal, "X", "buffer.delete_motion", kwargs: { motion: "h" })
|
|
431
|
+
@keymaps.bind(:normal, "s", "buffer.substitute_char")
|
|
432
|
+
@keymaps.bind(:normal, "D", "buffer.delete_motion", kwargs: { motion: "$" })
|
|
433
|
+
@keymaps.bind(:normal, "C", "buffer.change_motion", kwargs: { motion: "$" })
|
|
434
|
+
@keymaps.bind(:normal, "S", "buffer.change_line")
|
|
435
|
+
@keymaps.bind(:normal, "Y", "buffer.yank_line")
|
|
436
|
+
@keymaps.bind(:normal, "J", "buffer.join_lines")
|
|
437
|
+
@keymaps.bind(:normal, "~", "buffer.swapcase_char")
|
|
438
|
+
@keymaps.bind(:normal, "\"", "normal.register_pending_start")
|
|
439
|
+
@keymaps.bind(:normal, "d", "normal.operator_delete_start")
|
|
440
|
+
@keymaps.bind(:normal, "y", "normal.operator_yank_start")
|
|
441
|
+
@keymaps.bind(:normal, "c", "normal.operator_change_start")
|
|
442
|
+
@keymaps.bind(:normal, "=", "normal.operator_indent_start")
|
|
443
|
+
@keymaps.bind(:normal, "r", "normal.replace_pending_start")
|
|
444
|
+
@keymaps.bind(:normal, "f", "normal.find_char_forward_start")
|
|
445
|
+
@keymaps.bind(:normal, "F", "normal.find_char_backward_start")
|
|
446
|
+
@keymaps.bind(:normal, "t", "normal.find_till_forward_start")
|
|
447
|
+
@keymaps.bind(:normal, "T", "normal.find_till_backward_start")
|
|
448
|
+
@keymaps.bind(:normal, ";", "normal.find_repeat")
|
|
449
|
+
@keymaps.bind(:normal, ",", "normal.find_repeat_reverse")
|
|
450
|
+
@keymaps.bind(:normal, ".", "normal.change_repeat")
|
|
451
|
+
@keymaps.bind(:normal, "q", "normal.macro_record_toggle")
|
|
452
|
+
@keymaps.bind(:normal, "@", "normal.macro_play_pending_start")
|
|
453
|
+
@keymaps.bind(:normal, "m", "normal.mark_pending_start")
|
|
454
|
+
@keymaps.bind(:normal, "'", "normal.jump_mark_linewise_pending_start")
|
|
455
|
+
@keymaps.bind(:normal, "`", "normal.jump_mark_exact_pending_start")
|
|
304
456
|
@keymaps.bind(:normal, "p", "buffer.paste_after")
|
|
305
457
|
@keymaps.bind(:normal, "P", "buffer.paste_before")
|
|
306
458
|
@keymaps.bind(:normal, "u", "buffer.undo")
|
|
@@ -313,6 +465,10 @@ module RuVim
|
|
|
313
465
|
@keymaps.bind(:normal, ["<C-b>"], "cursor.page_up.default")
|
|
314
466
|
@keymaps.bind(:normal, ["<C-e>"], "window.scroll_down.line")
|
|
315
467
|
@keymaps.bind(:normal, ["<C-y>"], "window.scroll_up.line")
|
|
468
|
+
@keymaps.bind(:normal, "zt", "window.cursor_line_top")
|
|
469
|
+
@keymaps.bind(:normal, "zz", "window.cursor_line_center")
|
|
470
|
+
@keymaps.bind(:normal, "zb", "window.cursor_line_bottom")
|
|
471
|
+
@keymaps.bind(:normal, ["<C-c>"], "stdin.stream_stop")
|
|
316
472
|
@keymaps.bind(:normal, "n", "search.next")
|
|
317
473
|
@keymaps.bind(:normal, "N", "search.prev")
|
|
318
474
|
@keymaps.bind(:normal, "*", "search.word_forward")
|
|
@@ -320,6 +476,12 @@ module RuVim
|
|
|
320
476
|
@keymaps.bind(:normal, "g*", "search.word_forward_partial")
|
|
321
477
|
@keymaps.bind(:normal, "g#", "search.word_backward_partial")
|
|
322
478
|
@keymaps.bind(:normal, "gf", "file.goto_under_cursor")
|
|
479
|
+
@keymaps.bind(:normal, "gr", "rich.toggle")
|
|
480
|
+
@keymaps.bind(:normal, "g/", "search.filter")
|
|
481
|
+
@keymaps.bind(:normal, ["<C-g>"], "git.command_mode")
|
|
482
|
+
@keymaps.bind(:normal, "Q", "quickfix.open")
|
|
483
|
+
@keymaps.bind(:normal, ["]", "q"], "quickfix.next")
|
|
484
|
+
@keymaps.bind(:normal, ["[", "q"], "quickfix.prev")
|
|
323
485
|
@keymaps.bind(:normal, ["<PageUp>"], "cursor.page_up.default")
|
|
324
486
|
@keymaps.bind(:normal, ["<PageDown>"], "cursor.page_down.default")
|
|
325
487
|
@keymaps.bind(:normal, "\e", "ui.clear_message")
|
|
@@ -330,7 +492,12 @@ module RuVim
|
|
|
330
492
|
clear_stale_message_before_key(key)
|
|
331
493
|
@skip_record_for_current_key = false
|
|
332
494
|
append_dot_change_capture_key(key)
|
|
333
|
-
if key == :
|
|
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
|
|
334
501
|
handle_ctrl_c
|
|
335
502
|
track_mode_transition(mode_before)
|
|
336
503
|
record_macro_key_if_needed(key)
|
|
@@ -338,23 +505,30 @@ module RuVim
|
|
|
338
505
|
end
|
|
339
506
|
|
|
340
507
|
case @editor.mode
|
|
508
|
+
when :hit_enter
|
|
509
|
+
handle_hit_enter_key(key)
|
|
341
510
|
when :insert
|
|
342
511
|
handle_insert_key(key)
|
|
343
512
|
when :command_line
|
|
344
513
|
handle_command_line_key(key)
|
|
345
514
|
when :visual_char, :visual_line, :visual_block
|
|
346
515
|
handle_visual_key(key)
|
|
516
|
+
when :rich
|
|
517
|
+
handle_rich_key(key)
|
|
347
518
|
else
|
|
348
519
|
handle_normal_key(key)
|
|
349
520
|
end
|
|
350
521
|
track_mode_transition(mode_before)
|
|
351
522
|
load_current_ftplugin!
|
|
352
523
|
record_macro_key_if_needed(key)
|
|
524
|
+
rescue RuVim::CommandError => e
|
|
525
|
+
@editor.echo_error(e.message)
|
|
353
526
|
end
|
|
354
527
|
|
|
355
528
|
def clear_stale_message_before_key(key)
|
|
356
529
|
return if @editor.message.to_s.empty?
|
|
357
530
|
return if @editor.command_line_active?
|
|
531
|
+
return if @editor.hit_enter_active?
|
|
358
532
|
|
|
359
533
|
# Keep the error visible while the user is still dismissing/cancelling;
|
|
360
534
|
# otherwise, the next operation replaces the command-line area naturally.
|
|
@@ -363,6 +537,48 @@ module RuVim
|
|
|
363
537
|
@editor.clear_message
|
|
364
538
|
end
|
|
365
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
|
+
|
|
366
582
|
def handle_normal_key(key)
|
|
367
583
|
case
|
|
368
584
|
when handle_normal_key_pre_dispatch(key)
|
|
@@ -417,43 +633,7 @@ module RuVim
|
|
|
417
633
|
end
|
|
418
634
|
|
|
419
635
|
def handle_normal_direct_token(token)
|
|
420
|
-
|
|
421
|
-
when "\""
|
|
422
|
-
start_register_pending
|
|
423
|
-
when "d"
|
|
424
|
-
start_operator_pending(:delete)
|
|
425
|
-
when "y"
|
|
426
|
-
start_operator_pending(:yank)
|
|
427
|
-
when "c"
|
|
428
|
-
start_operator_pending(:change)
|
|
429
|
-
when "r"
|
|
430
|
-
start_replace_pending
|
|
431
|
-
when "f", "F", "t", "T"
|
|
432
|
-
start_find_pending(token)
|
|
433
|
-
when ";"
|
|
434
|
-
repeat_last_find(reverse: false)
|
|
435
|
-
when ","
|
|
436
|
-
repeat_last_find(reverse: true)
|
|
437
|
-
when "."
|
|
438
|
-
repeat_last_change
|
|
439
|
-
when "q"
|
|
440
|
-
if @editor.macro_recording?
|
|
441
|
-
stop_macro_recording
|
|
442
|
-
else
|
|
443
|
-
start_macro_record_pending
|
|
444
|
-
end
|
|
445
|
-
when "@"
|
|
446
|
-
start_macro_play_pending
|
|
447
|
-
when "m"
|
|
448
|
-
start_mark_pending
|
|
449
|
-
when "'"
|
|
450
|
-
start_jump_pending(linewise: true, repeat_token: "'")
|
|
451
|
-
when "`"
|
|
452
|
-
start_jump_pending(linewise: false, repeat_token: "`")
|
|
453
|
-
else
|
|
454
|
-
return false
|
|
455
|
-
end
|
|
456
|
-
true
|
|
636
|
+
false
|
|
457
637
|
end
|
|
458
638
|
|
|
459
639
|
def resolve_normal_key_sequence
|
|
@@ -462,7 +642,7 @@ module RuVim
|
|
|
462
642
|
when :pending, :ambiguous
|
|
463
643
|
if match.status == :ambiguous && match.invocation
|
|
464
644
|
inv = dup_invocation(match.invocation)
|
|
465
|
-
inv.count = @editor.pending_count
|
|
645
|
+
inv.count = @editor.pending_count
|
|
466
646
|
@pending_ambiguous_invocation = inv
|
|
467
647
|
else
|
|
468
648
|
@pending_ambiguous_invocation = nil
|
|
@@ -472,9 +652,15 @@ module RuVim
|
|
|
472
652
|
when :match
|
|
473
653
|
clear_pending_key_timeout
|
|
474
654
|
matched_keys = @pending_keys.dup
|
|
475
|
-
repeat_count = @editor.pending_count
|
|
655
|
+
repeat_count = @editor.pending_count
|
|
656
|
+
@pending_keys = []
|
|
476
657
|
invocation = dup_invocation(match.invocation)
|
|
477
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
|
|
478
664
|
@dispatcher.dispatch(@editor, invocation)
|
|
479
665
|
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
480
666
|
else
|
|
@@ -532,6 +718,7 @@ module RuVim
|
|
|
532
718
|
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
533
719
|
@editor.current_window.cursor_x += 1
|
|
534
720
|
maybe_showmatch_after_insert(key)
|
|
721
|
+
maybe_dedent_after_insert(key)
|
|
535
722
|
end
|
|
536
723
|
end
|
|
537
724
|
|
|
@@ -577,6 +764,8 @@ module RuVim
|
|
|
577
764
|
when "d"
|
|
578
765
|
@visual_pending = nil
|
|
579
766
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
|
|
767
|
+
when "="
|
|
768
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_indent"))
|
|
580
769
|
when "\""
|
|
581
770
|
start_register_pending
|
|
582
771
|
when "i", "a"
|
|
@@ -632,7 +821,7 @@ module RuVim
|
|
|
632
821
|
|
|
633
822
|
if id
|
|
634
823
|
clear_pending_key_timeout
|
|
635
|
-
count = @editor.pending_count
|
|
824
|
+
count = @editor.pending_count
|
|
636
825
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
637
826
|
else
|
|
638
827
|
clear_pending_key_timeout
|
|
@@ -688,6 +877,10 @@ module RuVim
|
|
|
688
877
|
|
|
689
878
|
def handle_list_window_enter
|
|
690
879
|
buffer = @editor.current_buffer
|
|
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
|
|
691
884
|
return false unless buffer.kind == :quickfix || buffer.kind == :location_list
|
|
692
885
|
|
|
693
886
|
item_index = @editor.current_window.cursor_y - 2
|
|
@@ -729,6 +922,47 @@ module RuVim
|
|
|
729
922
|
true
|
|
730
923
|
end
|
|
731
924
|
|
|
925
|
+
def handle_filter_buffer_enter
|
|
926
|
+
buffer = @editor.current_buffer
|
|
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
|
|
940
|
+
|
|
941
|
+
@editor.delete_buffer(filter_buf_id)
|
|
942
|
+
target_buf = @editor.buffers[target_buffer_id]
|
|
943
|
+
if target_buf
|
|
944
|
+
@editor.switch_to_buffer(target_buffer_id) unless @editor.current_buffer.id == target_buffer_id
|
|
945
|
+
@editor.current_window.cursor_y = [target_row, target_buf.lines.length - 1].min
|
|
946
|
+
@editor.current_window.cursor_x = 0
|
|
947
|
+
end
|
|
948
|
+
true
|
|
949
|
+
end
|
|
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
|
+
|
|
732
966
|
def arrow_key?(key)
|
|
733
967
|
%i[left right up down].include?(key)
|
|
734
968
|
end
|
|
@@ -744,7 +978,7 @@ module RuVim
|
|
|
744
978
|
up: "cursor.up",
|
|
745
979
|
down: "cursor.down"
|
|
746
980
|
}.fetch(key)
|
|
747
|
-
inv = CommandInvocation.new(id:, count: @editor.pending_count
|
|
981
|
+
inv = CommandInvocation.new(id:, count: @editor.pending_count)
|
|
748
982
|
@dispatcher.dispatch(@editor, inv)
|
|
749
983
|
@editor.pending_count = nil
|
|
750
984
|
@pending_keys = []
|
|
@@ -754,7 +988,7 @@ module RuVim
|
|
|
754
988
|
id = (key == :pageup ? "cursor.page_up" : "cursor.page_down")
|
|
755
989
|
inv = CommandInvocation.new(
|
|
756
990
|
id: id,
|
|
757
|
-
count: @editor.pending_count
|
|
991
|
+
count: @editor.pending_count,
|
|
758
992
|
kwargs: { page_lines: current_page_step_lines }
|
|
759
993
|
)
|
|
760
994
|
@dispatcher.dispatch(@editor, inv)
|
|
@@ -789,6 +1023,8 @@ module RuVim
|
|
|
789
1023
|
when :ctrl_o then "<C-o>"
|
|
790
1024
|
when :ctrl_w then "<C-w>"
|
|
791
1025
|
when :ctrl_l then "<C-l>"
|
|
1026
|
+
when :ctrl_c then "<C-c>"
|
|
1027
|
+
when :ctrl_g then "<C-g>"
|
|
792
1028
|
when :left then "<Left>"
|
|
793
1029
|
when :right then "<Right>"
|
|
794
1030
|
when :up then "<Up>"
|
|
@@ -797,6 +1033,10 @@ module RuVim
|
|
|
797
1033
|
when :end then "<End>"
|
|
798
1034
|
when :pageup then "<PageUp>"
|
|
799
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>"
|
|
800
1040
|
else nil
|
|
801
1041
|
end
|
|
802
1042
|
end
|
|
@@ -812,8 +1052,49 @@ module RuVim
|
|
|
812
1052
|
)
|
|
813
1053
|
end
|
|
814
1054
|
|
|
1055
|
+
# Rich mode: delegates to normal mode key handling but blocks mutating operations.
|
|
1056
|
+
RICH_MODE_BLOCKED_COMMANDS = %w[
|
|
1057
|
+
mode.insert mode.append mode.append_line_end mode.insert_nonblank
|
|
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
|
|
1065
|
+
|
|
1066
|
+
def handle_hit_enter_key(key)
|
|
1067
|
+
token = normalize_key_token(key)
|
|
1068
|
+
case token
|
|
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
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
def handle_rich_key(key)
|
|
1081
|
+
token = normalize_key_token(key)
|
|
1082
|
+
if token == "\e"
|
|
1083
|
+
RuVim::RichView.close!(@editor)
|
|
1084
|
+
return
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
handle_normal_key(key)
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
def rich_mode_block_command?(command_id)
|
|
1091
|
+
RICH_MODE_BLOCKED_COMMANDS.include?(command_id.to_s)
|
|
1092
|
+
end
|
|
1093
|
+
|
|
815
1094
|
def handle_ctrl_c
|
|
816
1095
|
case @editor.mode
|
|
1096
|
+
when :hit_enter
|
|
1097
|
+
@editor.exit_hit_enter_mode
|
|
817
1098
|
when :insert
|
|
818
1099
|
finish_insert_change_group
|
|
819
1100
|
finish_dot_change_capture
|
|
@@ -832,6 +1113,18 @@ module RuVim
|
|
|
832
1113
|
@jump_pending = nil
|
|
833
1114
|
clear_pending_key_timeout
|
|
834
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)
|
|
835
1128
|
else
|
|
836
1129
|
clear_pending_key_timeout
|
|
837
1130
|
@editor.pending_count = nil
|
|
@@ -847,6 +1140,33 @@ module RuVim
|
|
|
847
1140
|
end
|
|
848
1141
|
end
|
|
849
1142
|
|
|
1143
|
+
def handle_normal_ctrl_c
|
|
1144
|
+
clear_pending_key_timeout
|
|
1145
|
+
@editor.pending_count = nil
|
|
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
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def suspend_to_shell
|
|
1163
|
+
@terminal.suspend_for_tstp
|
|
1164
|
+
@screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
|
|
1165
|
+
@needs_redraw = true
|
|
1166
|
+
rescue StandardError => e
|
|
1167
|
+
@editor.echo_error("suspend failed: #{e.message}")
|
|
1168
|
+
end
|
|
1169
|
+
|
|
850
1170
|
def finish_insert_change_group
|
|
851
1171
|
@editor.current_buffer.end_change_group
|
|
852
1172
|
end
|
|
@@ -871,7 +1191,7 @@ module RuVim
|
|
|
871
1191
|
end
|
|
872
1192
|
|
|
873
1193
|
def start_operator_pending(name)
|
|
874
|
-
@operator_pending = { name:, count:
|
|
1194
|
+
@operator_pending = { name:, count: @editor.pending_count }
|
|
875
1195
|
@editor.pending_count = nil
|
|
876
1196
|
@pending_keys = []
|
|
877
1197
|
@editor.echo(name == :delete ? "d" : name.to_s)
|
|
@@ -946,6 +1266,14 @@ module RuVim
|
|
|
946
1266
|
@editor.echo("q")
|
|
947
1267
|
end
|
|
948
1268
|
|
|
1269
|
+
def toggle_macro_recording_or_start_pending
|
|
1270
|
+
if @editor.macro_recording?
|
|
1271
|
+
stop_macro_recording
|
|
1272
|
+
else
|
|
1273
|
+
start_macro_record_pending
|
|
1274
|
+
end
|
|
1275
|
+
end
|
|
1276
|
+
|
|
949
1277
|
def finish_macro_record_pending(token)
|
|
950
1278
|
@macro_record_pending = false
|
|
951
1279
|
if token == "\e"
|
|
@@ -993,7 +1321,7 @@ module RuVim
|
|
|
993
1321
|
return
|
|
994
1322
|
end
|
|
995
1323
|
|
|
996
|
-
count = @editor.pending_count
|
|
1324
|
+
count = @editor.pending_count
|
|
997
1325
|
@editor.pending_count = nil
|
|
998
1326
|
play_macro(name, count:)
|
|
999
1327
|
end
|
|
@@ -1015,7 +1343,7 @@ module RuVim
|
|
|
1015
1343
|
@last_macro_name = reg
|
|
1016
1344
|
@macro_play_stack << reg
|
|
1017
1345
|
@suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
|
|
1018
|
-
count.times do
|
|
1346
|
+
[count.to_i, 1].max.times do
|
|
1019
1347
|
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
1020
1348
|
end
|
|
1021
1349
|
@editor.echo("@#{reg}")
|
|
@@ -1046,7 +1374,7 @@ module RuVim
|
|
|
1046
1374
|
|
|
1047
1375
|
def handle_operator_pending_key(token)
|
|
1048
1376
|
op = @operator_pending
|
|
1049
|
-
if %w[i a].include?(token) && !op[:motion_prefix]
|
|
1377
|
+
if %w[i a g].include?(token) && !op[:motion_prefix]
|
|
1050
1378
|
@operator_pending[:motion_prefix] = token
|
|
1051
1379
|
@editor.echo("#{op[:name].to_s[0]}#{token}")
|
|
1052
1380
|
return
|
|
@@ -1086,6 +1414,18 @@ module RuVim
|
|
|
1086
1414
|
return
|
|
1087
1415
|
end
|
|
1088
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
|
+
|
|
1089
1429
|
if op[:name] == :change && motion == "c"
|
|
1090
1430
|
inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
|
|
1091
1431
|
@dispatcher.dispatch(@editor, inv)
|
|
@@ -1104,7 +1444,7 @@ module RuVim
|
|
|
1104
1444
|
end
|
|
1105
1445
|
|
|
1106
1446
|
def start_replace_pending
|
|
1107
|
-
@replace_pending = { count:
|
|
1447
|
+
@replace_pending = { count: @editor.pending_count }
|
|
1108
1448
|
@editor.pending_count = nil
|
|
1109
1449
|
@pending_keys = []
|
|
1110
1450
|
@editor.echo("r")
|
|
@@ -1145,9 +1485,9 @@ module RuVim
|
|
|
1145
1485
|
return if (@dot_replay_depth || 0).positive?
|
|
1146
1486
|
|
|
1147
1487
|
case invocation.id
|
|
1148
|
-
when "buffer.delete_char", "buffer.paste_after", "buffer.paste_before"
|
|
1488
|
+
when "buffer.delete_char", "buffer.delete_motion", "buffer.join_lines", "buffer.swapcase_char", "buffer.paste_after", "buffer.paste_before"
|
|
1149
1489
|
record_last_change_keys(count_prefixed_keys(count, matched_keys))
|
|
1150
|
-
when "mode.insert", "mode.append", "mode.append_line_end", "mode.insert_nonblank", "mode.open_below", "mode.open_above"
|
|
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"
|
|
1151
1491
|
begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
|
|
1152
1492
|
end
|
|
1153
1493
|
end
|
|
@@ -1192,7 +1532,7 @@ module RuVim
|
|
|
1192
1532
|
@find_pending = {
|
|
1193
1533
|
direction: (token == "f" || token == "t") ? :forward : :backward,
|
|
1194
1534
|
till: (token == "t" || token == "T"),
|
|
1195
|
-
count:
|
|
1535
|
+
count: @editor.pending_count
|
|
1196
1536
|
}
|
|
1197
1537
|
@editor.pending_count = nil
|
|
1198
1538
|
@pending_keys = []
|
|
@@ -1237,7 +1577,7 @@ module RuVim
|
|
|
1237
1577
|
else
|
|
1238
1578
|
last[:direction]
|
|
1239
1579
|
end
|
|
1240
|
-
count = @editor.pending_count
|
|
1580
|
+
count = @editor.pending_count
|
|
1241
1581
|
@editor.pending_count = nil
|
|
1242
1582
|
@pending_keys = []
|
|
1243
1583
|
moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
|
|
@@ -1251,7 +1591,7 @@ module RuVim
|
|
|
1251
1591
|
pos = win.cursor_x
|
|
1252
1592
|
target = nil
|
|
1253
1593
|
|
|
1254
|
-
count.times do
|
|
1594
|
+
[count.to_i, 1].max.times do
|
|
1255
1595
|
idx =
|
|
1256
1596
|
if direction == :forward
|
|
1257
1597
|
line.index(char, pos + 1)
|
|
@@ -1305,6 +1645,66 @@ module RuVim
|
|
|
1305
1645
|
@cmdline_history_index = nil
|
|
1306
1646
|
end
|
|
1307
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
|
+
|
|
1308
1708
|
def command_line_history_move(delta)
|
|
1309
1709
|
cmd = @editor.command_line
|
|
1310
1710
|
hist = @cmdline_history[cmd.prefix]
|
|
@@ -1333,7 +1733,7 @@ module RuVim
|
|
|
1333
1733
|
ctx = ex_completion_context(cmd)
|
|
1334
1734
|
return unless ctx
|
|
1335
1735
|
|
|
1336
|
-
matches = ex_completion_candidates(ctx)
|
|
1736
|
+
matches = reusable_command_line_completion_matches(cmd, ctx) || ex_completion_candidates(ctx)
|
|
1337
1737
|
case matches.length
|
|
1338
1738
|
when 0
|
|
1339
1739
|
clear_command_line_completion
|
|
@@ -1347,6 +1747,29 @@ module RuVim
|
|
|
1347
1747
|
update_incsearch_preview_if_needed
|
|
1348
1748
|
end
|
|
1349
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
|
+
|
|
1350
1773
|
def clear_command_line_completion
|
|
1351
1774
|
@cmdline_completion = nil
|
|
1352
1775
|
end
|
|
@@ -1419,13 +1842,56 @@ module RuVim
|
|
|
1419
1842
|
def show_command_line_completion_menu(matches, selected:, force:)
|
|
1420
1843
|
return unless force || @editor.effective_option("wildmenu")
|
|
1421
1844
|
|
|
1422
|
-
|
|
1423
|
-
items = matches.first(limit).each_with_index.map do |m, i|
|
|
1845
|
+
items = matches.each_with_index.map do |m, i|
|
|
1424
1846
|
idx = i
|
|
1425
1847
|
idx == selected ? "[#{m}]" : m
|
|
1426
1848
|
end
|
|
1427
|
-
items
|
|
1428
|
-
|
|
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
|
|
1429
1895
|
end
|
|
1430
1896
|
|
|
1431
1897
|
def common_prefix(strings)
|
|
@@ -1464,6 +1930,7 @@ module RuVim
|
|
|
1464
1930
|
end
|
|
1465
1931
|
|
|
1466
1932
|
def apply_insert_autoindent(row, x, previous_row:)
|
|
1933
|
+
return x if @paste_batch
|
|
1467
1934
|
buf = @editor.current_buffer
|
|
1468
1935
|
win = @editor.current_window
|
|
1469
1936
|
return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
|
|
@@ -1473,7 +1940,11 @@ module RuVim
|
|
|
1473
1940
|
indent = prev[/\A[ \t]*/].to_s
|
|
1474
1941
|
if @editor.effective_option("smartindent", window: win, buffer: buf)
|
|
1475
1942
|
trimmed = prev.rstrip
|
|
1476
|
-
|
|
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
|
|
1477
1948
|
sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
|
|
1478
1949
|
sw = effective_tabstop(win, buf) if sw <= 0
|
|
1479
1950
|
sw = 2 if sw <= 0
|
|
@@ -1495,6 +1966,34 @@ module RuVim
|
|
|
1495
1966
|
@editor.echo_temporary("match", duration_seconds: mt * 0.1)
|
|
1496
1967
|
end
|
|
1497
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
|
+
|
|
1498
1997
|
def clear_expired_transient_message_if_any
|
|
1499
1998
|
@needs_redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
|
|
1500
1999
|
end
|
|
@@ -1893,6 +2392,10 @@ module RuVim
|
|
|
1893
2392
|
return option_completion_candidates(prefix)
|
|
1894
2393
|
end
|
|
1895
2394
|
|
|
2395
|
+
if cmd == "git"
|
|
2396
|
+
return Git::Handler::GIT_SUBCOMMANDS.keys.sort.select { |s| s.start_with?(prefix) }
|
|
2397
|
+
end
|
|
2398
|
+
|
|
1896
2399
|
[]
|
|
1897
2400
|
end
|
|
1898
2401
|
|
|
@@ -1906,15 +2409,27 @@ module RuVim
|
|
|
1906
2409
|
else
|
|
1907
2410
|
File.dirname(input)
|
|
1908
2411
|
end
|
|
1909
|
-
base_dir = "." if base_dir == "."
|
|
1910
2412
|
partial = input.end_with?("/") ? "" : File.basename(input)
|
|
1911
|
-
pattern =
|
|
1912
|
-
|
|
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|
|
|
1913
2423
|
next if [".", ".."].include?(File.basename(p))
|
|
1914
2424
|
next unless p.start_with?(input) || input.empty?
|
|
1915
2425
|
next if wildignore_path?(p)
|
|
1916
2426
|
File.directory?(p) ? "#{p}/" : p
|
|
1917
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
|
|
1918
2433
|
rescue StandardError
|
|
1919
2434
|
[]
|
|
1920
2435
|
end
|
|
@@ -2066,6 +2581,17 @@ module RuVim
|
|
|
2066
2581
|
@editor.echo("readonly: #{buf.display_name}")
|
|
2067
2582
|
end
|
|
2068
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
|
+
|
|
2069
2595
|
def apply_startup_nomodifiable!
|
|
2070
2596
|
buf = @editor.current_buffer
|
|
2071
2597
|
return unless buf&.file_buffer?
|
|
@@ -2096,35 +2622,604 @@ module RuVim
|
|
|
2096
2622
|
list = Array(paths).compact
|
|
2097
2623
|
return if list.empty?
|
|
2098
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
|
+
|
|
2099
2632
|
first, *rest = list
|
|
2100
2633
|
@editor.open_path(first)
|
|
2101
2634
|
apply_startup_readonly! if @startup_readonly
|
|
2102
2635
|
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
2636
|
+
apply_startup_follow! if @startup_follow
|
|
2103
2637
|
|
|
2104
2638
|
case @startup_open_layout
|
|
2105
2639
|
when :horizontal
|
|
2640
|
+
first_win_id = @editor.current_window_id
|
|
2106
2641
|
rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
|
|
2642
|
+
@editor.focus_window(first_win_id)
|
|
2107
2643
|
when :vertical
|
|
2644
|
+
first_win_id = @editor.current_window_id
|
|
2108
2645
|
rest.each { |p| open_path_in_split!(p, layout: :vertical) }
|
|
2646
|
+
@editor.focus_window(first_win_id)
|
|
2109
2647
|
when :tab
|
|
2110
2648
|
rest.each { |p| open_path_in_tab!(p) }
|
|
2649
|
+
@editor.tabnext(-(@editor.tabpage_count - 1))
|
|
2111
2650
|
else
|
|
2112
|
-
#
|
|
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
|
|
2113
2665
|
end
|
|
2666
|
+
return unless bid
|
|
2667
|
+
|
|
2668
|
+
@editor.buffers.delete(bid)
|
|
2669
|
+
@editor.instance_variable_set(:@next_buffer_id, 1)
|
|
2114
2670
|
end
|
|
2115
2671
|
|
|
2116
2672
|
def open_path_in_split!(path, layout:)
|
|
2117
2673
|
@editor.split_current_window(layout:)
|
|
2118
|
-
|
|
2119
|
-
@editor.switch_to_buffer(buf.id)
|
|
2674
|
+
@editor.open_path(path)
|
|
2120
2675
|
apply_startup_readonly! if @startup_readonly
|
|
2121
2676
|
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
2677
|
+
apply_startup_follow! if @startup_follow
|
|
2122
2678
|
end
|
|
2123
2679
|
|
|
2124
2680
|
def open_path_in_tab!(path)
|
|
2125
2681
|
@editor.tabnew(path:)
|
|
2126
2682
|
apply_startup_readonly! if @startup_readonly
|
|
2127
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
|
|
2128
3223
|
end
|
|
2129
3224
|
|
|
2130
3225
|
def move_cursor_to_line(line_number)
|