ruvim 0.1.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 +29 -0
- data/docs/command.md +101 -0
- data/docs/config.md +203 -84
- data/docs/done.md +21 -0
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +195 -33
- data/docs/todo.md +183 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +94 -171
- data/lib/ruvim/app.rb +1543 -172
- 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 +21 -5
- data/lib/ruvim/context.rb +2 -7
- data/lib/ruvim/dispatcher.rb +153 -13
- data/lib/ruvim/display_width.rb +28 -2
- data/lib/ruvim/editor.rb +622 -69
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +1386 -114
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +52 -29
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +48 -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 +851 -119
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +28 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +37 -10
- data/lib/ruvim.rb +15 -0
- data/test/app_completion_test.rb +174 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +110 -2
- data/test/app_scenario_test.rb +998 -0
- data/test/app_startup_test.rb +197 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +438 -0
- data/test/display_width_test.rb +18 -0
- data/test/editor_register_test.rb +23 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +65 -14
- 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 +470 -0
- data/test/window_test.rb +26 -0
- metadata +37 -2
data/lib/ruvim/app.rb
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
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
|
|
32
|
+
@cmdline_completion = nil
|
|
33
|
+
@pending_key_deadline = nil
|
|
34
|
+
@pending_ambiguous_invocation = nil
|
|
35
|
+
@insert_start_location = nil
|
|
36
|
+
@incsearch_preview = nil
|
|
13
37
|
@needs_redraw = true
|
|
14
38
|
@clean_mode = clean
|
|
15
39
|
@skip_user_config = skip_user_config
|
|
@@ -28,6 +52,11 @@ module RuVim
|
|
|
28
52
|
@startup_open_layout = startup_open_layout
|
|
29
53
|
@startup_open_count = startup_open_count
|
|
30
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!
|
|
31
60
|
|
|
32
61
|
startup_mark("init.start")
|
|
33
62
|
register_builtins!
|
|
@@ -43,8 +72,10 @@ module RuVim
|
|
|
43
72
|
install_signal_handlers
|
|
44
73
|
startup_mark("signals.installed")
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
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?
|
|
48
79
|
verbose_log(1, "startup: intro")
|
|
49
80
|
@editor.show_intro_buffer_if_applicable!
|
|
50
81
|
else
|
|
@@ -59,25 +90,45 @@ module RuVim
|
|
|
59
90
|
verbose_log(1, "startup: run_startup_actions count=#{Array(startup_actions).length}")
|
|
60
91
|
run_startup_actions!(startup_actions)
|
|
61
92
|
startup_mark("startup_actions.done")
|
|
93
|
+
start_stdin_stream_reader! if @stream_buffer_id
|
|
62
94
|
write_startuptime_log!
|
|
63
95
|
end
|
|
64
96
|
|
|
65
97
|
def run
|
|
66
98
|
@terminal.with_ui do
|
|
67
99
|
loop do
|
|
100
|
+
@needs_redraw = true if drain_stream_events!
|
|
68
101
|
if @needs_redraw
|
|
69
102
|
@screen.render(@editor)
|
|
70
103
|
@needs_redraw = false
|
|
71
104
|
end
|
|
72
105
|
break unless @editor.running?
|
|
73
106
|
|
|
74
|
-
key = @input.read_key(
|
|
75
|
-
|
|
107
|
+
key = @input.read_key(
|
|
108
|
+
wakeup_ios: [@signal_r],
|
|
109
|
+
timeout: loop_timeout_seconds,
|
|
110
|
+
esc_timeout: escape_sequence_timeout_seconds
|
|
111
|
+
)
|
|
112
|
+
if key.nil?
|
|
113
|
+
handle_pending_key_timeout if pending_key_timeout_expired?
|
|
114
|
+
clear_expired_transient_message_if_any
|
|
115
|
+
next
|
|
116
|
+
end
|
|
76
117
|
|
|
77
118
|
handle_key(key)
|
|
78
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
|
|
79
127
|
end
|
|
80
128
|
end
|
|
129
|
+
ensure
|
|
130
|
+
shutdown_background_readers!
|
|
131
|
+
save_command_line_history!
|
|
81
132
|
end
|
|
82
133
|
|
|
83
134
|
def run_startup_actions!(actions, log_prefix: "startup")
|
|
@@ -89,6 +140,59 @@ module RuVim
|
|
|
89
140
|
|
|
90
141
|
private
|
|
91
142
|
|
|
143
|
+
def pending_key_timeout_seconds
|
|
144
|
+
return nil unless @pending_key_deadline
|
|
145
|
+
|
|
146
|
+
[@pending_key_deadline - monotonic_now, 0.0].max
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def loop_timeout_seconds
|
|
150
|
+
now = monotonic_now
|
|
151
|
+
timeouts = []
|
|
152
|
+
if @pending_key_deadline
|
|
153
|
+
timeouts << [@pending_key_deadline - now, 0.0].max
|
|
154
|
+
end
|
|
155
|
+
if (msg_to = @editor.transient_message_timeout_seconds(now:))
|
|
156
|
+
timeouts << msg_to
|
|
157
|
+
end
|
|
158
|
+
timeouts.min
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def pending_key_timeout_expired?
|
|
162
|
+
@pending_key_deadline && monotonic_now >= @pending_key_deadline
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def escape_sequence_timeout_seconds
|
|
166
|
+
ms = @editor.global_options["ttimeoutlen"].to_i
|
|
167
|
+
ms = 50 if ms <= 0
|
|
168
|
+
ms / 1000.0
|
|
169
|
+
rescue StandardError
|
|
170
|
+
0.005
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def arm_pending_key_timeout
|
|
174
|
+
ms = @editor.global_options["timeoutlen"].to_i
|
|
175
|
+
ms = 1000 if ms <= 0
|
|
176
|
+
@pending_key_deadline = monotonic_now + (ms / 1000.0)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def clear_pending_key_timeout
|
|
180
|
+
@pending_key_deadline = nil
|
|
181
|
+
@pending_ambiguous_invocation = nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def handle_pending_key_timeout
|
|
185
|
+
inv = @pending_ambiguous_invocation
|
|
186
|
+
clear_pending_key_timeout
|
|
187
|
+
if inv
|
|
188
|
+
@dispatcher.dispatch(@editor, dup_invocation(inv))
|
|
189
|
+
elsif @pending_keys && !@pending_keys.empty?
|
|
190
|
+
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
191
|
+
end
|
|
192
|
+
@editor.pending_count = nil
|
|
193
|
+
@pending_keys = []
|
|
194
|
+
end
|
|
195
|
+
|
|
92
196
|
def register_builtins!
|
|
93
197
|
cmd = CommandRegistry.instance
|
|
94
198
|
ex = ExCommandRegistry.instance
|
|
@@ -99,6 +203,17 @@ module RuVim
|
|
|
99
203
|
register_internal_unless(cmd, "cursor.down", call: :cursor_down, desc: "Move cursor down")
|
|
100
204
|
register_internal_unless(cmd, "cursor.page_up", call: :cursor_page_up, desc: "Move one page up")
|
|
101
205
|
register_internal_unless(cmd, "cursor.page_down", call: :cursor_page_down, desc: "Move one page down")
|
|
206
|
+
register_internal_unless(cmd, "window.scroll_up", call: :window_scroll_up, desc: "Scroll window up")
|
|
207
|
+
register_internal_unless(cmd, "window.scroll_down", call: :window_scroll_down, desc: "Scroll window down")
|
|
208
|
+
register_internal_unless(cmd, "cursor.page_up.default", call: :cursor_page_up_default, desc: "Move one page up (view-sized)")
|
|
209
|
+
register_internal_unless(cmd, "cursor.page_down.default", call: :cursor_page_down_default, desc: "Move one page down (view-sized)")
|
|
210
|
+
register_internal_unless(cmd, "cursor.page_up.half", call: :cursor_page_up_half, desc: "Move half page up")
|
|
211
|
+
register_internal_unless(cmd, "cursor.page_down.half", call: :cursor_page_down_half, desc: "Move half page down")
|
|
212
|
+
register_internal_unless(cmd, "window.scroll_up.line", call: :window_scroll_up_line, desc: "Scroll window up one line")
|
|
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")
|
|
102
217
|
register_internal_unless(cmd, "cursor.line_start", call: :cursor_line_start, desc: "Move to column 1")
|
|
103
218
|
register_internal_unless(cmd, "cursor.line_end", call: :cursor_line_end, desc: "Move to end of line")
|
|
104
219
|
register_internal_unless(cmd, "cursor.first_nonblank", call: :cursor_first_nonblank, desc: "Move to first nonblank")
|
|
@@ -124,10 +239,17 @@ module RuVim
|
|
|
124
239
|
register_internal_unless(cmd, "window.focus_right", call: :window_focus_right, desc: "Focus right window")
|
|
125
240
|
register_internal_unless(cmd, "window.focus_up", call: :window_focus_up, desc: "Focus upper window")
|
|
126
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")
|
|
127
246
|
register_internal_unless(cmd, "mode.command_line", call: :enter_command_line_mode, desc: "Enter command-line mode")
|
|
128
247
|
register_internal_unless(cmd, "mode.search_forward", call: :enter_search_forward_mode, desc: "Enter / search")
|
|
129
248
|
register_internal_unless(cmd, "mode.search_backward", call: :enter_search_backward_mode, desc: "Enter ? search")
|
|
130
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")
|
|
131
253
|
register_internal_unless(cmd, "buffer.delete_line", call: :delete_line, desc: "Delete current line")
|
|
132
254
|
register_internal_unless(cmd, "buffer.delete_motion", call: :delete_motion, desc: "Delete by motion")
|
|
133
255
|
register_internal_unless(cmd, "buffer.change_motion", call: :change_motion, desc: "Change by motion")
|
|
@@ -153,12 +275,43 @@ module RuVim
|
|
|
153
275
|
register_internal_unless(cmd, "jump.newer", call: :jump_newer, desc: "Jump newer")
|
|
154
276
|
register_internal_unless(cmd, "editor.buffer_next", call: :buffer_next, desc: "Next buffer")
|
|
155
277
|
register_internal_unless(cmd, "editor.buffer_prev", call: :buffer_prev, desc: "Previous buffer")
|
|
278
|
+
register_internal_unless(cmd, "editor.buffer_delete", call: :buffer_delete, desc: "Delete buffer")
|
|
156
279
|
register_internal_unless(cmd, "buffer.replace_char", call: :replace_char, desc: "Replace single char")
|
|
280
|
+
register_internal_unless(cmd, "file.goto_under_cursor", call: :file_goto_under_cursor, desc: "Open file under cursor")
|
|
157
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
|
+
)
|
|
158
309
|
|
|
159
310
|
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :maybe_one, bang: true)
|
|
160
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)
|
|
161
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)
|
|
162
315
|
register_ex_unless(ex, "e", call: :file_edit, aliases: %w[edit], desc: "Edit file / reload", nargs: :maybe_one, bang: true)
|
|
163
316
|
register_ex_unless(ex, "help", call: :ex_help, desc: "Show help / topics", nargs: :any)
|
|
164
317
|
register_ex_unless(ex, "command", call: :ex_define_command, desc: "Define user command", nargs: :any, bang: true)
|
|
@@ -167,7 +320,14 @@ module RuVim
|
|
|
167
320
|
register_ex_unless(ex, "bnext", call: :buffer_next, aliases: %w[bn], desc: "Next buffer", nargs: 0, bang: true)
|
|
168
321
|
register_ex_unless(ex, "bprev", call: :buffer_prev, aliases: %w[bp], desc: "Previous buffer", nargs: 0, bang: true)
|
|
169
322
|
register_ex_unless(ex, "buffer", call: :buffer_switch, aliases: %w[b], desc: "Switch buffer", nargs: 1, bang: true)
|
|
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)
|
|
170
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)
|
|
171
331
|
register_ex_unless(ex, "set", call: :ex_set, desc: "Set options", nargs: :any)
|
|
172
332
|
register_ex_unless(ex, "setlocal", call: :ex_setlocal, desc: "Set window/buffer local option", nargs: :any)
|
|
173
333
|
register_ex_unless(ex, "setglobal", call: :ex_setglobal, desc: "Set global option", nargs: :any)
|
|
@@ -176,6 +336,7 @@ module RuVim
|
|
|
176
336
|
register_ex_unless(ex, "tabnew", call: :tab_new, desc: "New tab", nargs: :maybe_one)
|
|
177
337
|
register_ex_unless(ex, "tabnext", call: :tab_next, aliases: %w[tabn], desc: "Next tab", nargs: 0)
|
|
178
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)
|
|
179
340
|
register_ex_unless(ex, "vimgrep", call: :ex_vimgrep, desc: "Populate quickfix from regex (minimal)", nargs: :any)
|
|
180
341
|
register_ex_unless(ex, "lvimgrep", call: :ex_lvimgrep, desc: "Populate location list from regex (minimal)", nargs: :any)
|
|
181
342
|
register_ex_unless(ex, "copen", call: :ex_copen, desc: "Open quickfix list", nargs: 0)
|
|
@@ -186,6 +347,15 @@ module RuVim
|
|
|
186
347
|
register_ex_unless(ex, "lclose", call: :ex_lclose, desc: "Close location list window", nargs: 0)
|
|
187
348
|
register_ex_unless(ex, "lnext", call: :ex_lnext, aliases: %w[ln], desc: "Next location item", nargs: 0)
|
|
188
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")
|
|
189
359
|
end
|
|
190
360
|
|
|
191
361
|
def bind_default_keys!
|
|
@@ -193,6 +363,10 @@ module RuVim
|
|
|
193
363
|
@keymaps.bind(:normal, "j", "cursor.down")
|
|
194
364
|
@keymaps.bind(:normal, "k", "cursor.up")
|
|
195
365
|
@keymaps.bind(:normal, "l", "cursor.right")
|
|
366
|
+
@keymaps.bind(:normal, ["<Left>"], "cursor.left")
|
|
367
|
+
@keymaps.bind(:normal, ["<Down>"], "cursor.down")
|
|
368
|
+
@keymaps.bind(:normal, ["<Up>"], "cursor.up")
|
|
369
|
+
@keymaps.bind(:normal, ["<Right>"], "cursor.right")
|
|
196
370
|
@keymaps.bind(:normal, "0", "cursor.line_start")
|
|
197
371
|
@keymaps.bind(:normal, "$", "cursor.line_end")
|
|
198
372
|
@keymaps.bind(:normal, "^", "cursor.first_nonblank")
|
|
@@ -216,198 +390,248 @@ module RuVim
|
|
|
216
390
|
@keymaps.bind(:normal, ["<C-w>", "j"], "window.focus_down")
|
|
217
391
|
@keymaps.bind(:normal, ["<C-w>", "k"], "window.focus_up")
|
|
218
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")
|
|
219
397
|
@keymaps.bind(:normal, ":", "mode.command_line")
|
|
220
398
|
@keymaps.bind(:normal, "/", "mode.search_forward")
|
|
221
399
|
@keymaps.bind(:normal, "?", "mode.search_backward")
|
|
222
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")
|
|
223
427
|
@keymaps.bind(:normal, "p", "buffer.paste_after")
|
|
224
428
|
@keymaps.bind(:normal, "P", "buffer.paste_before")
|
|
225
429
|
@keymaps.bind(:normal, "u", "buffer.undo")
|
|
226
430
|
@keymaps.bind(:normal, ["<C-r>"], "buffer.redo")
|
|
227
431
|
@keymaps.bind(:normal, ["<C-o>"], "jump.older")
|
|
228
432
|
@keymaps.bind(:normal, ["<C-i>"], "jump.newer")
|
|
433
|
+
@keymaps.bind(:normal, ["<C-d>"], "cursor.page_down.half")
|
|
434
|
+
@keymaps.bind(:normal, ["<C-u>"], "cursor.page_up.half")
|
|
435
|
+
@keymaps.bind(:normal, ["<C-f>"], "cursor.page_down.default")
|
|
436
|
+
@keymaps.bind(:normal, ["<C-b>"], "cursor.page_up.default")
|
|
437
|
+
@keymaps.bind(:normal, ["<C-e>"], "window.scroll_down.line")
|
|
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")
|
|
229
443
|
@keymaps.bind(:normal, "n", "search.next")
|
|
230
444
|
@keymaps.bind(:normal, "N", "search.prev")
|
|
231
445
|
@keymaps.bind(:normal, "*", "search.word_forward")
|
|
232
446
|
@keymaps.bind(:normal, "#", "search.word_backward")
|
|
233
447
|
@keymaps.bind(:normal, "g*", "search.word_forward_partial")
|
|
234
448
|
@keymaps.bind(:normal, "g#", "search.word_backward_partial")
|
|
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")
|
|
454
|
+
@keymaps.bind(:normal, ["<PageUp>"], "cursor.page_up.default")
|
|
455
|
+
@keymaps.bind(:normal, ["<PageDown>"], "cursor.page_down.default")
|
|
235
456
|
@keymaps.bind(:normal, "\e", "ui.clear_message")
|
|
236
457
|
end
|
|
237
458
|
|
|
238
459
|
def handle_key(key)
|
|
460
|
+
mode_before = @editor.mode
|
|
461
|
+
clear_stale_message_before_key(key)
|
|
239
462
|
@skip_record_for_current_key = false
|
|
240
463
|
append_dot_change_capture_key(key)
|
|
241
|
-
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
|
|
242
470
|
handle_ctrl_c
|
|
471
|
+
track_mode_transition(mode_before)
|
|
243
472
|
record_macro_key_if_needed(key)
|
|
244
473
|
return
|
|
245
474
|
end
|
|
246
475
|
|
|
247
476
|
case @editor.mode
|
|
477
|
+
when :hit_enter
|
|
478
|
+
handle_hit_enter_key(key)
|
|
248
479
|
when :insert
|
|
249
480
|
handle_insert_key(key)
|
|
250
481
|
when :command_line
|
|
251
482
|
handle_command_line_key(key)
|
|
252
483
|
when :visual_char, :visual_line, :visual_block
|
|
253
484
|
handle_visual_key(key)
|
|
485
|
+
when :rich
|
|
486
|
+
handle_rich_key(key)
|
|
254
487
|
else
|
|
255
488
|
handle_normal_key(key)
|
|
256
489
|
end
|
|
490
|
+
track_mode_transition(mode_before)
|
|
257
491
|
load_current_ftplugin!
|
|
258
492
|
record_macro_key_if_needed(key)
|
|
493
|
+
rescue RuVim::CommandError => e
|
|
494
|
+
@editor.echo_error(e.message)
|
|
259
495
|
end
|
|
260
496
|
|
|
261
|
-
def
|
|
262
|
-
if
|
|
263
|
-
|
|
264
|
-
|
|
497
|
+
def clear_stale_message_before_key(key)
|
|
498
|
+
return if @editor.message.to_s.empty?
|
|
499
|
+
return if @editor.command_line_active?
|
|
500
|
+
return if @editor.hit_enter_active?
|
|
501
|
+
|
|
502
|
+
# Keep the error visible while the user is still dismissing/cancelling;
|
|
503
|
+
# otherwise, the next operation replaces the command-line area naturally.
|
|
504
|
+
return if key == :ctrl_c
|
|
505
|
+
|
|
506
|
+
@editor.clear_message
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def handle_editor_app_action(name, **kwargs)
|
|
510
|
+
if @editor.rich_mode?
|
|
511
|
+
case name.to_sym
|
|
512
|
+
when :normal_operator_start
|
|
513
|
+
op = (kwargs[:name] || kwargs["name"]).to_sym
|
|
514
|
+
return if op == :delete || op == :change
|
|
515
|
+
when :normal_replace_pending_start, :normal_change_repeat
|
|
516
|
+
return
|
|
517
|
+
end
|
|
265
518
|
end
|
|
266
519
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
520
|
+
case name.to_sym
|
|
521
|
+
when :normal_register_pending_start
|
|
522
|
+
start_register_pending
|
|
523
|
+
when :normal_operator_start
|
|
524
|
+
start_operator_pending((kwargs[:name] || kwargs["name"]).to_sym)
|
|
525
|
+
when :normal_replace_pending_start
|
|
526
|
+
start_replace_pending
|
|
527
|
+
when :normal_find_pending_start
|
|
528
|
+
start_find_pending((kwargs[:token] || kwargs["token"]).to_s)
|
|
529
|
+
when :normal_find_repeat
|
|
530
|
+
repeat_last_find(reverse: !!(kwargs[:reverse] || kwargs["reverse"]))
|
|
531
|
+
when :normal_change_repeat
|
|
532
|
+
repeat_last_change
|
|
533
|
+
when :normal_macro_record_toggle
|
|
534
|
+
toggle_macro_recording_or_start_pending
|
|
535
|
+
when :normal_macro_play_pending_start
|
|
536
|
+
start_macro_play_pending
|
|
537
|
+
when :normal_mark_pending_start
|
|
538
|
+
start_mark_pending
|
|
539
|
+
when :normal_jump_pending_start
|
|
540
|
+
start_jump_pending(
|
|
541
|
+
linewise: !!(kwargs[:linewise] || kwargs["linewise"]),
|
|
542
|
+
repeat_token: (kwargs[:repeat_token] || kwargs["repeat_token"]).to_s
|
|
543
|
+
)
|
|
544
|
+
else
|
|
545
|
+
raise RuVim::CommandError, "Unknown app action: #{name}"
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def handle_normal_key(key)
|
|
550
|
+
case
|
|
551
|
+
when handle_normal_key_pre_dispatch(key)
|
|
552
|
+
when (token = normalize_key_token(key)).nil?
|
|
553
|
+
when handle_normal_pending_state(token)
|
|
554
|
+
when handle_normal_direct_token(token)
|
|
555
|
+
else
|
|
556
|
+
@pending_keys ||= []
|
|
557
|
+
@pending_keys << token
|
|
558
|
+
resolve_normal_key_sequence
|
|
270
559
|
end
|
|
560
|
+
end
|
|
271
561
|
|
|
272
|
-
|
|
562
|
+
def handle_normal_key_pre_dispatch(key)
|
|
563
|
+
case
|
|
564
|
+
when key == :enter && handle_list_window_enter
|
|
565
|
+
when digit_key?(key) && count_digit_allowed?(key)
|
|
273
566
|
@editor.pending_count = (@editor.pending_count.to_s + key).to_i
|
|
274
567
|
@editor.echo(@editor.pending_count.to_s)
|
|
275
568
|
@pending_keys = []
|
|
276
|
-
|
|
569
|
+
else
|
|
570
|
+
return false
|
|
277
571
|
end
|
|
572
|
+
true
|
|
573
|
+
end
|
|
278
574
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
575
|
+
def handle_normal_pending_state(token)
|
|
576
|
+
case
|
|
577
|
+
when @pending_keys && !@pending_keys.empty?
|
|
578
|
+
@pending_keys << token
|
|
579
|
+
resolve_normal_key_sequence
|
|
580
|
+
when @operator_pending
|
|
283
581
|
handle_operator_pending_key(token)
|
|
284
|
-
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
if @register_pending
|
|
582
|
+
when @register_pending
|
|
288
583
|
finish_register_pending(token)
|
|
289
|
-
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
if @mark_pending
|
|
584
|
+
when @mark_pending
|
|
293
585
|
finish_mark_pending(token)
|
|
294
|
-
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
if @jump_pending
|
|
586
|
+
when @jump_pending
|
|
298
587
|
finish_jump_pending(token)
|
|
299
|
-
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
if @macro_record_pending
|
|
588
|
+
when @macro_record_pending
|
|
303
589
|
finish_macro_record_pending(token)
|
|
304
|
-
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
if @macro_play_pending
|
|
590
|
+
when @macro_play_pending
|
|
308
591
|
finish_macro_play_pending(token)
|
|
309
|
-
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
if @replace_pending
|
|
592
|
+
when @replace_pending
|
|
313
593
|
handle_replace_pending_key(token)
|
|
314
|
-
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
if @find_pending
|
|
594
|
+
when @find_pending
|
|
318
595
|
finish_find_pending(token)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if token == "\""
|
|
323
|
-
start_register_pending
|
|
324
|
-
return
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
if token == "d"
|
|
328
|
-
start_operator_pending(:delete)
|
|
329
|
-
return
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
if token == "y"
|
|
333
|
-
start_operator_pending(:yank)
|
|
334
|
-
return
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
if token == "c"
|
|
338
|
-
start_operator_pending(:change)
|
|
339
|
-
return
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
if token == "r"
|
|
343
|
-
start_replace_pending
|
|
344
|
-
return
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
if %w[f F t T].include?(token)
|
|
348
|
-
start_find_pending(token)
|
|
349
|
-
return
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
if token == ";"
|
|
353
|
-
repeat_last_find(reverse: false)
|
|
354
|
-
return
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
if token == ","
|
|
358
|
-
repeat_last_find(reverse: true)
|
|
359
|
-
return
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
if token == "."
|
|
363
|
-
repeat_last_change
|
|
364
|
-
return
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
if token == "q"
|
|
368
|
-
if @editor.macro_recording?
|
|
369
|
-
stop_macro_recording
|
|
370
|
-
else
|
|
371
|
-
start_macro_record_pending
|
|
372
|
-
end
|
|
373
|
-
return
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
if token == "@"
|
|
377
|
-
start_macro_play_pending
|
|
378
|
-
return
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
if token == "m"
|
|
382
|
-
start_mark_pending
|
|
383
|
-
return
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
if token == "'"
|
|
387
|
-
start_jump_pending(linewise: true, repeat_token: "'")
|
|
388
|
-
return
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
if token == "`"
|
|
392
|
-
start_jump_pending(linewise: false, repeat_token: "`")
|
|
393
|
-
return
|
|
596
|
+
else
|
|
597
|
+
return false
|
|
394
598
|
end
|
|
599
|
+
true
|
|
600
|
+
end
|
|
395
601
|
|
|
396
|
-
|
|
397
|
-
|
|
602
|
+
def handle_normal_direct_token(token)
|
|
603
|
+
false
|
|
604
|
+
end
|
|
398
605
|
|
|
606
|
+
def resolve_normal_key_sequence
|
|
399
607
|
match = @keymaps.resolve_with_context(:normal, @pending_keys, editor: @editor)
|
|
400
608
|
case match.status
|
|
401
609
|
when :pending, :ambiguous
|
|
610
|
+
if match.status == :ambiguous && match.invocation
|
|
611
|
+
inv = dup_invocation(match.invocation)
|
|
612
|
+
inv.count = @editor.pending_count
|
|
613
|
+
@pending_ambiguous_invocation = inv
|
|
614
|
+
else
|
|
615
|
+
@pending_ambiguous_invocation = nil
|
|
616
|
+
end
|
|
617
|
+
arm_pending_key_timeout
|
|
402
618
|
return
|
|
403
619
|
when :match
|
|
620
|
+
clear_pending_key_timeout
|
|
404
621
|
matched_keys = @pending_keys.dup
|
|
405
|
-
repeat_count = @editor.pending_count
|
|
622
|
+
repeat_count = @editor.pending_count
|
|
623
|
+
@pending_keys = []
|
|
406
624
|
invocation = dup_invocation(match.invocation)
|
|
407
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
|
|
408
631
|
@dispatcher.dispatch(@editor, invocation)
|
|
409
632
|
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
410
633
|
else
|
|
634
|
+
clear_pending_key_timeout
|
|
411
635
|
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
412
636
|
end
|
|
413
637
|
@editor.pending_count = nil
|
|
@@ -424,28 +648,27 @@ module RuVim
|
|
|
424
648
|
@editor.echo("")
|
|
425
649
|
when :backspace
|
|
426
650
|
clear_insert_completion
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
@editor.current_window.cursor_x = x
|
|
651
|
+
return unless insert_backspace_allowed?
|
|
652
|
+
insert_backspace_in_insert_mode
|
|
430
653
|
when :ctrl_n
|
|
431
654
|
insert_complete(+1)
|
|
432
655
|
when :ctrl_p
|
|
433
656
|
insert_complete(-1)
|
|
434
657
|
when :ctrl_i
|
|
435
658
|
clear_insert_completion
|
|
436
|
-
|
|
437
|
-
@editor.current_window.cursor_x += 1
|
|
659
|
+
insert_tab_in_insert_mode
|
|
438
660
|
when :enter
|
|
439
661
|
clear_insert_completion
|
|
440
662
|
y, x = @editor.current_buffer.insert_newline(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
|
|
663
|
+
x = apply_insert_autoindent(y, x, previous_row: y - 1)
|
|
441
664
|
@editor.current_window.cursor_y = y
|
|
442
665
|
@editor.current_window.cursor_x = x
|
|
443
666
|
when :left
|
|
444
667
|
clear_insert_completion
|
|
445
|
-
|
|
668
|
+
dispatch_insert_cursor_motion("cursor.left")
|
|
446
669
|
when :right
|
|
447
670
|
clear_insert_completion
|
|
448
|
-
|
|
671
|
+
dispatch_insert_cursor_motion("cursor.right")
|
|
449
672
|
when :up
|
|
450
673
|
clear_insert_completion
|
|
451
674
|
@editor.current_window.move_up(@editor.current_buffer, 1)
|
|
@@ -461,6 +684,8 @@ module RuVim
|
|
|
461
684
|
clear_insert_completion
|
|
462
685
|
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
463
686
|
@editor.current_window.cursor_x += 1
|
|
687
|
+
maybe_showmatch_after_insert(key)
|
|
688
|
+
maybe_dedent_after_insert(key)
|
|
464
689
|
end
|
|
465
690
|
end
|
|
466
691
|
|
|
@@ -506,6 +731,8 @@ module RuVim
|
|
|
506
731
|
when "d"
|
|
507
732
|
@visual_pending = nil
|
|
508
733
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
|
|
734
|
+
when "="
|
|
735
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_indent"))
|
|
509
736
|
when "\""
|
|
510
737
|
start_register_pending
|
|
511
738
|
when "i", "a"
|
|
@@ -551,6 +778,7 @@ module RuVim
|
|
|
551
778
|
if token == "g"
|
|
552
779
|
@pending_keys ||= []
|
|
553
780
|
@pending_keys << token
|
|
781
|
+
arm_pending_key_timeout
|
|
554
782
|
return
|
|
555
783
|
end
|
|
556
784
|
|
|
@@ -559,9 +787,11 @@ module RuVim
|
|
|
559
787
|
end
|
|
560
788
|
|
|
561
789
|
if id
|
|
562
|
-
|
|
790
|
+
clear_pending_key_timeout
|
|
791
|
+
count = @editor.pending_count
|
|
563
792
|
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
564
793
|
else
|
|
794
|
+
clear_pending_key_timeout
|
|
565
795
|
@editor.echo_error("Unknown visual key: #{token}")
|
|
566
796
|
end
|
|
567
797
|
ensure
|
|
@@ -572,29 +802,87 @@ module RuVim
|
|
|
572
802
|
cmd = @editor.command_line
|
|
573
803
|
case key
|
|
574
804
|
when :escape
|
|
805
|
+
clear_command_line_completion
|
|
806
|
+
cancel_incsearch_preview_if_any
|
|
575
807
|
@editor.cancel_command_line
|
|
576
808
|
when :enter
|
|
809
|
+
clear_command_line_completion
|
|
577
810
|
line = cmd.text.dup
|
|
578
811
|
push_command_line_history(cmd.prefix, line)
|
|
579
812
|
handle_command_line_submit(cmd.prefix, line)
|
|
580
813
|
when :backspace
|
|
814
|
+
clear_command_line_completion
|
|
815
|
+
if cmd.text.empty? && cmd.cursor.zero?
|
|
816
|
+
cancel_incsearch_preview_if_any
|
|
817
|
+
@editor.cancel_command_line
|
|
818
|
+
return
|
|
819
|
+
end
|
|
581
820
|
cmd.backspace
|
|
582
821
|
when :up
|
|
822
|
+
clear_command_line_completion
|
|
583
823
|
command_line_history_move(-1)
|
|
584
824
|
when :down
|
|
825
|
+
clear_command_line_completion
|
|
585
826
|
command_line_history_move(1)
|
|
586
827
|
when :left
|
|
828
|
+
clear_command_line_completion
|
|
587
829
|
cmd.move_left
|
|
588
830
|
when :right
|
|
831
|
+
clear_command_line_completion
|
|
589
832
|
cmd.move_right
|
|
590
833
|
else
|
|
591
834
|
if key == :ctrl_i
|
|
592
835
|
command_line_complete
|
|
593
836
|
elsif key.is_a?(String)
|
|
837
|
+
clear_command_line_completion
|
|
594
838
|
@cmdline_history_index = nil
|
|
595
839
|
cmd.insert(key)
|
|
596
840
|
end
|
|
597
841
|
end
|
|
842
|
+
update_incsearch_preview_if_needed
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def handle_list_window_enter
|
|
846
|
+
buffer = @editor.current_buffer
|
|
847
|
+
return false unless buffer.kind == :quickfix || buffer.kind == :location_list
|
|
848
|
+
|
|
849
|
+
item_index = @editor.current_window.cursor_y - 2
|
|
850
|
+
if item_index.negative?
|
|
851
|
+
@editor.echo_error("No list item on this line")
|
|
852
|
+
return true
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
source_window_id = buffer.options["ruvim_list_source_window_id"]
|
|
856
|
+
source_window_id = source_window_id.to_i if source_window_id
|
|
857
|
+
source_window_id = nil unless source_window_id && @editor.windows.key?(source_window_id)
|
|
858
|
+
|
|
859
|
+
item =
|
|
860
|
+
if buffer.kind == :quickfix
|
|
861
|
+
@editor.select_quickfix(item_index)
|
|
862
|
+
else
|
|
863
|
+
owner_window_id = source_window_id || @editor.current_window_id
|
|
864
|
+
@editor.select_location_list(item_index, window_id: owner_window_id)
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
unless item
|
|
868
|
+
@editor.echo_error("#{buffer.kind == :quickfix ? 'quickfix' : 'location list'} item not found")
|
|
869
|
+
return true
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
if source_window_id
|
|
873
|
+
@editor.current_window_id = source_window_id
|
|
874
|
+
end
|
|
875
|
+
@editor.jump_to_location(item)
|
|
876
|
+
@editor.echo(
|
|
877
|
+
if buffer.kind == :quickfix
|
|
878
|
+
"qf #{@editor.quickfix_index.to_i + 1}/#{@editor.quickfix_items.length}"
|
|
879
|
+
else
|
|
880
|
+
owner_window_id = source_window_id || @editor.current_window_id
|
|
881
|
+
list = @editor.location_list(owner_window_id)
|
|
882
|
+
"ll #{list[:index].to_i + 1}/#{list[:items].length}"
|
|
883
|
+
end
|
|
884
|
+
)
|
|
885
|
+
true
|
|
598
886
|
end
|
|
599
887
|
|
|
600
888
|
def arrow_key?(key)
|
|
@@ -612,7 +900,7 @@ module RuVim
|
|
|
612
900
|
up: "cursor.up",
|
|
613
901
|
down: "cursor.down"
|
|
614
902
|
}.fetch(key)
|
|
615
|
-
inv = CommandInvocation.new(id:, count: @editor.pending_count
|
|
903
|
+
inv = CommandInvocation.new(id:, count: @editor.pending_count)
|
|
616
904
|
@dispatcher.dispatch(@editor, inv)
|
|
617
905
|
@editor.pending_count = nil
|
|
618
906
|
@pending_keys = []
|
|
@@ -622,7 +910,7 @@ module RuVim
|
|
|
622
910
|
id = (key == :pageup ? "cursor.page_up" : "cursor.page_down")
|
|
623
911
|
inv = CommandInvocation.new(
|
|
624
912
|
id: id,
|
|
625
|
-
count: @editor.pending_count
|
|
913
|
+
count: @editor.pending_count,
|
|
626
914
|
kwargs: { page_lines: current_page_step_lines }
|
|
627
915
|
)
|
|
628
916
|
@dispatcher.dispatch(@editor, inv)
|
|
@@ -630,13 +918,6 @@ module RuVim
|
|
|
630
918
|
@pending_keys = []
|
|
631
919
|
end
|
|
632
920
|
|
|
633
|
-
def current_page_step_lines
|
|
634
|
-
height = @screen.current_window_view_height(@editor)
|
|
635
|
-
[height - 1, 1].max
|
|
636
|
-
rescue StandardError
|
|
637
|
-
1
|
|
638
|
-
end
|
|
639
|
-
|
|
640
921
|
def digit_key?(key)
|
|
641
922
|
key.is_a?(String) && key.match?(/\A\d\z/)
|
|
642
923
|
end
|
|
@@ -653,14 +934,30 @@ module RuVim
|
|
|
653
934
|
when String then key
|
|
654
935
|
when :escape then "\e"
|
|
655
936
|
when :ctrl_r then "<C-r>"
|
|
937
|
+
when :ctrl_d then "<C-d>"
|
|
938
|
+
when :ctrl_u then "<C-u>"
|
|
939
|
+
when :ctrl_f then "<C-f>"
|
|
940
|
+
when :ctrl_b then "<C-b>"
|
|
941
|
+
when :ctrl_e then "<C-e>"
|
|
942
|
+
when :ctrl_y then "<C-y>"
|
|
656
943
|
when :ctrl_v then "<C-v>"
|
|
657
944
|
when :ctrl_i then "<C-i>"
|
|
658
945
|
when :ctrl_o then "<C-o>"
|
|
659
946
|
when :ctrl_w then "<C-w>"
|
|
947
|
+
when :ctrl_l then "<C-l>"
|
|
948
|
+
when :ctrl_c then "<C-c>"
|
|
949
|
+
when :left then "<Left>"
|
|
950
|
+
when :right then "<Right>"
|
|
951
|
+
when :up then "<Up>"
|
|
952
|
+
when :down then "<Down>"
|
|
660
953
|
when :home then "<Home>"
|
|
661
954
|
when :end then "<End>"
|
|
662
955
|
when :pageup then "<PageUp>"
|
|
663
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>"
|
|
664
961
|
else nil
|
|
665
962
|
end
|
|
666
963
|
end
|
|
@@ -676,23 +973,81 @@ module RuVim
|
|
|
676
973
|
)
|
|
677
974
|
end
|
|
678
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
|
+
|
|
679
1015
|
def handle_ctrl_c
|
|
680
1016
|
case @editor.mode
|
|
1017
|
+
when :hit_enter
|
|
1018
|
+
@editor.exit_hit_enter_mode
|
|
681
1019
|
when :insert
|
|
682
1020
|
finish_insert_change_group
|
|
683
1021
|
finish_dot_change_capture
|
|
684
1022
|
clear_insert_completion
|
|
1023
|
+
clear_pending_key_timeout
|
|
685
1024
|
@editor.enter_normal_mode
|
|
686
1025
|
@editor.echo("")
|
|
687
1026
|
when :command_line
|
|
1027
|
+
clear_pending_key_timeout
|
|
1028
|
+
cancel_incsearch_preview_if_any
|
|
688
1029
|
@editor.cancel_command_line
|
|
689
1030
|
when :visual_char, :visual_line, :visual_block
|
|
690
1031
|
@visual_pending = nil
|
|
691
1032
|
@register_pending = false
|
|
692
1033
|
@mark_pending = false
|
|
693
1034
|
@jump_pending = nil
|
|
1035
|
+
clear_pending_key_timeout
|
|
694
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)
|
|
695
1049
|
else
|
|
1050
|
+
clear_pending_key_timeout
|
|
696
1051
|
@editor.pending_count = nil
|
|
697
1052
|
@pending_keys = []
|
|
698
1053
|
@operator_pending = nil
|
|
@@ -706,11 +1061,34 @@ module RuVim
|
|
|
706
1061
|
end
|
|
707
1062
|
end
|
|
708
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
|
+
|
|
709
1086
|
def finish_insert_change_group
|
|
710
1087
|
@editor.current_buffer.end_change_group
|
|
711
1088
|
end
|
|
712
1089
|
|
|
713
1090
|
def handle_command_line_submit(prefix, line)
|
|
1091
|
+
clear_incsearch_preview_state(apply: false) if %w[/ ?].include?(prefix)
|
|
714
1092
|
case prefix
|
|
715
1093
|
when ":"
|
|
716
1094
|
verbose_log(2, "ex: #{line}")
|
|
@@ -729,7 +1107,7 @@ module RuVim
|
|
|
729
1107
|
end
|
|
730
1108
|
|
|
731
1109
|
def start_operator_pending(name)
|
|
732
|
-
@operator_pending = { name:, count:
|
|
1110
|
+
@operator_pending = { name:, count: @editor.pending_count }
|
|
733
1111
|
@editor.pending_count = nil
|
|
734
1112
|
@pending_keys = []
|
|
735
1113
|
@editor.echo(name == :delete ? "d" : name.to_s)
|
|
@@ -804,6 +1182,14 @@ module RuVim
|
|
|
804
1182
|
@editor.echo("q")
|
|
805
1183
|
end
|
|
806
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
|
+
|
|
807
1193
|
def finish_macro_record_pending(token)
|
|
808
1194
|
@macro_record_pending = false
|
|
809
1195
|
if token == "\e"
|
|
@@ -851,7 +1237,7 @@ module RuVim
|
|
|
851
1237
|
return
|
|
852
1238
|
end
|
|
853
1239
|
|
|
854
|
-
count = @editor.pending_count
|
|
1240
|
+
count = @editor.pending_count
|
|
855
1241
|
@editor.pending_count = nil
|
|
856
1242
|
play_macro(name, count:)
|
|
857
1243
|
end
|
|
@@ -873,7 +1259,7 @@ module RuVim
|
|
|
873
1259
|
@last_macro_name = reg
|
|
874
1260
|
@macro_play_stack << reg
|
|
875
1261
|
@suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
|
|
876
|
-
count.times do
|
|
1262
|
+
[count.to_i, 1].max.times do
|
|
877
1263
|
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
878
1264
|
end
|
|
879
1265
|
@editor.echo("@#{reg}")
|
|
@@ -944,6 +1330,18 @@ module RuVim
|
|
|
944
1330
|
return
|
|
945
1331
|
end
|
|
946
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
|
+
|
|
947
1345
|
if op[:name] == :change && motion == "c"
|
|
948
1346
|
inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
|
|
949
1347
|
@dispatcher.dispatch(@editor, inv)
|
|
@@ -962,7 +1360,7 @@ module RuVim
|
|
|
962
1360
|
end
|
|
963
1361
|
|
|
964
1362
|
def start_replace_pending
|
|
965
|
-
@replace_pending = { count:
|
|
1363
|
+
@replace_pending = { count: @editor.pending_count }
|
|
966
1364
|
@editor.pending_count = nil
|
|
967
1365
|
@pending_keys = []
|
|
968
1366
|
@editor.echo("r")
|
|
@@ -1003,9 +1401,9 @@ module RuVim
|
|
|
1003
1401
|
return if (@dot_replay_depth || 0).positive?
|
|
1004
1402
|
|
|
1005
1403
|
case invocation.id
|
|
1006
|
-
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"
|
|
1007
1405
|
record_last_change_keys(count_prefixed_keys(count, matched_keys))
|
|
1008
|
-
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"
|
|
1009
1407
|
begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
|
|
1010
1408
|
end
|
|
1011
1409
|
end
|
|
@@ -1050,7 +1448,7 @@ module RuVim
|
|
|
1050
1448
|
@find_pending = {
|
|
1051
1449
|
direction: (token == "f" || token == "t") ? :forward : :backward,
|
|
1052
1450
|
till: (token == "t" || token == "T"),
|
|
1053
|
-
count:
|
|
1451
|
+
count: @editor.pending_count
|
|
1054
1452
|
}
|
|
1055
1453
|
@editor.pending_count = nil
|
|
1056
1454
|
@pending_keys = []
|
|
@@ -1095,7 +1493,7 @@ module RuVim
|
|
|
1095
1493
|
else
|
|
1096
1494
|
last[:direction]
|
|
1097
1495
|
end
|
|
1098
|
-
count = @editor.pending_count
|
|
1496
|
+
count = @editor.pending_count
|
|
1099
1497
|
@editor.pending_count = nil
|
|
1100
1498
|
@pending_keys = []
|
|
1101
1499
|
moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
|
|
@@ -1109,7 +1507,7 @@ module RuVim
|
|
|
1109
1507
|
pos = win.cursor_x
|
|
1110
1508
|
target = nil
|
|
1111
1509
|
|
|
1112
|
-
count.times do
|
|
1510
|
+
[count.to_i, 1].max.times do
|
|
1113
1511
|
idx =
|
|
1114
1512
|
if direction == :forward
|
|
1115
1513
|
line.index(char, pos + 1)
|
|
@@ -1163,6 +1561,66 @@ module RuVim
|
|
|
1163
1561
|
@cmdline_history_index = nil
|
|
1164
1562
|
end
|
|
1165
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
|
+
|
|
1166
1624
|
def command_line_history_move(delta)
|
|
1167
1625
|
cmd = @editor.command_line
|
|
1168
1626
|
hist = @cmdline_history[cmd.prefix]
|
|
@@ -1181,6 +1639,7 @@ module RuVim
|
|
|
1181
1639
|
else
|
|
1182
1640
|
cmd.replace_text(hist[@cmdline_history_index])
|
|
1183
1641
|
end
|
|
1642
|
+
update_incsearch_preview_if_needed
|
|
1184
1643
|
end
|
|
1185
1644
|
|
|
1186
1645
|
def command_line_complete
|
|
@@ -1190,25 +1649,173 @@ module RuVim
|
|
|
1190
1649
|
ctx = ex_completion_context(cmd)
|
|
1191
1650
|
return unless ctx
|
|
1192
1651
|
|
|
1193
|
-
matches = ex_completion_candidates(ctx)
|
|
1652
|
+
matches = reusable_command_line_completion_matches(cmd, ctx) || ex_completion_candidates(ctx)
|
|
1194
1653
|
case matches.length
|
|
1195
1654
|
when 0
|
|
1655
|
+
clear_command_line_completion
|
|
1196
1656
|
@editor.echo("No completion")
|
|
1197
1657
|
when 1
|
|
1658
|
+
clear_command_line_completion
|
|
1198
1659
|
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches.first)
|
|
1199
1660
|
else
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1661
|
+
apply_wildmode_completion(cmd, ctx, matches)
|
|
1662
|
+
end
|
|
1663
|
+
update_incsearch_preview_if_needed
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
def reusable_command_line_completion_matches(cmd, ctx)
|
|
1667
|
+
state = @cmdline_completion
|
|
1668
|
+
return nil unless state
|
|
1669
|
+
return nil unless state[:prefix] == cmd.prefix
|
|
1670
|
+
return nil unless state[:kind] == ctx[:kind]
|
|
1671
|
+
return nil unless state[:command] == ctx[:command]
|
|
1672
|
+
return nil unless state[:arg_index] == ctx[:arg_index]
|
|
1673
|
+
return nil unless state[:token_start] == ctx[:token_start]
|
|
1674
|
+
|
|
1675
|
+
before_text = cmd.text[0...ctx[:token_start]].to_s
|
|
1676
|
+
after_text = cmd.text[ctx[:token_end]..].to_s
|
|
1677
|
+
return nil unless state[:before_text] == before_text
|
|
1678
|
+
return nil unless state[:after_text] == after_text
|
|
1679
|
+
|
|
1680
|
+
matches = Array(state[:matches]).map(&:to_s)
|
|
1681
|
+
return nil if matches.empty?
|
|
1682
|
+
|
|
1683
|
+
current_token = cmd.text[ctx[:token_start]...ctx[:token_end]].to_s
|
|
1684
|
+
return nil unless current_token.empty? || matches.include?(current_token) || common_prefix(matches).start_with?(current_token) || current_token.start_with?(common_prefix(matches))
|
|
1685
|
+
|
|
1686
|
+
matches
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
def clear_command_line_completion
|
|
1690
|
+
@cmdline_completion = nil
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
def apply_wildmode_completion(cmd, ctx, matches)
|
|
1694
|
+
mode_steps = wildmode_steps
|
|
1695
|
+
mode_steps = [:full] if mode_steps.empty?
|
|
1696
|
+
state = @cmdline_completion
|
|
1697
|
+
before_text = cmd.text[0...ctx[:token_start]].to_s
|
|
1698
|
+
after_text = cmd.text[ctx[:token_end]..].to_s
|
|
1699
|
+
same = state &&
|
|
1700
|
+
state[:prefix] == cmd.prefix &&
|
|
1701
|
+
state[:kind] == ctx[:kind] &&
|
|
1702
|
+
state[:command] == ctx[:command] &&
|
|
1703
|
+
state[:arg_index] == ctx[:arg_index] &&
|
|
1704
|
+
state[:token_start] == ctx[:token_start] &&
|
|
1705
|
+
state[:before_text] == before_text &&
|
|
1706
|
+
state[:after_text] == after_text &&
|
|
1707
|
+
state[:matches] == matches
|
|
1708
|
+
unless same
|
|
1709
|
+
state = {
|
|
1710
|
+
prefix: cmd.prefix,
|
|
1711
|
+
kind: ctx[:kind],
|
|
1712
|
+
command: ctx[:command],
|
|
1713
|
+
arg_index: ctx[:arg_index],
|
|
1714
|
+
token_start: ctx[:token_start],
|
|
1715
|
+
before_text: before_text,
|
|
1716
|
+
after_text: after_text,
|
|
1717
|
+
matches: matches.dup,
|
|
1718
|
+
step_index: -1,
|
|
1719
|
+
full_index: nil
|
|
1720
|
+
}
|
|
1721
|
+
end
|
|
1722
|
+
|
|
1723
|
+
state[:step_index] += 1
|
|
1724
|
+
step = mode_steps[state[:step_index] % mode_steps.length]
|
|
1725
|
+
case step
|
|
1726
|
+
when :longest
|
|
1727
|
+
pref = common_prefix(matches)
|
|
1728
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
|
|
1729
|
+
when :list
|
|
1730
|
+
show_command_line_completion_menu(matches, selected: state[:full_index], force: true)
|
|
1731
|
+
when :full
|
|
1732
|
+
state[:full_index] = state[:full_index] ? (state[:full_index] + 1) % matches.length : 0
|
|
1733
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches[state[:full_index]])
|
|
1734
|
+
show_command_line_completion_menu(matches, selected: state[:full_index], force: false)
|
|
1735
|
+
else
|
|
1736
|
+
pref = common_prefix(matches)
|
|
1737
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], pref) if pref.length > ctx[:prefix].length
|
|
1203
1738
|
end
|
|
1739
|
+
|
|
1740
|
+
@cmdline_completion = state
|
|
1204
1741
|
end
|
|
1205
1742
|
|
|
1206
|
-
def
|
|
1207
|
-
|
|
1743
|
+
def wildmode_steps
|
|
1744
|
+
raw = @editor.effective_option("wildmode").to_s
|
|
1745
|
+
return [:full] if raw.empty?
|
|
1208
1746
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1747
|
+
raw.split(",").flat_map do |tok|
|
|
1748
|
+
tok.to_s.split(":").map do |part|
|
|
1749
|
+
case part.strip.downcase
|
|
1750
|
+
when "longest" then :longest
|
|
1751
|
+
when "list" then :list
|
|
1752
|
+
when "full" then :full
|
|
1753
|
+
end
|
|
1754
|
+
end
|
|
1755
|
+
end.compact
|
|
1756
|
+
end
|
|
1757
|
+
|
|
1758
|
+
def show_command_line_completion_menu(matches, selected:, force:)
|
|
1759
|
+
return unless force || @editor.effective_option("wildmenu")
|
|
1760
|
+
|
|
1761
|
+
items = matches.each_with_index.map do |m, i|
|
|
1762
|
+
idx = i
|
|
1763
|
+
idx == selected ? "[#{m}]" : m
|
|
1764
|
+
end
|
|
1765
|
+
@editor.echo(compose_command_line_completion_menu(items))
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
def compose_command_line_completion_menu(items)
|
|
1769
|
+
parts = Array(items).map(&:to_s)
|
|
1770
|
+
return "" if parts.empty?
|
|
1771
|
+
|
|
1772
|
+
width = command_line_completion_menu_width
|
|
1773
|
+
width = [width.to_i, 1].max
|
|
1774
|
+
out = +""
|
|
1775
|
+
shown = 0
|
|
1776
|
+
|
|
1777
|
+
parts.each_with_index do |item, idx|
|
|
1778
|
+
token = shown.zero? ? item : " #{item}"
|
|
1779
|
+
if out.empty? && token.length > width
|
|
1780
|
+
out = token[0, width]
|
|
1781
|
+
shown = 1
|
|
1782
|
+
break
|
|
1783
|
+
end
|
|
1784
|
+
break if out.length + token.length > width
|
|
1785
|
+
|
|
1786
|
+
out << token
|
|
1787
|
+
shown = idx + 1
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
if shown < parts.length
|
|
1791
|
+
ellipsis = (out.empty? ? "..." : " ...")
|
|
1792
|
+
if out.length + ellipsis.length <= width
|
|
1793
|
+
out << ellipsis
|
|
1794
|
+
elsif width >= 3
|
|
1795
|
+
out = out[0, width - 3] + "..."
|
|
1796
|
+
else
|
|
1797
|
+
out = "." * width
|
|
1798
|
+
end
|
|
1799
|
+
end
|
|
1800
|
+
|
|
1801
|
+
out
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
def command_line_completion_menu_width
|
|
1805
|
+
return 80 unless defined?(@terminal) && @terminal && @terminal.respond_to?(:winsize)
|
|
1806
|
+
|
|
1807
|
+
_rows, cols = @terminal.winsize
|
|
1808
|
+
[cols.to_i, 1].max
|
|
1809
|
+
rescue StandardError
|
|
1810
|
+
80
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def common_prefix(strings)
|
|
1814
|
+
return "" if strings.empty?
|
|
1815
|
+
|
|
1816
|
+
prefix = strings.first.dup
|
|
1817
|
+
strings[1..]&.each do |s|
|
|
1818
|
+
while !prefix.empty? && !s.start_with?(prefix)
|
|
1212
1819
|
prefix = prefix[0...-1]
|
|
1213
1820
|
end
|
|
1214
1821
|
end
|
|
@@ -1219,6 +1826,98 @@ module RuVim
|
|
|
1219
1826
|
@insert_completion = nil
|
|
1220
1827
|
end
|
|
1221
1828
|
|
|
1829
|
+
def insert_tab_in_insert_mode
|
|
1830
|
+
buf = @editor.current_buffer
|
|
1831
|
+
win = @editor.current_window
|
|
1832
|
+
if @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
1833
|
+
width = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
1834
|
+
width = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if width <= 0
|
|
1835
|
+
width = 2 if width <= 0
|
|
1836
|
+
line = buf.line_at(win.cursor_y)
|
|
1837
|
+
current_col = RuVim::TextMetrics.screen_col_for_char_index(line, win.cursor_x, tabstop: effective_tabstop(win, buf))
|
|
1838
|
+
spaces = width - (current_col % width)
|
|
1839
|
+
spaces = width if spaces <= 0
|
|
1840
|
+
_y, x = buf.insert_text(win.cursor_y, win.cursor_x, " " * spaces)
|
|
1841
|
+
win.cursor_x = x
|
|
1842
|
+
else
|
|
1843
|
+
buf.insert_char(win.cursor_y, win.cursor_x, "\t")
|
|
1844
|
+
win.cursor_x += 1
|
|
1845
|
+
end
|
|
1846
|
+
end
|
|
1847
|
+
|
|
1848
|
+
def apply_insert_autoindent(row, x, previous_row:)
|
|
1849
|
+
buf = @editor.current_buffer
|
|
1850
|
+
win = @editor.current_window
|
|
1851
|
+
return x unless @editor.effective_option("autoindent", window: win, buffer: buf)
|
|
1852
|
+
return x if previous_row.negative?
|
|
1853
|
+
|
|
1854
|
+
prev = buf.line_at(previous_row)
|
|
1855
|
+
indent = prev[/\A[ \t]*/].to_s
|
|
1856
|
+
if @editor.effective_option("smartindent", window: win, buffer: buf)
|
|
1857
|
+
trimmed = prev.rstrip
|
|
1858
|
+
needs_indent = trimmed.end_with?("{", "[", "(")
|
|
1859
|
+
if !needs_indent
|
|
1860
|
+
needs_indent = buf.lang_module.indent_trigger?(trimmed)
|
|
1861
|
+
end
|
|
1862
|
+
if needs_indent
|
|
1863
|
+
sw = @editor.effective_option("shiftwidth", window: win, buffer: buf).to_i
|
|
1864
|
+
sw = effective_tabstop(win, buf) if sw <= 0
|
|
1865
|
+
sw = 2 if sw <= 0
|
|
1866
|
+
indent += " " * sw
|
|
1867
|
+
end
|
|
1868
|
+
end
|
|
1869
|
+
return x if indent.empty?
|
|
1870
|
+
|
|
1871
|
+
_y, new_x = buf.insert_text(row, x, indent)
|
|
1872
|
+
new_x
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
def maybe_showmatch_after_insert(key)
|
|
1876
|
+
return unless [")", "]", "}"].include?(key)
|
|
1877
|
+
return unless @editor.effective_option("showmatch")
|
|
1878
|
+
|
|
1879
|
+
mt = @editor.effective_option("matchtime").to_i
|
|
1880
|
+
mt = 5 if mt <= 0
|
|
1881
|
+
@editor.echo_temporary("match", duration_seconds: mt * 0.1)
|
|
1882
|
+
end
|
|
1883
|
+
|
|
1884
|
+
def maybe_dedent_after_insert(key)
|
|
1885
|
+
return unless @editor.effective_option("smartindent", window: @editor.current_window, buffer: @editor.current_buffer)
|
|
1886
|
+
|
|
1887
|
+
buf = @editor.current_buffer
|
|
1888
|
+
lang_mod = buf.lang_module
|
|
1889
|
+
|
|
1890
|
+
pattern = lang_mod.dedent_trigger(key)
|
|
1891
|
+
return unless pattern
|
|
1892
|
+
|
|
1893
|
+
row = @editor.current_window.cursor_y
|
|
1894
|
+
line = buf.line_at(row)
|
|
1895
|
+
m = line.match(pattern)
|
|
1896
|
+
return unless m
|
|
1897
|
+
|
|
1898
|
+
sw = @editor.effective_option("shiftwidth", buffer: buf).to_i
|
|
1899
|
+
sw = 2 if sw <= 0
|
|
1900
|
+
target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
|
|
1901
|
+
return unless target_indent
|
|
1902
|
+
|
|
1903
|
+
current_indent = m[1].length
|
|
1904
|
+
return if current_indent == target_indent
|
|
1905
|
+
|
|
1906
|
+
stripped = line.to_s.strip
|
|
1907
|
+
buf.delete_span(row, 0, row, current_indent) if current_indent > 0
|
|
1908
|
+
buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
|
|
1909
|
+
@editor.current_window.cursor_x = target_indent + stripped.length
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1912
|
+
def clear_expired_transient_message_if_any
|
|
1913
|
+
@needs_redraw = true if @editor.clear_expired_transient_message!(now: monotonic_now)
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
def effective_tabstop(window = @editor.current_window, buffer = @editor.current_buffer)
|
|
1917
|
+
v = @editor.effective_option("tabstop", window:, buffer:).to_i
|
|
1918
|
+
v.positive? ? v : 2
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1222
1921
|
def insert_complete(direction)
|
|
1223
1922
|
state = ensure_insert_completion_state
|
|
1224
1923
|
return unless state
|
|
@@ -1229,8 +1928,27 @@ module RuVim
|
|
|
1229
1928
|
return
|
|
1230
1929
|
end
|
|
1231
1930
|
|
|
1931
|
+
if state[:index].nil? && insert_completion_noselect? && matches.length > 1
|
|
1932
|
+
show_insert_completion_menu(matches, selected: nil)
|
|
1933
|
+
state[:index] = :pending_select
|
|
1934
|
+
return
|
|
1935
|
+
end
|
|
1936
|
+
|
|
1937
|
+
if state[:index].nil? && insert_completion_noinsert?
|
|
1938
|
+
preview_idx = direction.positive? ? 0 : matches.length - 1
|
|
1939
|
+
state[:index] = :pending_insert
|
|
1940
|
+
state[:pending_index] = preview_idx
|
|
1941
|
+
show_insert_completion_menu(matches, selected: preview_idx, current: matches[preview_idx])
|
|
1942
|
+
return
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1232
1945
|
idx = state[:index]
|
|
1233
|
-
idx =
|
|
1946
|
+
idx = nil if idx == :pending_select
|
|
1947
|
+
if idx == :pending_insert
|
|
1948
|
+
idx = state.delete(:pending_index) || (direction.positive? ? 0 : matches.length - 1)
|
|
1949
|
+
else
|
|
1950
|
+
idx = idx.nil? ? (direction.positive? ? 0 : matches.length - 1) : (idx + direction) % matches.length
|
|
1951
|
+
end
|
|
1234
1952
|
replacement = matches[idx]
|
|
1235
1953
|
|
|
1236
1954
|
end_col = state[:current_end_col]
|
|
@@ -1241,17 +1959,51 @@ module RuVim
|
|
|
1241
1959
|
@editor.current_window.cursor_x = new_x
|
|
1242
1960
|
state[:index] = idx
|
|
1243
1961
|
state[:current_end_col] = start_col + replacement.length
|
|
1244
|
-
|
|
1962
|
+
if matches.length == 1
|
|
1963
|
+
@editor.echo(replacement)
|
|
1964
|
+
else
|
|
1965
|
+
show_insert_completion_menu(matches, selected: idx, current: replacement)
|
|
1966
|
+
end
|
|
1245
1967
|
rescue StandardError => e
|
|
1246
1968
|
@editor.echo_error("Completion error: #{e.message}")
|
|
1247
1969
|
clear_insert_completion
|
|
1248
1970
|
end
|
|
1249
1971
|
|
|
1972
|
+
def insert_completion_noselect?
|
|
1973
|
+
@editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noselect")
|
|
1974
|
+
end
|
|
1975
|
+
|
|
1976
|
+
def insert_completion_noinsert?
|
|
1977
|
+
@editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }.include?("noinsert")
|
|
1978
|
+
end
|
|
1979
|
+
|
|
1980
|
+
def insert_completion_menu_enabled?
|
|
1981
|
+
opts = @editor.effective_option("completeopt").to_s.split(",").map { |s| s.strip.downcase }
|
|
1982
|
+
opts.include?("menu") || opts.include?("menuone")
|
|
1983
|
+
end
|
|
1984
|
+
|
|
1985
|
+
def show_insert_completion_menu(matches, selected:, current: nil)
|
|
1986
|
+
if insert_completion_menu_enabled?
|
|
1987
|
+
limit = [@editor.effective_option("pumheight").to_i, 1].max
|
|
1988
|
+
items = matches.first(limit).each_with_index.map do |m, i|
|
|
1989
|
+
i == selected ? "[#{m}]" : m
|
|
1990
|
+
end
|
|
1991
|
+
items << "..." if matches.length > limit
|
|
1992
|
+
if current
|
|
1993
|
+
@editor.echo("#{current} (#{selected + 1}/#{matches.length}) | #{items.join(' ')}")
|
|
1994
|
+
else
|
|
1995
|
+
@editor.echo(items.join(" "))
|
|
1996
|
+
end
|
|
1997
|
+
elsif current
|
|
1998
|
+
@editor.echo("#{current} (#{selected + 1}/#{matches.length})")
|
|
1999
|
+
end
|
|
2000
|
+
end
|
|
2001
|
+
|
|
1250
2002
|
def ensure_insert_completion_state
|
|
1251
2003
|
row = @editor.current_window.cursor_y
|
|
1252
2004
|
col = @editor.current_window.cursor_x
|
|
1253
2005
|
line = @editor.current_buffer.line_at(row)
|
|
1254
|
-
prefix = line[0...col].to_s
|
|
2006
|
+
prefix = trailing_keyword_fragment(line[0...col].to_s, @editor.current_window, @editor.current_buffer)
|
|
1255
2007
|
return nil if prefix.nil? || prefix.empty?
|
|
1256
2008
|
|
|
1257
2009
|
start_col = col - prefix.length
|
|
@@ -1280,9 +2032,10 @@ module RuVim
|
|
|
1280
2032
|
def collect_buffer_word_completions(prefix, current_word:)
|
|
1281
2033
|
words = []
|
|
1282
2034
|
seen = {}
|
|
2035
|
+
rx = keyword_scan_regex(@editor.current_window, @editor.current_buffer)
|
|
1283
2036
|
@editor.buffers.values.each do |buf|
|
|
1284
2037
|
buf.lines.each do |line|
|
|
1285
|
-
line.scan(
|
|
2038
|
+
line.scan(rx) do |w|
|
|
1286
2039
|
next unless w.start_with?(prefix)
|
|
1287
2040
|
next if w == current_word
|
|
1288
2041
|
next if seen[w]
|
|
@@ -1295,6 +2048,210 @@ module RuVim
|
|
|
1295
2048
|
words.sort
|
|
1296
2049
|
end
|
|
1297
2050
|
|
|
2051
|
+
def track_mode_transition(mode_before)
|
|
2052
|
+
mode_after = @editor.mode
|
|
2053
|
+
if mode_before != :insert && mode_after == :insert
|
|
2054
|
+
@insert_start_location = @editor.current_location
|
|
2055
|
+
elsif mode_before == :insert && mode_after != :insert
|
|
2056
|
+
@insert_start_location = nil
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
if mode_before != :command_line && mode_after == :command_line
|
|
2060
|
+
@incsearch_preview = nil
|
|
2061
|
+
elsif mode_before == :command_line && mode_after != :command_line
|
|
2062
|
+
@incsearch_preview = nil
|
|
2063
|
+
end
|
|
2064
|
+
end
|
|
2065
|
+
|
|
2066
|
+
def insert_backspace_allowed?
|
|
2067
|
+
buf = @editor.current_buffer
|
|
2068
|
+
win = @editor.current_window
|
|
2069
|
+
row = win.cursor_y
|
|
2070
|
+
col = win.cursor_x
|
|
2071
|
+
return false if row.zero? && col.zero?
|
|
2072
|
+
|
|
2073
|
+
opt = @editor.effective_option("backspace", window: win, buffer: buf).to_s
|
|
2074
|
+
allow = opt.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
2075
|
+
allow_all = allow.include?("2")
|
|
2076
|
+
allow_indent = allow_all || allow.include?("indent")
|
|
2077
|
+
|
|
2078
|
+
if col.zero? && row.positive?
|
|
2079
|
+
return true if allow_all || allow.include?("eol")
|
|
2080
|
+
|
|
2081
|
+
@editor.echo_error("backspace=eol required")
|
|
2082
|
+
return false
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
if @insert_start_location
|
|
2086
|
+
same_buf = @insert_start_location[:buffer_id] == buf.id
|
|
2087
|
+
if same_buf && (row < @insert_start_location[:row] || (row == @insert_start_location[:row] && col <= @insert_start_location[:col]))
|
|
2088
|
+
if allow_all || allow.include?("start")
|
|
2089
|
+
return true
|
|
2090
|
+
end
|
|
2091
|
+
|
|
2092
|
+
if allow_indent && same_row_autoindent_backspace?(buf, row, col)
|
|
2093
|
+
return true
|
|
2094
|
+
end
|
|
2095
|
+
|
|
2096
|
+
@editor.echo_error("backspace=start required")
|
|
2097
|
+
return false
|
|
2098
|
+
end
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
true
|
|
2102
|
+
end
|
|
2103
|
+
|
|
2104
|
+
def insert_backspace_in_insert_mode
|
|
2105
|
+
buf = @editor.current_buffer
|
|
2106
|
+
win = @editor.current_window
|
|
2107
|
+
row = win.cursor_y
|
|
2108
|
+
col = win.cursor_x
|
|
2109
|
+
|
|
2110
|
+
if row >= 0 && col.positive? && try_softtabstop_backspace(buf, win)
|
|
2111
|
+
return
|
|
2112
|
+
end
|
|
2113
|
+
|
|
2114
|
+
y, x = buf.backspace(row, col)
|
|
2115
|
+
win.cursor_y = y
|
|
2116
|
+
win.cursor_x = x
|
|
2117
|
+
end
|
|
2118
|
+
|
|
2119
|
+
def dispatch_insert_cursor_motion(id)
|
|
2120
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: id, count: 1))
|
|
2121
|
+
rescue StandardError => e
|
|
2122
|
+
@editor.echo_error("Motion error: #{e.message}")
|
|
2123
|
+
end
|
|
2124
|
+
|
|
2125
|
+
def try_softtabstop_backspace(buf, win)
|
|
2126
|
+
row = win.cursor_y
|
|
2127
|
+
col = win.cursor_x
|
|
2128
|
+
line = buf.line_at(row)
|
|
2129
|
+
return false unless line
|
|
2130
|
+
return false unless @editor.effective_option("expandtab", window: win, buffer: buf)
|
|
2131
|
+
|
|
2132
|
+
sts = @editor.effective_option("softtabstop", window: win, buffer: buf).to_i
|
|
2133
|
+
sts = @editor.effective_option("tabstop", window: win, buffer: buf).to_i if sts <= 0
|
|
2134
|
+
return false if sts <= 0
|
|
2135
|
+
|
|
2136
|
+
prefix = line[0...col].to_s
|
|
2137
|
+
m = prefix.match(/ +\z/)
|
|
2138
|
+
return false unless m
|
|
2139
|
+
|
|
2140
|
+
run = m[0].length
|
|
2141
|
+
return false if run <= 1
|
|
2142
|
+
|
|
2143
|
+
tabstop = effective_tabstop(win, buf)
|
|
2144
|
+
cur_screen = RuVim::TextMetrics.screen_col_for_char_index(line, col, tabstop:)
|
|
2145
|
+
target_screen = [cur_screen - sts, 0].max
|
|
2146
|
+
target_col = RuVim::TextMetrics.char_index_for_screen_col(line, target_screen, tabstop:, align: :floor)
|
|
2147
|
+
delete_cols = col - target_col
|
|
2148
|
+
delete_cols = [delete_cols, run, sts].min
|
|
2149
|
+
return false if delete_cols <= 1
|
|
2150
|
+
|
|
2151
|
+
# Only collapse whitespace run; if target lands before the run, clamp to run start.
|
|
2152
|
+
run_start = col - run
|
|
2153
|
+
target_col = [target_col, run_start].max
|
|
2154
|
+
delete_cols = col - target_col
|
|
2155
|
+
return false if delete_cols <= 1
|
|
2156
|
+
|
|
2157
|
+
buf.delete_span(row, target_col, row, col)
|
|
2158
|
+
win.cursor_x = target_col
|
|
2159
|
+
true
|
|
2160
|
+
rescue StandardError
|
|
2161
|
+
false
|
|
2162
|
+
end
|
|
2163
|
+
|
|
2164
|
+
def same_row_autoindent_backspace?(buf, row, col)
|
|
2165
|
+
return false unless @insert_start_location
|
|
2166
|
+
return false unless row == @insert_start_location[:row]
|
|
2167
|
+
return false unless col <= @insert_start_location[:col]
|
|
2168
|
+
|
|
2169
|
+
line = buf.line_at(row)
|
|
2170
|
+
line[0...@insert_start_location[:col]].to_s.match?(/\A[ \t]*\z/)
|
|
2171
|
+
rescue StandardError
|
|
2172
|
+
false
|
|
2173
|
+
end
|
|
2174
|
+
|
|
2175
|
+
def incsearch_enabled?
|
|
2176
|
+
return false unless @editor.command_line_active?
|
|
2177
|
+
return false unless ["/", "?"].include?(@editor.command_line.prefix)
|
|
2178
|
+
|
|
2179
|
+
!!@editor.effective_option("incsearch")
|
|
2180
|
+
end
|
|
2181
|
+
|
|
2182
|
+
def update_incsearch_preview_if_needed
|
|
2183
|
+
return unless incsearch_enabled?
|
|
2184
|
+
|
|
2185
|
+
cmd = @editor.command_line
|
|
2186
|
+
ensure_incsearch_preview_origin!(direction: (cmd.prefix == "/" ? :forward : :backward))
|
|
2187
|
+
pattern = cmd.text.to_s
|
|
2188
|
+
if pattern.empty?
|
|
2189
|
+
clear_incsearch_preview_state(apply: false)
|
|
2190
|
+
return
|
|
2191
|
+
end
|
|
2192
|
+
|
|
2193
|
+
buf = @editor.current_buffer
|
|
2194
|
+
win = @editor.current_window
|
|
2195
|
+
origin = @incsearch_preview[:origin]
|
|
2196
|
+
tmp_window = RuVim::Window.new(id: -1, buffer_id: buf.id)
|
|
2197
|
+
tmp_window.cursor_y = origin[:row]
|
|
2198
|
+
tmp_window.cursor_x = origin[:col]
|
|
2199
|
+
regex = GlobalCommands.instance.send(:compile_search_regex, pattern, editor: @editor, window: win, buffer: buf)
|
|
2200
|
+
match = GlobalCommands.instance.send(:find_next_match, buf, tmp_window, regex, direction: @incsearch_preview[:direction])
|
|
2201
|
+
if match
|
|
2202
|
+
win.cursor_y = match[:row]
|
|
2203
|
+
win.cursor_x = match[:col]
|
|
2204
|
+
win.clamp_to_buffer(buf)
|
|
2205
|
+
end
|
|
2206
|
+
@incsearch_preview[:active] = true
|
|
2207
|
+
rescue RuVim::CommandError, RegexpError
|
|
2208
|
+
# Keep editing command-line without forcing an error flash on every keystroke.
|
|
2209
|
+
end
|
|
2210
|
+
|
|
2211
|
+
def ensure_incsearch_preview_origin!(direction:)
|
|
2212
|
+
return if @incsearch_preview
|
|
2213
|
+
|
|
2214
|
+
@incsearch_preview = {
|
|
2215
|
+
origin: @editor.current_location,
|
|
2216
|
+
direction: direction,
|
|
2217
|
+
active: false
|
|
2218
|
+
}
|
|
2219
|
+
end
|
|
2220
|
+
|
|
2221
|
+
def cancel_incsearch_preview_if_any
|
|
2222
|
+
clear_incsearch_preview_state(apply: false)
|
|
2223
|
+
end
|
|
2224
|
+
|
|
2225
|
+
def clear_incsearch_preview_state(apply:)
|
|
2226
|
+
return unless @incsearch_preview
|
|
2227
|
+
|
|
2228
|
+
if !apply && @incsearch_preview[:origin]
|
|
2229
|
+
@editor.jump_to_location(@incsearch_preview[:origin])
|
|
2230
|
+
end
|
|
2231
|
+
@incsearch_preview = nil
|
|
2232
|
+
end
|
|
2233
|
+
|
|
2234
|
+
def trailing_keyword_fragment(prefix_text, window, buffer)
|
|
2235
|
+
cls = keyword_char_class(window, buffer)
|
|
2236
|
+
prefix_text.to_s[/[#{cls}]+\z/]
|
|
2237
|
+
rescue RegexpError
|
|
2238
|
+
prefix_text.to_s[/[[:alnum:]_]+\z/]
|
|
2239
|
+
end
|
|
2240
|
+
|
|
2241
|
+
def keyword_scan_regex(window, buffer)
|
|
2242
|
+
cls = keyword_char_class(window, buffer)
|
|
2243
|
+
/[#{cls}]+/
|
|
2244
|
+
rescue RegexpError
|
|
2245
|
+
/[[:alnum:]_]+/
|
|
2246
|
+
end
|
|
2247
|
+
|
|
2248
|
+
def keyword_char_class(window, buffer)
|
|
2249
|
+
raw = @editor.effective_option("iskeyword", window:, buffer:).to_s
|
|
2250
|
+
RuVim::KeywordChars.char_class(raw)
|
|
2251
|
+
rescue StandardError
|
|
2252
|
+
"[:alnum:]_"
|
|
2253
|
+
end
|
|
2254
|
+
|
|
1298
2255
|
def ex_completion_context(cmd)
|
|
1299
2256
|
text = cmd.text
|
|
1300
2257
|
cursor = cmd.cursor
|
|
@@ -1363,18 +2320,45 @@ module RuVim
|
|
|
1363
2320
|
else
|
|
1364
2321
|
File.dirname(input)
|
|
1365
2322
|
end
|
|
1366
|
-
base_dir = "." if base_dir == "."
|
|
1367
2323
|
partial = input.end_with?("/") ? "" : File.basename(input)
|
|
1368
|
-
pattern =
|
|
1369
|
-
|
|
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|
|
|
1370
2334
|
next if [".", ".."].include?(File.basename(p))
|
|
1371
2335
|
next unless p.start_with?(input) || input.empty?
|
|
2336
|
+
next if wildignore_path?(p)
|
|
1372
2337
|
File.directory?(p) ? "#{p}/" : p
|
|
1373
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
|
|
1374
2344
|
rescue StandardError
|
|
1375
2345
|
[]
|
|
1376
2346
|
end
|
|
1377
2347
|
|
|
2348
|
+
def wildignore_path?(path)
|
|
2349
|
+
spec = @editor.global_options["wildignore"].to_s
|
|
2350
|
+
return false if spec.empty?
|
|
2351
|
+
|
|
2352
|
+
flags = @editor.global_options["wildignorecase"] ? File::FNM_CASEFOLD : 0
|
|
2353
|
+
name = path.to_s
|
|
2354
|
+
base = File.basename(name)
|
|
2355
|
+
spec.split(",").map(&:strip).reject(&:empty?).any? do |pat|
|
|
2356
|
+
File.fnmatch?(pat, name, flags) || File.fnmatch?(pat, base, flags)
|
|
2357
|
+
end
|
|
2358
|
+
rescue StandardError
|
|
2359
|
+
false
|
|
2360
|
+
end
|
|
2361
|
+
|
|
1378
2362
|
def buffer_completion_candidates(prefix)
|
|
1379
2363
|
pfx = prefix.to_s
|
|
1380
2364
|
items = @editor.buffers.values.flat_map do |b|
|
|
@@ -1538,6 +2522,13 @@ module RuVim
|
|
|
1538
2522
|
list = Array(paths).compact
|
|
1539
2523
|
return if list.empty?
|
|
1540
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
|
+
|
|
1541
2532
|
first, *rest = list
|
|
1542
2533
|
@editor.open_path(first)
|
|
1543
2534
|
apply_startup_readonly! if @startup_readonly
|
|
@@ -1545,20 +2536,38 @@ module RuVim
|
|
|
1545
2536
|
|
|
1546
2537
|
case @startup_open_layout
|
|
1547
2538
|
when :horizontal
|
|
2539
|
+
first_win_id = @editor.current_window_id
|
|
1548
2540
|
rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
|
|
2541
|
+
@editor.focus_window(first_win_id)
|
|
1549
2542
|
when :vertical
|
|
2543
|
+
first_win_id = @editor.current_window_id
|
|
1550
2544
|
rest.each { |p| open_path_in_split!(p, layout: :vertical) }
|
|
2545
|
+
@editor.focus_window(first_win_id)
|
|
1551
2546
|
when :tab
|
|
1552
2547
|
rest.each { |p| open_path_in_tab!(p) }
|
|
2548
|
+
@editor.tabnext(-(@editor.tabpage_count - 1))
|
|
1553
2549
|
else
|
|
1554
|
-
#
|
|
2550
|
+
# Load remaining files as buffers (Vim-like behavior).
|
|
2551
|
+
rest.each { |p| @editor.add_buffer_from_file(p) }
|
|
2552
|
+
end
|
|
2553
|
+
end
|
|
2554
|
+
|
|
2555
|
+
# Remove the bootstrap empty buffer before opening real files,
|
|
2556
|
+
# resetting the buffer ID counter so the first file gets id 1.
|
|
2557
|
+
def evict_bootstrap_buffer!
|
|
2558
|
+
bid = @editor.buffer_ids.find do |id|
|
|
2559
|
+
b = @editor.buffers[id]
|
|
2560
|
+
b.path.nil? && !b.modified? && b.line_count <= 1 && b.kind == :file
|
|
1555
2561
|
end
|
|
2562
|
+
return unless bid
|
|
2563
|
+
|
|
2564
|
+
@editor.buffers.delete(bid)
|
|
2565
|
+
@editor.instance_variable_set(:@next_buffer_id, 1)
|
|
1556
2566
|
end
|
|
1557
2567
|
|
|
1558
2568
|
def open_path_in_split!(path, layout:)
|
|
1559
2569
|
@editor.split_current_window(layout:)
|
|
1560
|
-
|
|
1561
|
-
@editor.switch_to_buffer(buf.id)
|
|
2570
|
+
@editor.open_path(path)
|
|
1562
2571
|
apply_startup_readonly! if @startup_readonly
|
|
1563
2572
|
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
1564
2573
|
end
|
|
@@ -1569,6 +2578,368 @@ module RuVim
|
|
|
1569
2578
|
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
1570
2579
|
end
|
|
1571
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
|
+
|
|
1572
2943
|
def move_cursor_to_line(line_number)
|
|
1573
2944
|
win = @editor.current_window
|
|
1574
2945
|
buf = @editor.current_buffer
|