ruvim 0.2.0 → 0.3.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 +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +23 -0
- data/docs/command.md +85 -0
- data/docs/config.md +2 -2
- data/docs/done.md +21 -0
- data/docs/spec.md +157 -12
- data/docs/todo.md +1 -5
- data/docs/vim_diff.md +94 -172
- data/lib/ruvim/app.rb +882 -69
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -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 +455 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +890 -63
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +39 -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/json.rb +18 -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/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +503 -106
- 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 +14 -0
- data/test/app_completion_test.rb +73 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +729 -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/dispatcher_test.rb +322 -0
- data/test/editor_register_test.rb +23 -0
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -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 +478 -0
- data/test/screen_test.rb +304 -0
- metadata +33 -2
data/lib/ruvim/app.rb
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
1
6
|
module RuVim
|
|
2
7
|
class App
|
|
3
|
-
|
|
8
|
+
LARGE_FILE_ASYNC_THRESHOLD_BYTES = 64 * 1024 * 1024
|
|
9
|
+
LARGE_FILE_STAGED_PREFIX_BYTES = 8 * 1024 * 1024
|
|
10
|
+
ASYNC_FILE_READ_CHUNK_BYTES = 1 * 1024 * 1024
|
|
11
|
+
ASYNC_FILE_EVENT_FLUSH_BYTES = 4 * 1024 * 1024
|
|
12
|
+
|
|
13
|
+
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, restricted: false, verbose_level: 0, verbose_io: STDERR, startup_time_path: nil, startup_open_layout: nil, startup_open_count: nil)
|
|
14
|
+
startup_paths = Array(paths || path).compact
|
|
15
|
+
@ui_stdin = ui_stdin || stdin
|
|
16
|
+
@stdin_stream_mode = !!stdin_stream_mode
|
|
17
|
+
@stdin_stream_source = @stdin_stream_mode ? stdin : nil
|
|
4
18
|
@editor = Editor.new
|
|
5
|
-
@terminal = Terminal.new(stdin
|
|
6
|
-
@input = Input.new(
|
|
19
|
+
@terminal = Terminal.new(stdin: @ui_stdin, stdout:)
|
|
20
|
+
@input = Input.new(@ui_stdin)
|
|
7
21
|
@screen = Screen.new(terminal: @terminal)
|
|
8
22
|
@dispatcher = Dispatcher.new
|
|
9
23
|
@keymaps = KeymapManager.new
|
|
10
24
|
@signal_r, @signal_w = IO.pipe
|
|
25
|
+
@stream_event_queue = nil
|
|
26
|
+
@stream_reader_thread = nil
|
|
27
|
+
@stream_buffer_id = nil
|
|
28
|
+
@stream_stop_requested = false
|
|
29
|
+
@async_file_loads = {}
|
|
11
30
|
@cmdline_history = Hash.new { |h, k| h[k] = [] }
|
|
12
31
|
@cmdline_history_index = nil
|
|
13
32
|
@cmdline_completion = nil
|
|
@@ -33,6 +52,11 @@ module RuVim
|
|
|
33
52
|
@startup_open_layout = startup_open_layout
|
|
34
53
|
@startup_open_count = startup_open_count
|
|
35
54
|
@editor.restricted_mode = @restricted_mode
|
|
55
|
+
@editor.stdin_stream_stop_handler = method(:stdin_stream_stop_command)
|
|
56
|
+
@editor.open_path_handler = method(:open_path_with_large_file_support)
|
|
57
|
+
@editor.keymap_manager = @keymaps
|
|
58
|
+
@editor.app_action_handler = method(:handle_editor_app_action)
|
|
59
|
+
load_command_line_history!
|
|
36
60
|
|
|
37
61
|
startup_mark("init.start")
|
|
38
62
|
register_builtins!
|
|
@@ -48,8 +72,10 @@ module RuVim
|
|
|
48
72
|
install_signal_handlers
|
|
49
73
|
startup_mark("signals.installed")
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
if @stdin_stream_mode && startup_paths.empty?
|
|
76
|
+
verbose_log(1, "startup: stdin stream buffer")
|
|
77
|
+
prepare_stdin_stream_buffer!
|
|
78
|
+
elsif startup_paths.empty?
|
|
53
79
|
verbose_log(1, "startup: intro")
|
|
54
80
|
@editor.show_intro_buffer_if_applicable!
|
|
55
81
|
else
|
|
@@ -64,12 +90,14 @@ module RuVim
|
|
|
64
90
|
verbose_log(1, "startup: run_startup_actions count=#{Array(startup_actions).length}")
|
|
65
91
|
run_startup_actions!(startup_actions)
|
|
66
92
|
startup_mark("startup_actions.done")
|
|
93
|
+
start_stdin_stream_reader! if @stream_buffer_id
|
|
67
94
|
write_startuptime_log!
|
|
68
95
|
end
|
|
69
96
|
|
|
70
97
|
def run
|
|
71
98
|
@terminal.with_ui do
|
|
72
99
|
loop do
|
|
100
|
+
@needs_redraw = true if drain_stream_events!
|
|
73
101
|
if @needs_redraw
|
|
74
102
|
@screen.render(@editor)
|
|
75
103
|
@needs_redraw = false
|
|
@@ -89,8 +117,18 @@ module RuVim
|
|
|
89
117
|
|
|
90
118
|
handle_key(key)
|
|
91
119
|
@needs_redraw = true
|
|
120
|
+
|
|
121
|
+
# Batch insert-mode keystrokes to avoid per-char rendering during paste
|
|
122
|
+
while @editor.mode == :insert && @input.has_pending_input?
|
|
123
|
+
batch_key = @input.read_key(timeout: 0, esc_timeout: 0)
|
|
124
|
+
break unless batch_key
|
|
125
|
+
handle_key(batch_key)
|
|
126
|
+
end
|
|
92
127
|
end
|
|
93
128
|
end
|
|
129
|
+
ensure
|
|
130
|
+
shutdown_background_readers!
|
|
131
|
+
save_command_line_history!
|
|
94
132
|
end
|
|
95
133
|
|
|
96
134
|
def run_startup_actions!(actions, log_prefix: "startup")
|
|
@@ -173,6 +211,9 @@ module RuVim
|
|
|
173
211
|
register_internal_unless(cmd, "cursor.page_down.half", call: :cursor_page_down_half, desc: "Move half page down")
|
|
174
212
|
register_internal_unless(cmd, "window.scroll_up.line", call: :window_scroll_up_line, desc: "Scroll window up one line")
|
|
175
213
|
register_internal_unless(cmd, "window.scroll_down.line", call: :window_scroll_down_line, desc: "Scroll window down one line")
|
|
214
|
+
register_internal_unless(cmd, "window.cursor_line_top", call: :window_cursor_line_top, desc: "Put cursor line at top")
|
|
215
|
+
register_internal_unless(cmd, "window.cursor_line_center", call: :window_cursor_line_center, desc: "Put cursor line at center")
|
|
216
|
+
register_internal_unless(cmd, "window.cursor_line_bottom", call: :window_cursor_line_bottom, desc: "Put cursor line at bottom")
|
|
176
217
|
register_internal_unless(cmd, "cursor.line_start", call: :cursor_line_start, desc: "Move to column 1")
|
|
177
218
|
register_internal_unless(cmd, "cursor.line_end", call: :cursor_line_end, desc: "Move to end of line")
|
|
178
219
|
register_internal_unless(cmd, "cursor.first_nonblank", call: :cursor_first_nonblank, desc: "Move to first nonblank")
|
|
@@ -198,10 +239,17 @@ module RuVim
|
|
|
198
239
|
register_internal_unless(cmd, "window.focus_right", call: :window_focus_right, desc: "Focus right window")
|
|
199
240
|
register_internal_unless(cmd, "window.focus_up", call: :window_focus_up, desc: "Focus upper window")
|
|
200
241
|
register_internal_unless(cmd, "window.focus_down", call: :window_focus_down, desc: "Focus lower window")
|
|
242
|
+
register_internal_unless(cmd, "window.focus_or_split_left", call: :window_focus_or_split_left, desc: "Focus left window or split")
|
|
243
|
+
register_internal_unless(cmd, "window.focus_or_split_right", call: :window_focus_or_split_right, desc: "Focus right window or split")
|
|
244
|
+
register_internal_unless(cmd, "window.focus_or_split_up", call: :window_focus_or_split_up, desc: "Focus upper window or split")
|
|
245
|
+
register_internal_unless(cmd, "window.focus_or_split_down", call: :window_focus_or_split_down, desc: "Focus lower window or split")
|
|
201
246
|
register_internal_unless(cmd, "mode.command_line", call: :enter_command_line_mode, desc: "Enter command-line mode")
|
|
202
247
|
register_internal_unless(cmd, "mode.search_forward", call: :enter_search_forward_mode, desc: "Enter / search")
|
|
203
248
|
register_internal_unless(cmd, "mode.search_backward", call: :enter_search_backward_mode, desc: "Enter ? search")
|
|
204
249
|
register_internal_unless(cmd, "buffer.delete_char", call: :delete_char, desc: "Delete char under cursor")
|
|
250
|
+
register_internal_unless(cmd, "buffer.substitute_char", call: :substitute_char, desc: "Substitute char(s)")
|
|
251
|
+
register_internal_unless(cmd, "buffer.swapcase_char", call: :swapcase_char, desc: "Swap case under cursor")
|
|
252
|
+
register_internal_unless(cmd, "buffer.join_lines", call: :join_lines, desc: "Join lines")
|
|
205
253
|
register_internal_unless(cmd, "buffer.delete_line", call: :delete_line, desc: "Delete current line")
|
|
206
254
|
register_internal_unless(cmd, "buffer.delete_motion", call: :delete_motion, desc: "Delete by motion")
|
|
207
255
|
register_internal_unless(cmd, "buffer.change_motion", call: :change_motion, desc: "Change by motion")
|
|
@@ -231,10 +279,39 @@ module RuVim
|
|
|
231
279
|
register_internal_unless(cmd, "buffer.replace_char", call: :replace_char, desc: "Replace single char")
|
|
232
280
|
register_internal_unless(cmd, "file.goto_under_cursor", call: :file_goto_under_cursor, desc: "Open file under cursor")
|
|
233
281
|
register_internal_unless(cmd, "ui.clear_message", call: :clear_message, desc: "Clear message")
|
|
282
|
+
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")
|
|
283
|
+
register_internal_unless(cmd, "normal.operator_delete_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :delete) }, desc: "Start delete operator")
|
|
284
|
+
register_internal_unless(cmd, "normal.operator_yank_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :yank) }, desc: "Start yank operator")
|
|
285
|
+
register_internal_unless(cmd, "normal.operator_change_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :change) }, desc: "Start change operator")
|
|
286
|
+
register_internal_unless(cmd, "normal.operator_indent_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_operator_start, name: :indent) }, desc: "Start indent operator")
|
|
287
|
+
register_internal_unless(cmd, "buffer.indent_lines", call: :indent_lines, desc: "Auto-indent lines")
|
|
288
|
+
register_internal_unless(cmd, "buffer.indent_motion", call: :indent_motion, desc: "Auto-indent motion range")
|
|
289
|
+
register_internal_unless(cmd, "buffer.visual_indent", call: :visual_indent, desc: "Auto-indent visual selection")
|
|
290
|
+
register_internal_unless(cmd, "normal.replace_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_replace_pending_start) }, desc: "Start replace-char pending")
|
|
291
|
+
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")
|
|
292
|
+
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")
|
|
293
|
+
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")
|
|
294
|
+
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")
|
|
295
|
+
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")
|
|
296
|
+
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")
|
|
297
|
+
register_internal_unless(cmd, "normal.change_repeat", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_change_repeat) }, desc: "Repeat last change")
|
|
298
|
+
register_internal_unless(cmd, "normal.macro_record_toggle", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_macro_record_toggle) }, desc: "Start/stop macro recording")
|
|
299
|
+
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")
|
|
300
|
+
register_internal_unless(cmd, "normal.mark_pending_start", call: ->(ctx, **) { ctx.editor.invoke_app_action(:normal_mark_pending_start) }, desc: "Start mark set pending")
|
|
301
|
+
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")
|
|
302
|
+
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")
|
|
303
|
+
register_internal_unless(
|
|
304
|
+
cmd,
|
|
305
|
+
"stdin.stream_stop",
|
|
306
|
+
call: ->(ctx, **) { ctx.editor.stdin_stream_stop_or_cancel! },
|
|
307
|
+
desc: "Stop stdin follow stream (or cancel pending state)"
|
|
308
|
+
)
|
|
234
309
|
|
|
235
310
|
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :maybe_one, bang: true)
|
|
236
311
|
register_ex_unless(ex, "q", call: :app_quit, aliases: %w[quit], desc: "Quit", nargs: 0, bang: true)
|
|
312
|
+
register_ex_unless(ex, "qa", call: :app_quit_all, aliases: %w[qall], desc: "Quit all", nargs: 0, bang: true)
|
|
237
313
|
register_ex_unless(ex, "wq", call: :file_write_quit, desc: "Write and quit", nargs: :maybe_one, bang: true)
|
|
314
|
+
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
315
|
register_ex_unless(ex, "e", call: :file_edit, aliases: %w[edit], desc: "Edit file / reload", nargs: :maybe_one, bang: true)
|
|
239
316
|
register_ex_unless(ex, "help", call: :ex_help, desc: "Show help / topics", nargs: :any)
|
|
240
317
|
register_ex_unless(ex, "command", call: :ex_define_command, desc: "Define user command", nargs: :any, bang: true)
|
|
@@ -244,7 +321,13 @@ module RuVim
|
|
|
244
321
|
register_ex_unless(ex, "bprev", call: :buffer_prev, aliases: %w[bp], desc: "Previous buffer", nargs: 0, bang: true)
|
|
245
322
|
register_ex_unless(ex, "buffer", call: :buffer_switch, aliases: %w[b], desc: "Switch buffer", nargs: 1, bang: true)
|
|
246
323
|
register_ex_unless(ex, "bdelete", call: :buffer_delete, aliases: %w[bd], desc: "Delete buffer", nargs: :maybe_one, bang: true)
|
|
324
|
+
register_ex_unless(ex, "args", call: :arglist_show, desc: "Show argument list", nargs: 0)
|
|
325
|
+
register_ex_unless(ex, "next", call: :arglist_next, desc: "Next argument", nargs: 0)
|
|
326
|
+
register_ex_unless(ex, "prev", call: :arglist_prev, desc: "Previous argument", nargs: 0)
|
|
327
|
+
register_ex_unless(ex, "first", call: :arglist_first, desc: "First argument", nargs: 0)
|
|
328
|
+
register_ex_unless(ex, "last", call: :arglist_last, desc: "Last argument", nargs: 0)
|
|
247
329
|
register_ex_unless(ex, "commands", call: :ex_commands, desc: "List Ex commands", nargs: 0)
|
|
330
|
+
register_ex_unless(ex, "bindings", call: :ex_bindings, desc: "List active key bindings", nargs: :any)
|
|
248
331
|
register_ex_unless(ex, "set", call: :ex_set, desc: "Set options", nargs: :any)
|
|
249
332
|
register_ex_unless(ex, "setlocal", call: :ex_setlocal, desc: "Set window/buffer local option", nargs: :any)
|
|
250
333
|
register_ex_unless(ex, "setglobal", call: :ex_setglobal, desc: "Set global option", nargs: :any)
|
|
@@ -253,6 +336,7 @@ module RuVim
|
|
|
253
336
|
register_ex_unless(ex, "tabnew", call: :tab_new, desc: "New tab", nargs: :maybe_one)
|
|
254
337
|
register_ex_unless(ex, "tabnext", call: :tab_next, aliases: %w[tabn], desc: "Next tab", nargs: 0)
|
|
255
338
|
register_ex_unless(ex, "tabprev", call: :tab_prev, aliases: %w[tabp], desc: "Prev tab", nargs: 0)
|
|
339
|
+
register_ex_unless(ex, "tabs", call: :tab_list, desc: "List tabs", nargs: 0)
|
|
256
340
|
register_ex_unless(ex, "vimgrep", call: :ex_vimgrep, desc: "Populate quickfix from regex (minimal)", nargs: :any)
|
|
257
341
|
register_ex_unless(ex, "lvimgrep", call: :ex_lvimgrep, desc: "Populate location list from regex (minimal)", nargs: :any)
|
|
258
342
|
register_ex_unless(ex, "copen", call: :ex_copen, desc: "Open quickfix list", nargs: 0)
|
|
@@ -263,6 +347,15 @@ module RuVim
|
|
|
263
347
|
register_ex_unless(ex, "lclose", call: :ex_lclose, desc: "Close location list window", nargs: 0)
|
|
264
348
|
register_ex_unless(ex, "lnext", call: :ex_lnext, aliases: %w[ln], desc: "Next location item", nargs: 0)
|
|
265
349
|
register_ex_unless(ex, "lprev", call: :ex_lprev, aliases: %w[lp], desc: "Prev location item", nargs: 0)
|
|
350
|
+
register_ex_unless(ex, "grep", call: :ex_grep, desc: "Search with external grep", nargs: :any)
|
|
351
|
+
register_ex_unless(ex, "lgrep", call: :ex_lgrep, desc: "Search with external grep (location list)", nargs: :any)
|
|
352
|
+
register_ex_unless(ex, "d", call: :ex_delete_lines, aliases: %w[delete], desc: "Delete lines", nargs: :any)
|
|
353
|
+
register_ex_unless(ex, "y", call: :ex_yank_lines, aliases: %w[yank], desc: "Yank lines", nargs: :any)
|
|
354
|
+
register_ex_unless(ex, "rich", call: :ex_rich, desc: "Open/close Rich View", nargs: :maybe_one)
|
|
355
|
+
register_internal_unless(cmd, "rich.toggle", call: :rich_toggle, desc: "Toggle Rich View")
|
|
356
|
+
register_internal_unless(cmd, "quickfix.next", call: :ex_cnext, desc: "Next quickfix item")
|
|
357
|
+
register_internal_unless(cmd, "quickfix.prev", call: :ex_cprev, desc: "Prev quickfix item")
|
|
358
|
+
register_internal_unless(cmd, "quickfix.open", call: :ex_copen, desc: "Open quickfix list")
|
|
266
359
|
end
|
|
267
360
|
|
|
268
361
|
def bind_default_keys!
|
|
@@ -297,10 +390,40 @@ module RuVim
|
|
|
297
390
|
@keymaps.bind(:normal, ["<C-w>", "j"], "window.focus_down")
|
|
298
391
|
@keymaps.bind(:normal, ["<C-w>", "k"], "window.focus_up")
|
|
299
392
|
@keymaps.bind(:normal, ["<C-w>", "l"], "window.focus_right")
|
|
393
|
+
@keymaps.bind(:normal, ["<S-Left>"], "window.focus_or_split_left")
|
|
394
|
+
@keymaps.bind(:normal, ["<S-Right>"], "window.focus_or_split_right")
|
|
395
|
+
@keymaps.bind(:normal, ["<S-Up>"], "window.focus_or_split_up")
|
|
396
|
+
@keymaps.bind(:normal, ["<S-Down>"], "window.focus_or_split_down")
|
|
300
397
|
@keymaps.bind(:normal, ":", "mode.command_line")
|
|
301
398
|
@keymaps.bind(:normal, "/", "mode.search_forward")
|
|
302
399
|
@keymaps.bind(:normal, "?", "mode.search_backward")
|
|
303
400
|
@keymaps.bind(:normal, "x", "buffer.delete_char")
|
|
401
|
+
@keymaps.bind(:normal, "X", "buffer.delete_motion", kwargs: { motion: "h" })
|
|
402
|
+
@keymaps.bind(:normal, "s", "buffer.substitute_char")
|
|
403
|
+
@keymaps.bind(:normal, "D", "buffer.delete_motion", kwargs: { motion: "$" })
|
|
404
|
+
@keymaps.bind(:normal, "C", "buffer.change_motion", kwargs: { motion: "$" })
|
|
405
|
+
@keymaps.bind(:normal, "S", "buffer.change_line")
|
|
406
|
+
@keymaps.bind(:normal, "Y", "buffer.yank_line")
|
|
407
|
+
@keymaps.bind(:normal, "J", "buffer.join_lines")
|
|
408
|
+
@keymaps.bind(:normal, "~", "buffer.swapcase_char")
|
|
409
|
+
@keymaps.bind(:normal, "\"", "normal.register_pending_start")
|
|
410
|
+
@keymaps.bind(:normal, "d", "normal.operator_delete_start")
|
|
411
|
+
@keymaps.bind(:normal, "y", "normal.operator_yank_start")
|
|
412
|
+
@keymaps.bind(:normal, "c", "normal.operator_change_start")
|
|
413
|
+
@keymaps.bind(:normal, "=", "normal.operator_indent_start")
|
|
414
|
+
@keymaps.bind(:normal, "r", "normal.replace_pending_start")
|
|
415
|
+
@keymaps.bind(:normal, "f", "normal.find_char_forward_start")
|
|
416
|
+
@keymaps.bind(:normal, "F", "normal.find_char_backward_start")
|
|
417
|
+
@keymaps.bind(:normal, "t", "normal.find_till_forward_start")
|
|
418
|
+
@keymaps.bind(:normal, "T", "normal.find_till_backward_start")
|
|
419
|
+
@keymaps.bind(:normal, ";", "normal.find_repeat")
|
|
420
|
+
@keymaps.bind(:normal, ",", "normal.find_repeat_reverse")
|
|
421
|
+
@keymaps.bind(:normal, ".", "normal.change_repeat")
|
|
422
|
+
@keymaps.bind(:normal, "q", "normal.macro_record_toggle")
|
|
423
|
+
@keymaps.bind(:normal, "@", "normal.macro_play_pending_start")
|
|
424
|
+
@keymaps.bind(:normal, "m", "normal.mark_pending_start")
|
|
425
|
+
@keymaps.bind(:normal, "'", "normal.jump_mark_linewise_pending_start")
|
|
426
|
+
@keymaps.bind(:normal, "`", "normal.jump_mark_exact_pending_start")
|
|
304
427
|
@keymaps.bind(:normal, "p", "buffer.paste_after")
|
|
305
428
|
@keymaps.bind(:normal, "P", "buffer.paste_before")
|
|
306
429
|
@keymaps.bind(:normal, "u", "buffer.undo")
|
|
@@ -313,6 +436,10 @@ module RuVim
|
|
|
313
436
|
@keymaps.bind(:normal, ["<C-b>"], "cursor.page_up.default")
|
|
314
437
|
@keymaps.bind(:normal, ["<C-e>"], "window.scroll_down.line")
|
|
315
438
|
@keymaps.bind(:normal, ["<C-y>"], "window.scroll_up.line")
|
|
439
|
+
@keymaps.bind(:normal, "zt", "window.cursor_line_top")
|
|
440
|
+
@keymaps.bind(:normal, "zz", "window.cursor_line_center")
|
|
441
|
+
@keymaps.bind(:normal, "zb", "window.cursor_line_bottom")
|
|
442
|
+
@keymaps.bind(:normal, ["<C-c>"], "stdin.stream_stop")
|
|
316
443
|
@keymaps.bind(:normal, "n", "search.next")
|
|
317
444
|
@keymaps.bind(:normal, "N", "search.prev")
|
|
318
445
|
@keymaps.bind(:normal, "*", "search.word_forward")
|
|
@@ -320,6 +447,10 @@ module RuVim
|
|
|
320
447
|
@keymaps.bind(:normal, "g*", "search.word_forward_partial")
|
|
321
448
|
@keymaps.bind(:normal, "g#", "search.word_backward_partial")
|
|
322
449
|
@keymaps.bind(:normal, "gf", "file.goto_under_cursor")
|
|
450
|
+
@keymaps.bind(:normal, "gr", "rich.toggle")
|
|
451
|
+
@keymaps.bind(:normal, "Q", "quickfix.open")
|
|
452
|
+
@keymaps.bind(:normal, ["]", "q"], "quickfix.next")
|
|
453
|
+
@keymaps.bind(:normal, ["[", "q"], "quickfix.prev")
|
|
323
454
|
@keymaps.bind(:normal, ["<PageUp>"], "cursor.page_up.default")
|
|
324
455
|
@keymaps.bind(:normal, ["<PageDown>"], "cursor.page_down.default")
|
|
325
456
|
@keymaps.bind(:normal, "\e", "ui.clear_message")
|
|
@@ -330,7 +461,12 @@ module RuVim
|
|
|
330
461
|
clear_stale_message_before_key(key)
|
|
331
462
|
@skip_record_for_current_key = false
|
|
332
463
|
append_dot_change_capture_key(key)
|
|
333
|
-
if key == :
|
|
464
|
+
if key == :ctrl_z
|
|
465
|
+
suspend_to_shell
|
|
466
|
+
track_mode_transition(mode_before)
|
|
467
|
+
return
|
|
468
|
+
end
|
|
469
|
+
if key == :ctrl_c && @editor.mode != :normal
|
|
334
470
|
handle_ctrl_c
|
|
335
471
|
track_mode_transition(mode_before)
|
|
336
472
|
record_macro_key_if_needed(key)
|
|
@@ -338,23 +474,30 @@ module RuVim
|
|
|
338
474
|
end
|
|
339
475
|
|
|
340
476
|
case @editor.mode
|
|
477
|
+
when :hit_enter
|
|
478
|
+
handle_hit_enter_key(key)
|
|
341
479
|
when :insert
|
|
342
480
|
handle_insert_key(key)
|
|
343
481
|
when :command_line
|
|
344
482
|
handle_command_line_key(key)
|
|
345
483
|
when :visual_char, :visual_line, :visual_block
|
|
346
484
|
handle_visual_key(key)
|
|
485
|
+
when :rich
|
|
486
|
+
handle_rich_key(key)
|
|
347
487
|
else
|
|
348
488
|
handle_normal_key(key)
|
|
349
489
|
end
|
|
350
490
|
track_mode_transition(mode_before)
|
|
351
491
|
load_current_ftplugin!
|
|
352
492
|
record_macro_key_if_needed(key)
|
|
493
|
+
rescue RuVim::CommandError => e
|
|
494
|
+
@editor.echo_error(e.message)
|
|
353
495
|
end
|
|
354
496
|
|
|
355
497
|
def clear_stale_message_before_key(key)
|
|
356
498
|
return if @editor.message.to_s.empty?
|
|
357
499
|
return if @editor.command_line_active?
|
|
500
|
+
return if @editor.hit_enter_active?
|
|
358
501
|
|
|
359
502
|
# Keep the error visible while the user is still dismissing/cancelling;
|
|
360
503
|
# otherwise, the next operation replaces the command-line area naturally.
|
|
@@ -363,6 +506,46 @@ module RuVim
|
|
|
363
506
|
@editor.clear_message
|
|
364
507
|
end
|
|
365
508
|
|
|
509
|
+
def handle_editor_app_action(name, **kwargs)
|
|
510
|
+
if @editor.rich_mode?
|
|
511
|
+
case name.to_sym
|
|
512
|
+
when :normal_operator_start
|
|
513
|
+
op = (kwargs[:name] || kwargs["name"]).to_sym
|
|
514
|
+
return if op == :delete || op == :change
|
|
515
|
+
when :normal_replace_pending_start, :normal_change_repeat
|
|
516
|
+
return
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
case name.to_sym
|
|
521
|
+
when :normal_register_pending_start
|
|
522
|
+
start_register_pending
|
|
523
|
+
when :normal_operator_start
|
|
524
|
+
start_operator_pending((kwargs[:name] || kwargs["name"]).to_sym)
|
|
525
|
+
when :normal_replace_pending_start
|
|
526
|
+
start_replace_pending
|
|
527
|
+
when :normal_find_pending_start
|
|
528
|
+
start_find_pending((kwargs[:token] || kwargs["token"]).to_s)
|
|
529
|
+
when :normal_find_repeat
|
|
530
|
+
repeat_last_find(reverse: !!(kwargs[:reverse] || kwargs["reverse"]))
|
|
531
|
+
when :normal_change_repeat
|
|
532
|
+
repeat_last_change
|
|
533
|
+
when :normal_macro_record_toggle
|
|
534
|
+
toggle_macro_recording_or_start_pending
|
|
535
|
+
when :normal_macro_play_pending_start
|
|
536
|
+
start_macro_play_pending
|
|
537
|
+
when :normal_mark_pending_start
|
|
538
|
+
start_mark_pending
|
|
539
|
+
when :normal_jump_pending_start
|
|
540
|
+
start_jump_pending(
|
|
541
|
+
linewise: !!(kwargs[:linewise] || kwargs["linewise"]),
|
|
542
|
+
repeat_token: (kwargs[:repeat_token] || kwargs["repeat_token"]).to_s
|
|
543
|
+
)
|
|
544
|
+
else
|
|
545
|
+
raise RuVim::CommandError, "Unknown app action: #{name}"
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
366
549
|
def handle_normal_key(key)
|
|
367
550
|
case
|
|
368
551
|
when handle_normal_key_pre_dispatch(key)
|
|
@@ -417,43 +600,7 @@ module RuVim
|
|
|
417
600
|
end
|
|
418
601
|
|
|
419
602
|
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
|
|
603
|
+
false
|
|
457
604
|
end
|
|
458
605
|
|
|
459
606
|
def resolve_normal_key_sequence
|
|
@@ -462,7 +609,7 @@ module RuVim
|
|
|
462
609
|
when :pending, :ambiguous
|
|
463
610
|
if match.status == :ambiguous && match.invocation
|
|
464
611
|
inv = dup_invocation(match.invocation)
|
|
465
|
-
inv.count = @editor.pending_count
|
|
612
|
+
inv.count = @editor.pending_count
|
|
466
613
|
@pending_ambiguous_invocation = inv
|
|
467
614
|
else
|
|
468
615
|
@pending_ambiguous_invocation = nil
|
|
@@ -472,9 +619,15 @@ module RuVim
|
|
|
472
619
|
when :match
|
|
473
620
|
clear_pending_key_timeout
|
|
474
621
|
matched_keys = @pending_keys.dup
|
|
475
|
-
repeat_count = @editor.pending_count
|
|
622
|
+
repeat_count = @editor.pending_count
|
|
623
|
+
@pending_keys = []
|
|
476
624
|
invocation = dup_invocation(match.invocation)
|
|
477
625
|
invocation.count = repeat_count
|
|
626
|
+
if @editor.rich_mode? && rich_mode_block_command?(invocation.id)
|
|
627
|
+
@editor.pending_count = nil
|
|
628
|
+
@pending_keys = []
|
|
629
|
+
return
|
|
630
|
+
end
|
|
478
631
|
@dispatcher.dispatch(@editor, invocation)
|
|
479
632
|
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
480
633
|
else
|
|
@@ -532,6 +685,7 @@ module RuVim
|
|
|
532
685
|
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
533
686
|
@editor.current_window.cursor_x += 1
|
|
534
687
|
maybe_showmatch_after_insert(key)
|
|
688
|
+
maybe_dedent_after_insert(key)
|
|
535
689
|
end
|
|
536
690
|
end
|
|
537
691
|
|
|
@@ -577,6 +731,8 @@ module RuVim
|
|
|
577
731
|
when "d"
|
|
578
732
|
@visual_pending = nil
|
|
579
733
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
|
|
734
|
+
when "="
|
|
735
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_indent"))
|
|
580
736
|
when "\""
|
|
581
737
|
start_register_pending
|
|
582
738
|
when "i", "a"
|
|
@@ -632,7 +788,7 @@ module RuVim
|
|
|
632
788
|
|
|
633
789
|
if id
|
|
634
790
|
clear_pending_key_timeout
|
|
635
|
-
count = @editor.pending_count
|
|
791
|
+
count = @editor.pending_count
|
|
636
792
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
637
793
|
else
|
|
638
794
|
clear_pending_key_timeout
|
|
@@ -744,7 +900,7 @@ module RuVim
|
|
|
744
900
|
up: "cursor.up",
|
|
745
901
|
down: "cursor.down"
|
|
746
902
|
}.fetch(key)
|
|
747
|
-
inv = CommandInvocation.new(id:, count: @editor.pending_count
|
|
903
|
+
inv = CommandInvocation.new(id:, count: @editor.pending_count)
|
|
748
904
|
@dispatcher.dispatch(@editor, inv)
|
|
749
905
|
@editor.pending_count = nil
|
|
750
906
|
@pending_keys = []
|
|
@@ -754,7 +910,7 @@ module RuVim
|
|
|
754
910
|
id = (key == :pageup ? "cursor.page_up" : "cursor.page_down")
|
|
755
911
|
inv = CommandInvocation.new(
|
|
756
912
|
id: id,
|
|
757
|
-
count: @editor.pending_count
|
|
913
|
+
count: @editor.pending_count,
|
|
758
914
|
kwargs: { page_lines: current_page_step_lines }
|
|
759
915
|
)
|
|
760
916
|
@dispatcher.dispatch(@editor, inv)
|
|
@@ -789,6 +945,7 @@ module RuVim
|
|
|
789
945
|
when :ctrl_o then "<C-o>"
|
|
790
946
|
when :ctrl_w then "<C-w>"
|
|
791
947
|
when :ctrl_l then "<C-l>"
|
|
948
|
+
when :ctrl_c then "<C-c>"
|
|
792
949
|
when :left then "<Left>"
|
|
793
950
|
when :right then "<Right>"
|
|
794
951
|
when :up then "<Up>"
|
|
@@ -797,6 +954,10 @@ module RuVim
|
|
|
797
954
|
when :end then "<End>"
|
|
798
955
|
when :pageup then "<PageUp>"
|
|
799
956
|
when :pagedown then "<PageDown>"
|
|
957
|
+
when :shift_up then "<S-Up>"
|
|
958
|
+
when :shift_down then "<S-Down>"
|
|
959
|
+
when :shift_left then "<S-Left>"
|
|
960
|
+
when :shift_right then "<S-Right>"
|
|
800
961
|
else nil
|
|
801
962
|
end
|
|
802
963
|
end
|
|
@@ -812,8 +973,49 @@ module RuVim
|
|
|
812
973
|
)
|
|
813
974
|
end
|
|
814
975
|
|
|
976
|
+
# Rich mode: delegates to normal mode key handling but blocks mutating operations.
|
|
977
|
+
RICH_MODE_BLOCKED_COMMANDS = %w[
|
|
978
|
+
mode.insert mode.append mode.append_line_end mode.insert_nonblank
|
|
979
|
+
mode.open_below mode.open_above
|
|
980
|
+
buffer.delete_char buffer.delete_line buffer.delete_motion
|
|
981
|
+
buffer.change_motion buffer.change_line
|
|
982
|
+
buffer.paste_after buffer.paste_before
|
|
983
|
+
buffer.replace_char
|
|
984
|
+
buffer.visual_delete
|
|
985
|
+
].freeze
|
|
986
|
+
|
|
987
|
+
def handle_hit_enter_key(key)
|
|
988
|
+
token = normalize_key_token(key)
|
|
989
|
+
case token
|
|
990
|
+
when ":"
|
|
991
|
+
@editor.exit_hit_enter_mode
|
|
992
|
+
@editor.enter_command_line_mode(":")
|
|
993
|
+
when "/", "?"
|
|
994
|
+
@editor.exit_hit_enter_mode
|
|
995
|
+
@editor.enter_command_line_mode(token)
|
|
996
|
+
else
|
|
997
|
+
@editor.exit_hit_enter_mode
|
|
998
|
+
end
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def handle_rich_key(key)
|
|
1002
|
+
token = normalize_key_token(key)
|
|
1003
|
+
if token == "\e"
|
|
1004
|
+
RuVim::RichView.close!(@editor)
|
|
1005
|
+
return
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
handle_normal_key(key)
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def rich_mode_block_command?(command_id)
|
|
1012
|
+
RICH_MODE_BLOCKED_COMMANDS.include?(command_id.to_s)
|
|
1013
|
+
end
|
|
1014
|
+
|
|
815
1015
|
def handle_ctrl_c
|
|
816
1016
|
case @editor.mode
|
|
1017
|
+
when :hit_enter
|
|
1018
|
+
@editor.exit_hit_enter_mode
|
|
817
1019
|
when :insert
|
|
818
1020
|
finish_insert_change_group
|
|
819
1021
|
finish_dot_change_capture
|
|
@@ -832,6 +1034,18 @@ module RuVim
|
|
|
832
1034
|
@jump_pending = nil
|
|
833
1035
|
clear_pending_key_timeout
|
|
834
1036
|
@editor.enter_normal_mode
|
|
1037
|
+
when :rich
|
|
1038
|
+
clear_pending_key_timeout
|
|
1039
|
+
@editor.pending_count = nil
|
|
1040
|
+
@pending_keys = []
|
|
1041
|
+
@operator_pending = nil
|
|
1042
|
+
@replace_pending = nil
|
|
1043
|
+
@register_pending = false
|
|
1044
|
+
@mark_pending = false
|
|
1045
|
+
@jump_pending = nil
|
|
1046
|
+
@macro_record_pending = false
|
|
1047
|
+
@macro_play_pending = false
|
|
1048
|
+
RuVim::RichView.close!(@editor)
|
|
835
1049
|
else
|
|
836
1050
|
clear_pending_key_timeout
|
|
837
1051
|
@editor.pending_count = nil
|
|
@@ -847,6 +1061,28 @@ module RuVim
|
|
|
847
1061
|
end
|
|
848
1062
|
end
|
|
849
1063
|
|
|
1064
|
+
def handle_normal_ctrl_c
|
|
1065
|
+
clear_pending_key_timeout
|
|
1066
|
+
@editor.pending_count = nil
|
|
1067
|
+
@pending_keys = []
|
|
1068
|
+
@operator_pending = nil
|
|
1069
|
+
@replace_pending = nil
|
|
1070
|
+
@register_pending = false
|
|
1071
|
+
@mark_pending = false
|
|
1072
|
+
@jump_pending = nil
|
|
1073
|
+
@macro_record_pending = false
|
|
1074
|
+
@macro_play_pending = false
|
|
1075
|
+
@editor.clear_message
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def suspend_to_shell
|
|
1079
|
+
@terminal.suspend_for_tstp
|
|
1080
|
+
@screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
|
|
1081
|
+
@needs_redraw = true
|
|
1082
|
+
rescue StandardError => e
|
|
1083
|
+
@editor.echo_error("suspend failed: #{e.message}")
|
|
1084
|
+
end
|
|
1085
|
+
|
|
850
1086
|
def finish_insert_change_group
|
|
851
1087
|
@editor.current_buffer.end_change_group
|
|
852
1088
|
end
|
|
@@ -871,7 +1107,7 @@ module RuVim
|
|
|
871
1107
|
end
|
|
872
1108
|
|
|
873
1109
|
def start_operator_pending(name)
|
|
874
|
-
@operator_pending = { name:, count:
|
|
1110
|
+
@operator_pending = { name:, count: @editor.pending_count }
|
|
875
1111
|
@editor.pending_count = nil
|
|
876
1112
|
@pending_keys = []
|
|
877
1113
|
@editor.echo(name == :delete ? "d" : name.to_s)
|
|
@@ -946,6 +1182,14 @@ module RuVim
|
|
|
946
1182
|
@editor.echo("q")
|
|
947
1183
|
end
|
|
948
1184
|
|
|
1185
|
+
def toggle_macro_recording_or_start_pending
|
|
1186
|
+
if @editor.macro_recording?
|
|
1187
|
+
stop_macro_recording
|
|
1188
|
+
else
|
|
1189
|
+
start_macro_record_pending
|
|
1190
|
+
end
|
|
1191
|
+
end
|
|
1192
|
+
|
|
949
1193
|
def finish_macro_record_pending(token)
|
|
950
1194
|
@macro_record_pending = false
|
|
951
1195
|
if token == "\e"
|
|
@@ -993,7 +1237,7 @@ module RuVim
|
|
|
993
1237
|
return
|
|
994
1238
|
end
|
|
995
1239
|
|
|
996
|
-
count = @editor.pending_count
|
|
1240
|
+
count = @editor.pending_count
|
|
997
1241
|
@editor.pending_count = nil
|
|
998
1242
|
play_macro(name, count:)
|
|
999
1243
|
end
|
|
@@ -1015,7 +1259,7 @@ module RuVim
|
|
|
1015
1259
|
@last_macro_name = reg
|
|
1016
1260
|
@macro_play_stack << reg
|
|
1017
1261
|
@suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
|
|
1018
|
-
count.times do
|
|
1262
|
+
[count.to_i, 1].max.times do
|
|
1019
1263
|
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
1020
1264
|
end
|
|
1021
1265
|
@editor.echo("@#{reg}")
|
|
@@ -1086,6 +1330,18 @@ module RuVim
|
|
|
1086
1330
|
return
|
|
1087
1331
|
end
|
|
1088
1332
|
|
|
1333
|
+
if op[:name] == :indent && motion == "="
|
|
1334
|
+
inv = CommandInvocation.new(id: "buffer.indent_lines", count: op[:count])
|
|
1335
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1336
|
+
return
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
if op[:name] == :indent
|
|
1340
|
+
inv = CommandInvocation.new(id: "buffer.indent_motion", count: op[:count], kwargs: { motion: motion })
|
|
1341
|
+
@dispatcher.dispatch(@editor, inv)
|
|
1342
|
+
return
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1089
1345
|
if op[:name] == :change && motion == "c"
|
|
1090
1346
|
inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
|
|
1091
1347
|
@dispatcher.dispatch(@editor, inv)
|
|
@@ -1104,7 +1360,7 @@ module RuVim
|
|
|
1104
1360
|
end
|
|
1105
1361
|
|
|
1106
1362
|
def start_replace_pending
|
|
1107
|
-
@replace_pending = { count:
|
|
1363
|
+
@replace_pending = { count: @editor.pending_count }
|
|
1108
1364
|
@editor.pending_count = nil
|
|
1109
1365
|
@pending_keys = []
|
|
1110
1366
|
@editor.echo("r")
|
|
@@ -1145,9 +1401,9 @@ module RuVim
|
|
|
1145
1401
|
return if (@dot_replay_depth || 0).positive?
|
|
1146
1402
|
|
|
1147
1403
|
case invocation.id
|
|
1148
|
-
when "buffer.delete_char", "buffer.paste_after", "buffer.paste_before"
|
|
1404
|
+
when "buffer.delete_char", "buffer.delete_motion", "buffer.join_lines", "buffer.swapcase_char", "buffer.paste_after", "buffer.paste_before"
|
|
1149
1405
|
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"
|
|
1406
|
+
when "mode.insert", "mode.append", "mode.append_line_end", "mode.insert_nonblank", "mode.open_below", "mode.open_above", "buffer.substitute_char", "buffer.change_motion", "buffer.change_line"
|
|
1151
1407
|
begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
|
|
1152
1408
|
end
|
|
1153
1409
|
end
|
|
@@ -1192,7 +1448,7 @@ module RuVim
|
|
|
1192
1448
|
@find_pending = {
|
|
1193
1449
|
direction: (token == "f" || token == "t") ? :forward : :backward,
|
|
1194
1450
|
till: (token == "t" || token == "T"),
|
|
1195
|
-
count:
|
|
1451
|
+
count: @editor.pending_count
|
|
1196
1452
|
}
|
|
1197
1453
|
@editor.pending_count = nil
|
|
1198
1454
|
@pending_keys = []
|
|
@@ -1237,7 +1493,7 @@ module RuVim
|
|
|
1237
1493
|
else
|
|
1238
1494
|
last[:direction]
|
|
1239
1495
|
end
|
|
1240
|
-
count = @editor.pending_count
|
|
1496
|
+
count = @editor.pending_count
|
|
1241
1497
|
@editor.pending_count = nil
|
|
1242
1498
|
@pending_keys = []
|
|
1243
1499
|
moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
|
|
@@ -1251,7 +1507,7 @@ module RuVim
|
|
|
1251
1507
|
pos = win.cursor_x
|
|
1252
1508
|
target = nil
|
|
1253
1509
|
|
|
1254
|
-
count.times do
|
|
1510
|
+
[count.to_i, 1].max.times do
|
|
1255
1511
|
idx =
|
|
1256
1512
|
if direction == :forward
|
|
1257
1513
|
line.index(char, pos + 1)
|
|
@@ -1305,6 +1561,66 @@ module RuVim
|
|
|
1305
1561
|
@cmdline_history_index = nil
|
|
1306
1562
|
end
|
|
1307
1563
|
|
|
1564
|
+
def load_command_line_history!
|
|
1565
|
+
path = command_line_history_file_path
|
|
1566
|
+
return unless path
|
|
1567
|
+
return unless File.file?(path)
|
|
1568
|
+
|
|
1569
|
+
raw = File.read(path)
|
|
1570
|
+
data = JSON.parse(raw)
|
|
1571
|
+
return unless data.is_a?(Hash)
|
|
1572
|
+
|
|
1573
|
+
loaded = Hash.new { |h, k| h[k] = [] }
|
|
1574
|
+
data.each do |prefix, items|
|
|
1575
|
+
key = prefix.to_s
|
|
1576
|
+
next unless [":", "/", "?"].include?(key)
|
|
1577
|
+
next unless items.is_a?(Array)
|
|
1578
|
+
|
|
1579
|
+
hist = loaded[key]
|
|
1580
|
+
items.each do |item|
|
|
1581
|
+
text = item.to_s
|
|
1582
|
+
next if text.empty?
|
|
1583
|
+
|
|
1584
|
+
hist.delete(text)
|
|
1585
|
+
hist << text
|
|
1586
|
+
end
|
|
1587
|
+
hist.shift while hist.length > 100
|
|
1588
|
+
end
|
|
1589
|
+
@cmdline_history = loaded
|
|
1590
|
+
rescue StandardError => e
|
|
1591
|
+
verbose_log(1, "history load error: #{e.message}")
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
def save_command_line_history!
|
|
1595
|
+
path = command_line_history_file_path
|
|
1596
|
+
return unless path
|
|
1597
|
+
|
|
1598
|
+
payload = {
|
|
1599
|
+
":" => Array(@cmdline_history[":"]).map(&:to_s).last(100),
|
|
1600
|
+
"/" => Array(@cmdline_history["/"]).map(&:to_s).last(100),
|
|
1601
|
+
"?" => Array(@cmdline_history["?"]).map(&:to_s).last(100)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
1605
|
+
tmp = "#{path}.tmp"
|
|
1606
|
+
File.write(tmp, JSON.pretty_generate(payload) + "\n")
|
|
1607
|
+
File.rename(tmp, path)
|
|
1608
|
+
rescue StandardError => e
|
|
1609
|
+
verbose_log(1, "history save error: #{e.message}")
|
|
1610
|
+
end
|
|
1611
|
+
|
|
1612
|
+
def command_line_history_file_path
|
|
1613
|
+
xdg_state_home = ENV["XDG_STATE_HOME"].to_s
|
|
1614
|
+
if !xdg_state_home.empty?
|
|
1615
|
+
return File.join(xdg_state_home, "ruvim", "history.json")
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
home = ENV["HOME"].to_s
|
|
1619
|
+
return nil if home.empty?
|
|
1620
|
+
|
|
1621
|
+
File.join(home, ".ruvim", "history.json")
|
|
1622
|
+
end
|
|
1623
|
+
|
|
1308
1624
|
def command_line_history_move(delta)
|
|
1309
1625
|
cmd = @editor.command_line
|
|
1310
1626
|
hist = @cmdline_history[cmd.prefix]
|
|
@@ -1333,7 +1649,7 @@ module RuVim
|
|
|
1333
1649
|
ctx = ex_completion_context(cmd)
|
|
1334
1650
|
return unless ctx
|
|
1335
1651
|
|
|
1336
|
-
matches = ex_completion_candidates(ctx)
|
|
1652
|
+
matches = reusable_command_line_completion_matches(cmd, ctx) || ex_completion_candidates(ctx)
|
|
1337
1653
|
case matches.length
|
|
1338
1654
|
when 0
|
|
1339
1655
|
clear_command_line_completion
|
|
@@ -1347,6 +1663,29 @@ module RuVim
|
|
|
1347
1663
|
update_incsearch_preview_if_needed
|
|
1348
1664
|
end
|
|
1349
1665
|
|
|
1666
|
+
def reusable_command_line_completion_matches(cmd, ctx)
|
|
1667
|
+
state = @cmdline_completion
|
|
1668
|
+
return nil unless state
|
|
1669
|
+
return nil unless state[:prefix] == cmd.prefix
|
|
1670
|
+
return nil unless state[:kind] == ctx[:kind]
|
|
1671
|
+
return nil unless state[:command] == ctx[:command]
|
|
1672
|
+
return nil unless state[:arg_index] == ctx[:arg_index]
|
|
1673
|
+
return nil unless state[:token_start] == ctx[:token_start]
|
|
1674
|
+
|
|
1675
|
+
before_text = cmd.text[0...ctx[:token_start]].to_s
|
|
1676
|
+
after_text = cmd.text[ctx[:token_end]..].to_s
|
|
1677
|
+
return nil unless state[:before_text] == before_text
|
|
1678
|
+
return nil unless state[:after_text] == after_text
|
|
1679
|
+
|
|
1680
|
+
matches = Array(state[:matches]).map(&:to_s)
|
|
1681
|
+
return nil if matches.empty?
|
|
1682
|
+
|
|
1683
|
+
current_token = cmd.text[ctx[:token_start]...ctx[:token_end]].to_s
|
|
1684
|
+
return nil unless current_token.empty? || matches.include?(current_token) || common_prefix(matches).start_with?(current_token) || current_token.start_with?(common_prefix(matches))
|
|
1685
|
+
|
|
1686
|
+
matches
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1350
1689
|
def clear_command_line_completion
|
|
1351
1690
|
@cmdline_completion = nil
|
|
1352
1691
|
end
|
|
@@ -1419,13 +1758,56 @@ module RuVim
|
|
|
1419
1758
|
def show_command_line_completion_menu(matches, selected:, force:)
|
|
1420
1759
|
return unless force || @editor.effective_option("wildmenu")
|
|
1421
1760
|
|
|
1422
|
-
|
|
1423
|
-
items = matches.first(limit).each_with_index.map do |m, i|
|
|
1761
|
+
items = matches.each_with_index.map do |m, i|
|
|
1424
1762
|
idx = i
|
|
1425
1763
|
idx == selected ? "[#{m}]" : m
|
|
1426
1764
|
end
|
|
1427
|
-
items
|
|
1428
|
-
|
|
1765
|
+
@editor.echo(compose_command_line_completion_menu(items))
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
def compose_command_line_completion_menu(items)
|
|
1769
|
+
parts = Array(items).map(&:to_s)
|
|
1770
|
+
return "" if parts.empty?
|
|
1771
|
+
|
|
1772
|
+
width = command_line_completion_menu_width
|
|
1773
|
+
width = [width.to_i, 1].max
|
|
1774
|
+
out = +""
|
|
1775
|
+
shown = 0
|
|
1776
|
+
|
|
1777
|
+
parts.each_with_index do |item, idx|
|
|
1778
|
+
token = shown.zero? ? item : " #{item}"
|
|
1779
|
+
if out.empty? && token.length > width
|
|
1780
|
+
out = token[0, width]
|
|
1781
|
+
shown = 1
|
|
1782
|
+
break
|
|
1783
|
+
end
|
|
1784
|
+
break if out.length + token.length > width
|
|
1785
|
+
|
|
1786
|
+
out << token
|
|
1787
|
+
shown = idx + 1
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
if shown < parts.length
|
|
1791
|
+
ellipsis = (out.empty? ? "..." : " ...")
|
|
1792
|
+
if out.length + ellipsis.length <= width
|
|
1793
|
+
out << ellipsis
|
|
1794
|
+
elsif width >= 3
|
|
1795
|
+
out = out[0, width - 3] + "..."
|
|
1796
|
+
else
|
|
1797
|
+
out = "." * width
|
|
1798
|
+
end
|
|
1799
|
+
end
|
|
1800
|
+
|
|
1801
|
+
out
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
def command_line_completion_menu_width
|
|
1805
|
+
return 80 unless defined?(@terminal) && @terminal && @terminal.respond_to?(:winsize)
|
|
1806
|
+
|
|
1807
|
+
_rows, cols = @terminal.winsize
|
|
1808
|
+
[cols.to_i, 1].max
|
|
1809
|
+
rescue StandardError
|
|
1810
|
+
80
|
|
1429
1811
|
end
|
|
1430
1812
|
|
|
1431
1813
|
def common_prefix(strings)
|
|
@@ -1473,7 +1855,11 @@ module RuVim
|
|
|
1473
1855
|
indent = prev[/\A[ \t]*/].to_s
|
|
1474
1856
|
if @editor.effective_option("smartindent", window: win, buffer: buf)
|
|
1475
1857
|
trimmed = prev.rstrip
|
|
1476
|
-
|
|
1858
|
+
needs_indent = trimmed.end_with?("{", "[", "(")
|
|
1859
|
+
if !needs_indent
|
|
1860
|
+
needs_indent = buf.lang_module.indent_trigger?(trimmed)
|
|
1861
|
+
end
|
|
1862
|
+
if needs_indent
|
|
1477
1863
|
sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
|
|
1478
1864
|
sw = effective_tabstop(win, buf) if sw <= 0
|
|
1479
1865
|
sw = 2 if sw <= 0
|
|
@@ -1495,6 +1881,34 @@ module RuVim
|
|
|
1495
1881
|
@editor.echo_temporary("match", duration_seconds: mt * 0.1)
|
|
1496
1882
|
end
|
|
1497
1883
|
|
|
1884
|
+
def maybe_dedent_after_insert(key)
|
|
1885
|
+
return unless @editor.effective_option("smartindent", window: @editor.current_window, buffer: @editor.current_buffer)
|
|
1886
|
+
|
|
1887
|
+
buf = @editor.current_buffer
|
|
1888
|
+
lang_mod = buf.lang_module
|
|
1889
|
+
|
|
1890
|
+
pattern = lang_mod.dedent_trigger(key)
|
|
1891
|
+
return unless pattern
|
|
1892
|
+
|
|
1893
|
+
row = @editor.current_window.cursor_y
|
|
1894
|
+
line = buf.line_at(row)
|
|
1895
|
+
m = line.match(pattern)
|
|
1896
|
+
return unless m
|
|
1897
|
+
|
|
1898
|
+
sw = @editor.effective_option("shiftwidth", buffer: buf).to_i
|
|
1899
|
+
sw = 2 if sw <= 0
|
|
1900
|
+
target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
|
|
1901
|
+
return unless target_indent
|
|
1902
|
+
|
|
1903
|
+
current_indent = m[1].length
|
|
1904
|
+
return if current_indent == target_indent
|
|
1905
|
+
|
|
1906
|
+
stripped = line.to_s.strip
|
|
1907
|
+
buf.delete_span(row, 0, row, current_indent) if current_indent > 0
|
|
1908
|
+
buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
|
|
1909
|
+
@editor.current_window.cursor_x = target_indent + stripped.length
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1498
1912
|
def clear_expired_transient_message_if_any
|
|
1499
1913
|
@needs_redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
|
|
1500
1914
|
end
|
|
@@ -1906,15 +2320,27 @@ module RuVim
|
|
|
1906
2320
|
else
|
|
1907
2321
|
File.dirname(input)
|
|
1908
2322
|
end
|
|
1909
|
-
base_dir = "." if base_dir == "."
|
|
1910
2323
|
partial = input.end_with?("/") ? "" : File.basename(input)
|
|
1911
|
-
pattern =
|
|
1912
|
-
|
|
2324
|
+
pattern =
|
|
2325
|
+
if input.empty?
|
|
2326
|
+
"*"
|
|
2327
|
+
elsif base_dir == "."
|
|
2328
|
+
"#{partial}*"
|
|
2329
|
+
else
|
|
2330
|
+
File.join(base_dir, "#{partial}*")
|
|
2331
|
+
end
|
|
2332
|
+
partial_starts_with_dot = partial.start_with?(".")
|
|
2333
|
+
entries = Dir.glob(pattern, File::FNM_DOTMATCH).filter_map do |p|
|
|
1913
2334
|
next if [".", ".."].include?(File.basename(p))
|
|
1914
2335
|
next unless p.start_with?(input) || input.empty?
|
|
1915
2336
|
next if wildignore_path?(p)
|
|
1916
2337
|
File.directory?(p) ? "#{p}/" : p
|
|
1917
2338
|
end
|
|
2339
|
+
entries.sort_by do |p|
|
|
2340
|
+
base = File.basename(p.to_s.sub(%r{/\z}, ""))
|
|
2341
|
+
hidden_rank = (!partial_starts_with_dot && base.start_with?(".")) ? 1 : 0
|
|
2342
|
+
[hidden_rank, p]
|
|
2343
|
+
end
|
|
1918
2344
|
rescue StandardError
|
|
1919
2345
|
[]
|
|
1920
2346
|
end
|
|
@@ -2096,6 +2522,13 @@ module RuVim
|
|
|
2096
2522
|
list = Array(paths).compact
|
|
2097
2523
|
return if list.empty?
|
|
2098
2524
|
|
|
2525
|
+
# Remove the bootstrap empty buffer and reset the ID counter
|
|
2526
|
+
# so the first file gets buffer id 1 (Vim-like behavior).
|
|
2527
|
+
evict_bootstrap_buffer!
|
|
2528
|
+
|
|
2529
|
+
# Initialize arglist with all paths
|
|
2530
|
+
@editor.set_arglist(list)
|
|
2531
|
+
|
|
2099
2532
|
first, *rest = list
|
|
2100
2533
|
@editor.open_path(first)
|
|
2101
2534
|
apply_startup_readonly! if @startup_readonly
|
|
@@ -2103,20 +2536,38 @@ module RuVim
|
|
|
2103
2536
|
|
|
2104
2537
|
case @startup_open_layout
|
|
2105
2538
|
when :horizontal
|
|
2539
|
+
first_win_id = @editor.current_window_id
|
|
2106
2540
|
rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
|
|
2541
|
+
@editor.focus_window(first_win_id)
|
|
2107
2542
|
when :vertical
|
|
2543
|
+
first_win_id = @editor.current_window_id
|
|
2108
2544
|
rest.each { |p| open_path_in_split!(p, layout: :vertical) }
|
|
2545
|
+
@editor.focus_window(first_win_id)
|
|
2109
2546
|
when :tab
|
|
2110
2547
|
rest.each { |p| open_path_in_tab!(p) }
|
|
2548
|
+
@editor.tabnext(-(@editor.tabpage_count - 1))
|
|
2111
2549
|
else
|
|
2112
|
-
#
|
|
2550
|
+
# Load remaining files as buffers (Vim-like behavior).
|
|
2551
|
+
rest.each { |p| @editor.add_buffer_from_file(p) }
|
|
2113
2552
|
end
|
|
2114
2553
|
end
|
|
2115
2554
|
|
|
2555
|
+
# Remove the bootstrap empty buffer before opening real files,
|
|
2556
|
+
# resetting the buffer ID counter so the first file gets id 1.
|
|
2557
|
+
def evict_bootstrap_buffer!
|
|
2558
|
+
bid = @editor.buffer_ids.find do |id|
|
|
2559
|
+
b = @editor.buffers[id]
|
|
2560
|
+
b.path.nil? && !b.modified? && b.line_count <= 1 && b.kind == :file
|
|
2561
|
+
end
|
|
2562
|
+
return unless bid
|
|
2563
|
+
|
|
2564
|
+
@editor.buffers.delete(bid)
|
|
2565
|
+
@editor.instance_variable_set(:@next_buffer_id, 1)
|
|
2566
|
+
end
|
|
2567
|
+
|
|
2116
2568
|
def open_path_in_split!(path, layout:)
|
|
2117
2569
|
@editor.split_current_window(layout:)
|
|
2118
|
-
|
|
2119
|
-
@editor.switch_to_buffer(buf.id)
|
|
2570
|
+
@editor.open_path(path)
|
|
2120
2571
|
apply_startup_readonly! if @startup_readonly
|
|
2121
2572
|
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
2122
2573
|
end
|
|
@@ -2127,6 +2578,368 @@ module RuVim
|
|
|
2127
2578
|
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
2128
2579
|
end
|
|
2129
2580
|
|
|
2581
|
+
def open_path_with_large_file_support(path)
|
|
2582
|
+
return @editor.open_path_sync(path) unless should_open_path_async?(path)
|
|
2583
|
+
return @editor.open_path_sync(path) unless can_start_async_file_load?
|
|
2584
|
+
|
|
2585
|
+
open_path_asynchronously!(path)
|
|
2586
|
+
end
|
|
2587
|
+
|
|
2588
|
+
def should_open_path_async?(path)
|
|
2589
|
+
p = path.to_s
|
|
2590
|
+
return false if p.empty?
|
|
2591
|
+
return false unless File.file?(p)
|
|
2592
|
+
|
|
2593
|
+
File.size(p) >= large_file_async_threshold_bytes
|
|
2594
|
+
rescue StandardError
|
|
2595
|
+
false
|
|
2596
|
+
end
|
|
2597
|
+
|
|
2598
|
+
def can_start_async_file_load?
|
|
2599
|
+
@async_file_loads.empty?
|
|
2600
|
+
end
|
|
2601
|
+
|
|
2602
|
+
def large_file_async_threshold_bytes
|
|
2603
|
+
raw = ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"]
|
|
2604
|
+
n = raw.to_i if raw
|
|
2605
|
+
return n if n && n.positive?
|
|
2606
|
+
|
|
2607
|
+
LARGE_FILE_ASYNC_THRESHOLD_BYTES
|
|
2608
|
+
end
|
|
2609
|
+
|
|
2610
|
+
def open_path_asynchronously!(path)
|
|
2611
|
+
file_size = File.size(path)
|
|
2612
|
+
buf = @editor.add_empty_buffer(path: path)
|
|
2613
|
+
@editor.switch_to_buffer(buf.id)
|
|
2614
|
+
buf.loading_state = :live
|
|
2615
|
+
buf.modified = false
|
|
2616
|
+
|
|
2617
|
+
ensure_stream_event_queue!
|
|
2618
|
+
io = File.open(path, "rb")
|
|
2619
|
+
state = { path: path, io: io, thread: nil, ended_with_newline: false }
|
|
2620
|
+
staged_prefix_bytes = async_file_staged_prefix_bytes
|
|
2621
|
+
staged_mode = file_size > staged_prefix_bytes
|
|
2622
|
+
if staged_mode
|
|
2623
|
+
prefix = io.read(staged_prefix_bytes) || "".b
|
|
2624
|
+
unless prefix.empty?
|
|
2625
|
+
buf.append_stream_text!(Buffer.decode_text(prefix))
|
|
2626
|
+
state[:ended_with_newline] = prefix.end_with?("\n")
|
|
2627
|
+
end
|
|
2628
|
+
end
|
|
2629
|
+
|
|
2630
|
+
if io.eof?
|
|
2631
|
+
buf.finalize_async_file_load!(ended_with_newline: state[:ended_with_newline])
|
|
2632
|
+
buf.loading_state = :closed
|
|
2633
|
+
io.close unless io.closed?
|
|
2634
|
+
return buf
|
|
2635
|
+
end
|
|
2636
|
+
|
|
2637
|
+
@async_file_loads[buf.id] = state
|
|
2638
|
+
state[:thread] = start_async_file_loader_thread(buf.id, io, bulk_once: staged_mode)
|
|
2639
|
+
|
|
2640
|
+
size_mb = file_size.fdiv(1024 * 1024)
|
|
2641
|
+
if staged_mode
|
|
2642
|
+
@editor.echo(format("\"%s\" loading... (showing first %.0fMB of %.1fMB)", path, staged_prefix_bytes.fdiv(1024 * 1024), size_mb))
|
|
2643
|
+
else
|
|
2644
|
+
@editor.echo(format("\"%s\" loading... (%.1fMB)", path, size_mb))
|
|
2645
|
+
end
|
|
2646
|
+
buf
|
|
2647
|
+
rescue StandardError
|
|
2648
|
+
@async_file_loads.delete(buf.id) if buf
|
|
2649
|
+
raise
|
|
2650
|
+
end
|
|
2651
|
+
|
|
2652
|
+
def async_file_staged_prefix_bytes
|
|
2653
|
+
raw = ENV["RUVIM_ASYNC_FILE_PREFIX_BYTES"]
|
|
2654
|
+
n = raw.to_i if raw
|
|
2655
|
+
return n if n && n.positive?
|
|
2656
|
+
|
|
2657
|
+
LARGE_FILE_STAGED_PREFIX_BYTES
|
|
2658
|
+
end
|
|
2659
|
+
|
|
2660
|
+
def start_async_file_loader_thread(buffer_id, io, bulk_once: false)
|
|
2661
|
+
Thread.new do
|
|
2662
|
+
if bulk_once
|
|
2663
|
+
rest = io.read || "".b
|
|
2664
|
+
unless rest.empty?
|
|
2665
|
+
@stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: Buffer.decode_text(rest) }
|
|
2666
|
+
notify_signal_wakeup
|
|
2667
|
+
end
|
|
2668
|
+
@stream_event_queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: rest.end_with?("\n") }
|
|
2669
|
+
notify_signal_wakeup
|
|
2670
|
+
next
|
|
2671
|
+
end
|
|
2672
|
+
|
|
2673
|
+
ended_with_newline = false
|
|
2674
|
+
pending_text = +""
|
|
2675
|
+
loop do
|
|
2676
|
+
chunk = io.readpartial(ASYNC_FILE_READ_CHUNK_BYTES)
|
|
2677
|
+
next if chunk.nil? || chunk.empty?
|
|
2678
|
+
|
|
2679
|
+
ended_with_newline = chunk.end_with?("\n")
|
|
2680
|
+
pending_text << Buffer.decode_text(chunk)
|
|
2681
|
+
next if pending_text.bytesize < ASYNC_FILE_EVENT_FLUSH_BYTES
|
|
2682
|
+
|
|
2683
|
+
@stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: pending_text }
|
|
2684
|
+
pending_text = +""
|
|
2685
|
+
notify_signal_wakeup
|
|
2686
|
+
end
|
|
2687
|
+
rescue EOFError
|
|
2688
|
+
unless pending_text.empty?
|
|
2689
|
+
@stream_event_queue << { type: :file_data, buffer_id: buffer_id, data: pending_text }
|
|
2690
|
+
notify_signal_wakeup
|
|
2691
|
+
end
|
|
2692
|
+
@stream_event_queue << { type: :file_eof, buffer_id: buffer_id, ended_with_newline: ended_with_newline }
|
|
2693
|
+
notify_signal_wakeup
|
|
2694
|
+
rescue StandardError => e
|
|
2695
|
+
@stream_event_queue << { type: :file_error, buffer_id: buffer_id, error: e.message.to_s }
|
|
2696
|
+
notify_signal_wakeup
|
|
2697
|
+
ensure
|
|
2698
|
+
begin
|
|
2699
|
+
io.close unless io.closed?
|
|
2700
|
+
rescue StandardError
|
|
2701
|
+
nil
|
|
2702
|
+
end
|
|
2703
|
+
end
|
|
2704
|
+
end
|
|
2705
|
+
|
|
2706
|
+
def prepare_stdin_stream_buffer!
|
|
2707
|
+
buf = @editor.current_buffer
|
|
2708
|
+
if buf.intro_buffer?
|
|
2709
|
+
@editor.materialize_intro_buffer!
|
|
2710
|
+
buf = @editor.current_buffer
|
|
2711
|
+
end
|
|
2712
|
+
|
|
2713
|
+
buf.replace_all_lines!([""])
|
|
2714
|
+
buf.configure_special!(kind: :stream, name: "[stdin]", readonly: true, modifiable: false)
|
|
2715
|
+
buf.modified = false
|
|
2716
|
+
buf.stream_state = :live
|
|
2717
|
+
buf.options["filetype"] = "text"
|
|
2718
|
+
@stream_stop_requested = false
|
|
2719
|
+
ensure_stream_event_queue!
|
|
2720
|
+
@stream_buffer_id = buf.id
|
|
2721
|
+
move_window_to_stream_end!(@editor.current_window, buf)
|
|
2722
|
+
@editor.echo("[stdin] follow")
|
|
2723
|
+
end
|
|
2724
|
+
|
|
2725
|
+
def stdin_stream_stop_command
|
|
2726
|
+
return if stop_stdin_stream!
|
|
2727
|
+
|
|
2728
|
+
handle_normal_ctrl_c
|
|
2729
|
+
end
|
|
2730
|
+
|
|
2731
|
+
def stop_stdin_stream!
|
|
2732
|
+
buf = @editor.buffers[@stream_buffer_id]
|
|
2733
|
+
return false unless buf&.kind == :stream
|
|
2734
|
+
return false unless (buf.stream_state || :live) == :live
|
|
2735
|
+
|
|
2736
|
+
@stream_stop_requested = true
|
|
2737
|
+
io = @stdin_stream_source
|
|
2738
|
+
@stdin_stream_source = nil
|
|
2739
|
+
begin
|
|
2740
|
+
io.close if io && io.respond_to?(:close) && !(io.respond_to?(:closed?) && io.closed?)
|
|
2741
|
+
rescue StandardError
|
|
2742
|
+
nil
|
|
2743
|
+
end
|
|
2744
|
+
if @stream_reader_thread&.alive?
|
|
2745
|
+
@stream_reader_thread.kill
|
|
2746
|
+
@stream_reader_thread.join(0.05)
|
|
2747
|
+
end
|
|
2748
|
+
@stream_reader_thread = nil
|
|
2749
|
+
|
|
2750
|
+
buf.stream_state = :closed
|
|
2751
|
+
@editor.echo("[stdin] closed")
|
|
2752
|
+
notify_signal_wakeup
|
|
2753
|
+
true
|
|
2754
|
+
end
|
|
2755
|
+
|
|
2756
|
+
def start_stdin_stream_reader!
|
|
2757
|
+
return unless @stdin_stream_source
|
|
2758
|
+
ensure_stream_event_queue!
|
|
2759
|
+
return if @stream_reader_thread&.alive?
|
|
2760
|
+
|
|
2761
|
+
@stream_stop_requested = false
|
|
2762
|
+
io = @stdin_stream_source
|
|
2763
|
+
@stream_reader_thread = Thread.new do
|
|
2764
|
+
loop do
|
|
2765
|
+
chunk = io.readpartial(4096)
|
|
2766
|
+
next if chunk.nil? || chunk.empty?
|
|
2767
|
+
|
|
2768
|
+
@stream_event_queue << { type: :data, data: Buffer.decode_text(chunk) }
|
|
2769
|
+
notify_signal_wakeup
|
|
2770
|
+
end
|
|
2771
|
+
rescue EOFError
|
|
2772
|
+
unless @stream_stop_requested
|
|
2773
|
+
@stream_event_queue << { type: :eof }
|
|
2774
|
+
notify_signal_wakeup
|
|
2775
|
+
end
|
|
2776
|
+
rescue IOError => e
|
|
2777
|
+
unless @stream_stop_requested
|
|
2778
|
+
@stream_event_queue << { type: :error, error: e.message.to_s }
|
|
2779
|
+
notify_signal_wakeup
|
|
2780
|
+
end
|
|
2781
|
+
rescue StandardError => e
|
|
2782
|
+
unless @stream_stop_requested
|
|
2783
|
+
@stream_event_queue << { type: :error, error: e.message.to_s }
|
|
2784
|
+
notify_signal_wakeup
|
|
2785
|
+
end
|
|
2786
|
+
end
|
|
2787
|
+
end
|
|
2788
|
+
|
|
2789
|
+
def drain_stream_events!
|
|
2790
|
+
return false unless @stream_event_queue
|
|
2791
|
+
|
|
2792
|
+
changed = false
|
|
2793
|
+
loop do
|
|
2794
|
+
event = @stream_event_queue.pop(true)
|
|
2795
|
+
case event[:type]
|
|
2796
|
+
when :data
|
|
2797
|
+
changed = apply_stream_chunk!(event[:data]) || changed
|
|
2798
|
+
when :eof
|
|
2799
|
+
if (buf = @editor.buffers[@stream_buffer_id])
|
|
2800
|
+
buf.stream_state = :closed
|
|
2801
|
+
end
|
|
2802
|
+
@editor.echo("[stdin] EOF")
|
|
2803
|
+
changed = true
|
|
2804
|
+
when :error
|
|
2805
|
+
next if ignore_stream_shutdown_error?(event[:error])
|
|
2806
|
+
if (buf = @editor.buffers[@stream_buffer_id])
|
|
2807
|
+
buf.stream_state = :error
|
|
2808
|
+
end
|
|
2809
|
+
@editor.echo_error("[stdin] stream error: #{event[:error]}")
|
|
2810
|
+
changed = true
|
|
2811
|
+
when :file_data
|
|
2812
|
+
changed = apply_async_file_chunk!(event[:buffer_id], event[:data]) || changed
|
|
2813
|
+
when :file_eof
|
|
2814
|
+
changed = finish_async_file_load!(event[:buffer_id], ended_with_newline: event[:ended_with_newline]) || changed
|
|
2815
|
+
when :file_error
|
|
2816
|
+
changed = fail_async_file_load!(event[:buffer_id], event[:error]) || changed
|
|
2817
|
+
end
|
|
2818
|
+
end
|
|
2819
|
+
rescue ThreadError
|
|
2820
|
+
changed
|
|
2821
|
+
end
|
|
2822
|
+
|
|
2823
|
+
def apply_stream_chunk!(text)
|
|
2824
|
+
return false if text.to_s.empty?
|
|
2825
|
+
|
|
2826
|
+
buf = @editor.buffers[@stream_buffer_id]
|
|
2827
|
+
return false unless buf
|
|
2828
|
+
|
|
2829
|
+
follow_window_ids = @editor.windows.values.filter_map do |win|
|
|
2830
|
+
next unless win.buffer_id == buf.id
|
|
2831
|
+
next unless stream_window_following_end?(win, buf)
|
|
2832
|
+
|
|
2833
|
+
win.id
|
|
2834
|
+
end
|
|
2835
|
+
|
|
2836
|
+
buf.append_stream_text!(text)
|
|
2837
|
+
|
|
2838
|
+
follow_window_ids.each do |win_id|
|
|
2839
|
+
win = @editor.windows[win_id]
|
|
2840
|
+
move_window_to_stream_end!(win, buf) if win
|
|
2841
|
+
end
|
|
2842
|
+
|
|
2843
|
+
true
|
|
2844
|
+
end
|
|
2845
|
+
|
|
2846
|
+
def apply_async_file_chunk!(buffer_id, text)
|
|
2847
|
+
return false if text.to_s.empty?
|
|
2848
|
+
|
|
2849
|
+
buf = @editor.buffers[buffer_id]
|
|
2850
|
+
return false unless buf
|
|
2851
|
+
|
|
2852
|
+
buf.append_stream_text!(text)
|
|
2853
|
+
true
|
|
2854
|
+
end
|
|
2855
|
+
|
|
2856
|
+
def finish_async_file_load!(buffer_id, ended_with_newline:)
|
|
2857
|
+
@async_file_loads.delete(buffer_id)
|
|
2858
|
+
buf = @editor.buffers[buffer_id]
|
|
2859
|
+
return false unless buf
|
|
2860
|
+
|
|
2861
|
+
buf.finalize_async_file_load!(ended_with_newline: !!ended_with_newline)
|
|
2862
|
+
buf.loading_state = :closed
|
|
2863
|
+
true
|
|
2864
|
+
end
|
|
2865
|
+
|
|
2866
|
+
def fail_async_file_load!(buffer_id, error)
|
|
2867
|
+
state = @async_file_loads.delete(buffer_id)
|
|
2868
|
+
buf = @editor.buffers[buffer_id]
|
|
2869
|
+
if buf
|
|
2870
|
+
buf.loading_state = :error
|
|
2871
|
+
end
|
|
2872
|
+
@editor.echo_error("\"#{(state && state[:path]) || (buf && buf.display_name) || buffer_id}\" load error: #{error}")
|
|
2873
|
+
true
|
|
2874
|
+
end
|
|
2875
|
+
|
|
2876
|
+
def stream_window_following_end?(win, buf)
|
|
2877
|
+
return false unless win
|
|
2878
|
+
|
|
2879
|
+
last_row = buf.line_count - 1
|
|
2880
|
+
win.cursor_y >= last_row
|
|
2881
|
+
end
|
|
2882
|
+
|
|
2883
|
+
def move_window_to_stream_end!(win, buf)
|
|
2884
|
+
return unless win && buf
|
|
2885
|
+
|
|
2886
|
+
last_row = buf.line_count - 1
|
|
2887
|
+
win.cursor_y = last_row
|
|
2888
|
+
win.cursor_x = buf.line_length(last_row)
|
|
2889
|
+
win.clamp_to_buffer(buf)
|
|
2890
|
+
end
|
|
2891
|
+
|
|
2892
|
+
def shutdown_stream_reader!
|
|
2893
|
+
thread = @stream_reader_thread
|
|
2894
|
+
@stream_reader_thread = nil
|
|
2895
|
+
@stream_stop_requested = true
|
|
2896
|
+
return unless thread
|
|
2897
|
+
return unless thread.alive?
|
|
2898
|
+
|
|
2899
|
+
thread.kill
|
|
2900
|
+
thread.join(0.05)
|
|
2901
|
+
rescue StandardError
|
|
2902
|
+
nil
|
|
2903
|
+
end
|
|
2904
|
+
|
|
2905
|
+
def shutdown_async_file_loaders!
|
|
2906
|
+
loaders = @async_file_loads
|
|
2907
|
+
@async_file_loads = {}
|
|
2908
|
+
loaders.each_value do |state|
|
|
2909
|
+
io = state[:io]
|
|
2910
|
+
thread = state[:thread]
|
|
2911
|
+
begin
|
|
2912
|
+
io.close if io && !io.closed?
|
|
2913
|
+
rescue StandardError
|
|
2914
|
+
nil
|
|
2915
|
+
end
|
|
2916
|
+
next unless thread&.alive?
|
|
2917
|
+
|
|
2918
|
+
thread.kill
|
|
2919
|
+
thread.join(0.05)
|
|
2920
|
+
rescue StandardError
|
|
2921
|
+
nil
|
|
2922
|
+
end
|
|
2923
|
+
end
|
|
2924
|
+
|
|
2925
|
+
def shutdown_background_readers!
|
|
2926
|
+
shutdown_stream_reader!
|
|
2927
|
+
shutdown_async_file_loaders!
|
|
2928
|
+
end
|
|
2929
|
+
|
|
2930
|
+
def ignore_stream_shutdown_error?(message)
|
|
2931
|
+
buf = @editor.buffers[@stream_buffer_id]
|
|
2932
|
+
return false unless buf&.kind == :stream
|
|
2933
|
+
return false unless (buf.stream_state || :live) == :closed
|
|
2934
|
+
|
|
2935
|
+
msg = message.to_s.downcase
|
|
2936
|
+
msg.include?("stream closed") || msg.include?("closed in another thread")
|
|
2937
|
+
end
|
|
2938
|
+
|
|
2939
|
+
def ensure_stream_event_queue!
|
|
2940
|
+
@stream_event_queue ||= Queue.new
|
|
2941
|
+
end
|
|
2942
|
+
|
|
2130
2943
|
def move_cursor_to_line(line_number)
|
|
2131
2944
|
win = @editor.current_window
|
|
2132
2945
|
buf = @editor.current_buffer
|