ruvim 0.1.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 +7 -0
- data/.github/workflows/test.yml +15 -0
- data/README.md +135 -0
- data/Rakefile +36 -0
- data/docs/binding.md +125 -0
- data/docs/command.md +306 -0
- data/docs/config.md +155 -0
- data/docs/done.md +112 -0
- data/docs/plugin.md +559 -0
- data/docs/spec.md +655 -0
- data/docs/todo.md +63 -0
- data/docs/tutorial.md +490 -0
- data/docs/vim_diff.md +179 -0
- data/exe/ruvim +6 -0
- data/lib/ruvim/app.rb +1600 -0
- data/lib/ruvim/buffer.rb +421 -0
- data/lib/ruvim/cli.rb +264 -0
- data/lib/ruvim/clipboard.rb +73 -0
- data/lib/ruvim/command_invocation.rb +14 -0
- data/lib/ruvim/command_line.rb +63 -0
- data/lib/ruvim/command_registry.rb +38 -0
- data/lib/ruvim/config_dsl.rb +134 -0
- data/lib/ruvim/config_loader.rb +68 -0
- data/lib/ruvim/context.rb +26 -0
- data/lib/ruvim/dispatcher.rb +120 -0
- data/lib/ruvim/display_width.rb +110 -0
- data/lib/ruvim/editor.rb +1025 -0
- data/lib/ruvim/ex_command_registry.rb +80 -0
- data/lib/ruvim/global_commands.rb +1889 -0
- data/lib/ruvim/highlighter.rb +52 -0
- data/lib/ruvim/input.rb +66 -0
- data/lib/ruvim/keymap_manager.rb +96 -0
- data/lib/ruvim/screen.rb +452 -0
- data/lib/ruvim/terminal.rb +30 -0
- data/lib/ruvim/text_metrics.rb +96 -0
- data/lib/ruvim/version.rb +5 -0
- data/lib/ruvim/window.rb +71 -0
- data/lib/ruvim.rb +30 -0
- data/sig/ruvim.rbs +4 -0
- data/test/app_completion_test.rb +39 -0
- data/test/app_dot_repeat_test.rb +54 -0
- data/test/app_motion_test.rb +73 -0
- data/test/app_register_test.rb +47 -0
- data/test/app_scenario_test.rb +77 -0
- data/test/app_startup_test.rb +199 -0
- data/test/app_text_object_test.rb +54 -0
- data/test/app_unicode_behavior_test.rb +66 -0
- data/test/buffer_test.rb +72 -0
- data/test/cli_test.rb +165 -0
- data/test/config_dsl_test.rb +78 -0
- data/test/dispatcher_test.rb +124 -0
- data/test/editor_mark_test.rb +69 -0
- data/test/editor_register_test.rb +64 -0
- data/test/fixtures/render_basic_snapshot.txt +8 -0
- data/test/fixtures/render_basic_snapshot_nonumber.txt +8 -0
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +7 -0
- data/test/highlighter_test.rb +16 -0
- data/test/input_screen_integration_test.rb +69 -0
- data/test/keymap_manager_test.rb +48 -0
- data/test/render_snapshot_test.rb +70 -0
- data/test/screen_test.rb +123 -0
- data/test/search_option_test.rb +39 -0
- data/test/test_helper.rb +15 -0
- data/test/text_metrics_test.rb +42 -0
- data/test/window_test.rb +21 -0
- metadata +106 -0
data/lib/ruvim/app.rb
ADDED
|
@@ -0,0 +1,1600 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class App
|
|
3
|
+
def initialize(path: nil, paths: nil, stdin: STDIN, 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)
|
|
4
|
+
@editor = Editor.new
|
|
5
|
+
@terminal = Terminal.new(stdin:, stdout:)
|
|
6
|
+
@input = Input.new(stdin:)
|
|
7
|
+
@screen = Screen.new(terminal: @terminal)
|
|
8
|
+
@dispatcher = Dispatcher.new
|
|
9
|
+
@keymaps = KeymapManager.new
|
|
10
|
+
@signal_r, @signal_w = IO.pipe
|
|
11
|
+
@cmdline_history = Hash.new { |h, k| h[k] = [] }
|
|
12
|
+
@cmdline_history_index = nil
|
|
13
|
+
@needs_redraw = true
|
|
14
|
+
@clean_mode = clean
|
|
15
|
+
@skip_user_config = skip_user_config
|
|
16
|
+
@config_path = config_path
|
|
17
|
+
@startup_readonly = readonly
|
|
18
|
+
@startup_diff_mode = diff_mode
|
|
19
|
+
@startup_quickfix_errorfile = quickfix_errorfile
|
|
20
|
+
@startup_session_file = session_file
|
|
21
|
+
@startup_nomodifiable = nomodifiable
|
|
22
|
+
@restricted_mode = restricted
|
|
23
|
+
@verbose_level = verbose_level.to_i
|
|
24
|
+
@verbose_io = verbose_io
|
|
25
|
+
@startup_time_path = startup_time_path
|
|
26
|
+
@startup_time_origin = monotonic_now
|
|
27
|
+
@startup_timeline = []
|
|
28
|
+
@startup_open_layout = startup_open_layout
|
|
29
|
+
@startup_open_count = startup_open_count
|
|
30
|
+
@editor.restricted_mode = @restricted_mode
|
|
31
|
+
|
|
32
|
+
startup_mark("init.start")
|
|
33
|
+
register_builtins!
|
|
34
|
+
bind_default_keys!
|
|
35
|
+
init_config_loader!
|
|
36
|
+
@editor.ensure_bootstrap_buffer!
|
|
37
|
+
verbose_log(1, "startup: run_pre_config_actions count=#{Array(pre_config_actions).length}")
|
|
38
|
+
run_startup_actions!(pre_config_actions, log_prefix: "pre-config")
|
|
39
|
+
startup_mark("pre_config_actions.done")
|
|
40
|
+
verbose_log(1, "startup: load_user_config")
|
|
41
|
+
load_user_config!
|
|
42
|
+
startup_mark("config.loaded")
|
|
43
|
+
install_signal_handlers
|
|
44
|
+
startup_mark("signals.installed")
|
|
45
|
+
|
|
46
|
+
startup_paths = Array(paths || path).compact
|
|
47
|
+
if startup_paths.empty?
|
|
48
|
+
verbose_log(1, "startup: intro")
|
|
49
|
+
@editor.show_intro_buffer_if_applicable!
|
|
50
|
+
else
|
|
51
|
+
verbose_log(1, "startup: open_paths #{startup_paths.inspect} layout=#{@startup_open_layout || :single}")
|
|
52
|
+
open_startup_paths!(startup_paths)
|
|
53
|
+
end
|
|
54
|
+
startup_mark("buffers.opened")
|
|
55
|
+
verbose_log(1, "startup: load_current_ftplugin")
|
|
56
|
+
load_current_ftplugin!
|
|
57
|
+
startup_mark("ftplugin.loaded")
|
|
58
|
+
apply_startup_compat_mode_messages!
|
|
59
|
+
verbose_log(1, "startup: run_startup_actions count=#{Array(startup_actions).length}")
|
|
60
|
+
run_startup_actions!(startup_actions)
|
|
61
|
+
startup_mark("startup_actions.done")
|
|
62
|
+
write_startuptime_log!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run
|
|
66
|
+
@terminal.with_ui do
|
|
67
|
+
loop do
|
|
68
|
+
if @needs_redraw
|
|
69
|
+
@screen.render(@editor)
|
|
70
|
+
@needs_redraw = false
|
|
71
|
+
end
|
|
72
|
+
break unless @editor.running?
|
|
73
|
+
|
|
74
|
+
key = @input.read_key(wakeup_ios: [@signal_r])
|
|
75
|
+
next if key.nil?
|
|
76
|
+
|
|
77
|
+
handle_key(key)
|
|
78
|
+
@needs_redraw = true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_startup_actions!(actions, log_prefix: "startup")
|
|
84
|
+
Array(actions).each do |action|
|
|
85
|
+
run_startup_action!(action, log_prefix:)
|
|
86
|
+
break unless @editor.running?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def register_builtins!
|
|
93
|
+
cmd = CommandRegistry.instance
|
|
94
|
+
ex = ExCommandRegistry.instance
|
|
95
|
+
|
|
96
|
+
register_internal_unless(cmd, "cursor.left", call: :cursor_left, desc: "Move cursor left")
|
|
97
|
+
register_internal_unless(cmd, "cursor.right", call: :cursor_right, desc: "Move cursor right")
|
|
98
|
+
register_internal_unless(cmd, "cursor.up", call: :cursor_up, desc: "Move cursor up")
|
|
99
|
+
register_internal_unless(cmd, "cursor.down", call: :cursor_down, desc: "Move cursor down")
|
|
100
|
+
register_internal_unless(cmd, "cursor.page_up", call: :cursor_page_up, desc: "Move one page up")
|
|
101
|
+
register_internal_unless(cmd, "cursor.page_down", call: :cursor_page_down, desc: "Move one page down")
|
|
102
|
+
register_internal_unless(cmd, "cursor.line_start", call: :cursor_line_start, desc: "Move to column 1")
|
|
103
|
+
register_internal_unless(cmd, "cursor.line_end", call: :cursor_line_end, desc: "Move to end of line")
|
|
104
|
+
register_internal_unless(cmd, "cursor.first_nonblank", call: :cursor_first_nonblank, desc: "Move to first nonblank")
|
|
105
|
+
register_internal_unless(cmd, "cursor.buffer_start", call: :cursor_buffer_start, desc: "Move to start of buffer")
|
|
106
|
+
register_internal_unless(cmd, "cursor.buffer_end", call: :cursor_buffer_end, desc: "Move to end of buffer")
|
|
107
|
+
register_internal_unless(cmd, "cursor.word_forward", call: :cursor_word_forward, desc: "Move to next word")
|
|
108
|
+
register_internal_unless(cmd, "cursor.word_backward", call: :cursor_word_backward, desc: "Move to previous word")
|
|
109
|
+
register_internal_unless(cmd, "cursor.word_end", call: :cursor_word_end, desc: "Move to end of word")
|
|
110
|
+
register_internal_unless(cmd, "cursor.match_bracket", call: :cursor_match_bracket, desc: "Jump to matching bracket")
|
|
111
|
+
register_internal_unless(cmd, "mode.insert", call: :enter_insert_mode, desc: "Enter insert mode")
|
|
112
|
+
register_internal_unless(cmd, "mode.append", call: :append_mode, desc: "Append after cursor")
|
|
113
|
+
register_internal_unless(cmd, "mode.append_line_end", call: :append_line_end_mode, desc: "Append at line end")
|
|
114
|
+
register_internal_unless(cmd, "mode.insert_nonblank", call: :insert_line_start_nonblank_mode, desc: "Insert at first nonblank")
|
|
115
|
+
register_internal_unless(cmd, "mode.open_below", call: :open_line_below, desc: "Open line below")
|
|
116
|
+
register_internal_unless(cmd, "mode.open_above", call: :open_line_above, desc: "Open line above")
|
|
117
|
+
register_internal_unless(cmd, "mode.visual_char", call: :enter_visual_char_mode, desc: "Enter visual char mode")
|
|
118
|
+
register_internal_unless(cmd, "mode.visual_line", call: :enter_visual_line_mode, desc: "Enter visual line mode")
|
|
119
|
+
register_internal_unless(cmd, "mode.visual_block", call: :enter_visual_block_mode, desc: "Enter visual block mode")
|
|
120
|
+
register_internal_unless(cmd, "window.split", call: :window_split, desc: "Horizontal split")
|
|
121
|
+
register_internal_unless(cmd, "window.vsplit", call: :window_vsplit, desc: "Vertical split")
|
|
122
|
+
register_internal_unless(cmd, "window.focus_next", call: :window_focus_next, desc: "Next window")
|
|
123
|
+
register_internal_unless(cmd, "window.focus_left", call: :window_focus_left, desc: "Focus left window")
|
|
124
|
+
register_internal_unless(cmd, "window.focus_right", call: :window_focus_right, desc: "Focus right window")
|
|
125
|
+
register_internal_unless(cmd, "window.focus_up", call: :window_focus_up, desc: "Focus upper window")
|
|
126
|
+
register_internal_unless(cmd, "window.focus_down", call: :window_focus_down, desc: "Focus lower window")
|
|
127
|
+
register_internal_unless(cmd, "mode.command_line", call: :enter_command_line_mode, desc: "Enter command-line mode")
|
|
128
|
+
register_internal_unless(cmd, "mode.search_forward", call: :enter_search_forward_mode, desc: "Enter / search")
|
|
129
|
+
register_internal_unless(cmd, "mode.search_backward", call: :enter_search_backward_mode, desc: "Enter ? search")
|
|
130
|
+
register_internal_unless(cmd, "buffer.delete_char", call: :delete_char, desc: "Delete char under cursor")
|
|
131
|
+
register_internal_unless(cmd, "buffer.delete_line", call: :delete_line, desc: "Delete current line")
|
|
132
|
+
register_internal_unless(cmd, "buffer.delete_motion", call: :delete_motion, desc: "Delete by motion")
|
|
133
|
+
register_internal_unless(cmd, "buffer.change_motion", call: :change_motion, desc: "Change by motion")
|
|
134
|
+
register_internal_unless(cmd, "buffer.change_line", call: :change_line, desc: "Change line(s)")
|
|
135
|
+
register_internal_unless(cmd, "buffer.yank_line", call: :yank_line, desc: "Yank line(s)")
|
|
136
|
+
register_internal_unless(cmd, "buffer.yank_motion", call: :yank_motion, desc: "Yank by motion")
|
|
137
|
+
register_internal_unless(cmd, "buffer.paste_after", call: :paste_after, desc: "Paste after")
|
|
138
|
+
register_internal_unless(cmd, "buffer.paste_before", call: :paste_before, desc: "Paste before")
|
|
139
|
+
register_internal_unless(cmd, "buffer.visual_yank", call: :visual_yank, desc: "Yank visual selection")
|
|
140
|
+
register_internal_unless(cmd, "buffer.visual_delete", call: :visual_delete, desc: "Delete visual selection")
|
|
141
|
+
register_internal_unless(cmd, "buffer.visual_select_text_object", call: :visual_select_text_object, desc: "Select visual text object")
|
|
142
|
+
register_internal_unless(cmd, "buffer.undo", call: :buffer_undo, desc: "Undo")
|
|
143
|
+
register_internal_unless(cmd, "buffer.redo", call: :buffer_redo, desc: "Redo")
|
|
144
|
+
register_internal_unless(cmd, "search.next", call: :search_next, desc: "Repeat search")
|
|
145
|
+
register_internal_unless(cmd, "search.prev", call: :search_prev, desc: "Repeat search backward")
|
|
146
|
+
register_internal_unless(cmd, "search.word_forward", call: :search_word_forward, desc: "Search word forward")
|
|
147
|
+
register_internal_unless(cmd, "search.word_backward", call: :search_word_backward, desc: "Search word backward")
|
|
148
|
+
register_internal_unless(cmd, "search.word_forward_partial", call: :search_word_forward_partial, desc: "Search partial word forward")
|
|
149
|
+
register_internal_unless(cmd, "search.word_backward_partial", call: :search_word_backward_partial, desc: "Search partial word backward")
|
|
150
|
+
register_internal_unless(cmd, "mark.set", call: :mark_set, desc: "Set mark")
|
|
151
|
+
register_internal_unless(cmd, "mark.jump", call: :mark_jump, desc: "Jump to mark")
|
|
152
|
+
register_internal_unless(cmd, "jump.older", call: :jump_older, desc: "Jump older")
|
|
153
|
+
register_internal_unless(cmd, "jump.newer", call: :jump_newer, desc: "Jump newer")
|
|
154
|
+
register_internal_unless(cmd, "editor.buffer_next", call: :buffer_next, desc: "Next buffer")
|
|
155
|
+
register_internal_unless(cmd, "editor.buffer_prev", call: :buffer_prev, desc: "Previous buffer")
|
|
156
|
+
register_internal_unless(cmd, "buffer.replace_char", call: :replace_char, desc: "Replace single char")
|
|
157
|
+
register_internal_unless(cmd, "ui.clear_message", call: :clear_message, desc: "Clear message")
|
|
158
|
+
|
|
159
|
+
register_ex_unless(ex, "w", call: :file_write, aliases: %w[write], desc: "Write current buffer", nargs: :maybe_one, bang: true)
|
|
160
|
+
register_ex_unless(ex, "q", call: :app_quit, aliases: %w[quit], desc: "Quit", nargs: 0, bang: true)
|
|
161
|
+
register_ex_unless(ex, "wq", call: :file_write_quit, desc: "Write and quit", nargs: :maybe_one, bang: true)
|
|
162
|
+
register_ex_unless(ex, "e", call: :file_edit, aliases: %w[edit], desc: "Edit file / reload", nargs: :maybe_one, bang: true)
|
|
163
|
+
register_ex_unless(ex, "help", call: :ex_help, desc: "Show help / topics", nargs: :any)
|
|
164
|
+
register_ex_unless(ex, "command", call: :ex_define_command, desc: "Define user command", nargs: :any, bang: true)
|
|
165
|
+
register_ex_unless(ex, "ruby", call: :ex_ruby, aliases: %w[rb], desc: "Evaluate Ruby", nargs: :any, bang: false)
|
|
166
|
+
register_ex_unless(ex, "ls", call: :buffer_list, aliases: %w[buffers], desc: "List buffers", nargs: 0)
|
|
167
|
+
register_ex_unless(ex, "bnext", call: :buffer_next, aliases: %w[bn], desc: "Next buffer", nargs: 0, bang: true)
|
|
168
|
+
register_ex_unless(ex, "bprev", call: :buffer_prev, aliases: %w[bp], desc: "Previous buffer", nargs: 0, bang: true)
|
|
169
|
+
register_ex_unless(ex, "buffer", call: :buffer_switch, aliases: %w[b], desc: "Switch buffer", nargs: 1, bang: true)
|
|
170
|
+
register_ex_unless(ex, "commands", call: :ex_commands, desc: "List Ex commands", nargs: 0)
|
|
171
|
+
register_ex_unless(ex, "set", call: :ex_set, desc: "Set options", nargs: :any)
|
|
172
|
+
register_ex_unless(ex, "setlocal", call: :ex_setlocal, desc: "Set window/buffer local option", nargs: :any)
|
|
173
|
+
register_ex_unless(ex, "setglobal", call: :ex_setglobal, desc: "Set global option", nargs: :any)
|
|
174
|
+
register_ex_unless(ex, "split", call: :window_split, desc: "Horizontal split", nargs: 0)
|
|
175
|
+
register_ex_unless(ex, "vsplit", call: :window_vsplit, desc: "Vertical split", nargs: 0)
|
|
176
|
+
register_ex_unless(ex, "tabnew", call: :tab_new, desc: "New tab", nargs: :maybe_one)
|
|
177
|
+
register_ex_unless(ex, "tabnext", call: :tab_next, aliases: %w[tabn], desc: "Next tab", nargs: 0)
|
|
178
|
+
register_ex_unless(ex, "tabprev", call: :tab_prev, aliases: %w[tabp], desc: "Prev tab", nargs: 0)
|
|
179
|
+
register_ex_unless(ex, "vimgrep", call: :ex_vimgrep, desc: "Populate quickfix from regex (minimal)", nargs: :any)
|
|
180
|
+
register_ex_unless(ex, "lvimgrep", call: :ex_lvimgrep, desc: "Populate location list from regex (minimal)", nargs: :any)
|
|
181
|
+
register_ex_unless(ex, "copen", call: :ex_copen, desc: "Open quickfix list", nargs: 0)
|
|
182
|
+
register_ex_unless(ex, "cclose", call: :ex_cclose, desc: "Close quickfix window", nargs: 0)
|
|
183
|
+
register_ex_unless(ex, "cnext", call: :ex_cnext, aliases: %w[cn], desc: "Next quickfix item", nargs: 0)
|
|
184
|
+
register_ex_unless(ex, "cprev", call: :ex_cprev, aliases: %w[cp], desc: "Prev quickfix item", nargs: 0)
|
|
185
|
+
register_ex_unless(ex, "lopen", call: :ex_lopen, desc: "Open location list", nargs: 0)
|
|
186
|
+
register_ex_unless(ex, "lclose", call: :ex_lclose, desc: "Close location list window", nargs: 0)
|
|
187
|
+
register_ex_unless(ex, "lnext", call: :ex_lnext, aliases: %w[ln], desc: "Next location item", nargs: 0)
|
|
188
|
+
register_ex_unless(ex, "lprev", call: :ex_lprev, aliases: %w[lp], desc: "Prev location item", nargs: 0)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def bind_default_keys!
|
|
192
|
+
@keymaps.bind(:normal, "h", "cursor.left")
|
|
193
|
+
@keymaps.bind(:normal, "j", "cursor.down")
|
|
194
|
+
@keymaps.bind(:normal, "k", "cursor.up")
|
|
195
|
+
@keymaps.bind(:normal, "l", "cursor.right")
|
|
196
|
+
@keymaps.bind(:normal, "0", "cursor.line_start")
|
|
197
|
+
@keymaps.bind(:normal, "$", "cursor.line_end")
|
|
198
|
+
@keymaps.bind(:normal, "^", "cursor.first_nonblank")
|
|
199
|
+
@keymaps.bind(:normal, "gg", "cursor.buffer_start")
|
|
200
|
+
@keymaps.bind(:normal, "G", "cursor.buffer_end")
|
|
201
|
+
@keymaps.bind(:normal, "w", "cursor.word_forward")
|
|
202
|
+
@keymaps.bind(:normal, "b", "cursor.word_backward")
|
|
203
|
+
@keymaps.bind(:normal, "e", "cursor.word_end")
|
|
204
|
+
@keymaps.bind(:normal, "%", "cursor.match_bracket")
|
|
205
|
+
@keymaps.bind(:normal, "i", "mode.insert")
|
|
206
|
+
@keymaps.bind(:normal, "a", "mode.append")
|
|
207
|
+
@keymaps.bind(:normal, "A", "mode.append_line_end")
|
|
208
|
+
@keymaps.bind(:normal, "I", "mode.insert_nonblank")
|
|
209
|
+
@keymaps.bind(:normal, "o", "mode.open_below")
|
|
210
|
+
@keymaps.bind(:normal, "O", "mode.open_above")
|
|
211
|
+
@keymaps.bind(:normal, "v", "mode.visual_char")
|
|
212
|
+
@keymaps.bind(:normal, "V", "mode.visual_line")
|
|
213
|
+
@keymaps.bind(:normal, ["<C-v>"], "mode.visual_block")
|
|
214
|
+
@keymaps.bind(:normal, ["<C-w>", "w"], "window.focus_next")
|
|
215
|
+
@keymaps.bind(:normal, ["<C-w>", "h"], "window.focus_left")
|
|
216
|
+
@keymaps.bind(:normal, ["<C-w>", "j"], "window.focus_down")
|
|
217
|
+
@keymaps.bind(:normal, ["<C-w>", "k"], "window.focus_up")
|
|
218
|
+
@keymaps.bind(:normal, ["<C-w>", "l"], "window.focus_right")
|
|
219
|
+
@keymaps.bind(:normal, ":", "mode.command_line")
|
|
220
|
+
@keymaps.bind(:normal, "/", "mode.search_forward")
|
|
221
|
+
@keymaps.bind(:normal, "?", "mode.search_backward")
|
|
222
|
+
@keymaps.bind(:normal, "x", "buffer.delete_char")
|
|
223
|
+
@keymaps.bind(:normal, "p", "buffer.paste_after")
|
|
224
|
+
@keymaps.bind(:normal, "P", "buffer.paste_before")
|
|
225
|
+
@keymaps.bind(:normal, "u", "buffer.undo")
|
|
226
|
+
@keymaps.bind(:normal, ["<C-r>"], "buffer.redo")
|
|
227
|
+
@keymaps.bind(:normal, ["<C-o>"], "jump.older")
|
|
228
|
+
@keymaps.bind(:normal, ["<C-i>"], "jump.newer")
|
|
229
|
+
@keymaps.bind(:normal, "n", "search.next")
|
|
230
|
+
@keymaps.bind(:normal, "N", "search.prev")
|
|
231
|
+
@keymaps.bind(:normal, "*", "search.word_forward")
|
|
232
|
+
@keymaps.bind(:normal, "#", "search.word_backward")
|
|
233
|
+
@keymaps.bind(:normal, "g*", "search.word_forward_partial")
|
|
234
|
+
@keymaps.bind(:normal, "g#", "search.word_backward_partial")
|
|
235
|
+
@keymaps.bind(:normal, "\e", "ui.clear_message")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def handle_key(key)
|
|
239
|
+
@skip_record_for_current_key = false
|
|
240
|
+
append_dot_change_capture_key(key)
|
|
241
|
+
if key == :ctrl_c
|
|
242
|
+
handle_ctrl_c
|
|
243
|
+
record_macro_key_if_needed(key)
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
case @editor.mode
|
|
248
|
+
when :insert
|
|
249
|
+
handle_insert_key(key)
|
|
250
|
+
when :command_line
|
|
251
|
+
handle_command_line_key(key)
|
|
252
|
+
when :visual_char, :visual_line, :visual_block
|
|
253
|
+
handle_visual_key(key)
|
|
254
|
+
else
|
|
255
|
+
handle_normal_key(key)
|
|
256
|
+
end
|
|
257
|
+
load_current_ftplugin!
|
|
258
|
+
record_macro_key_if_needed(key)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def handle_normal_key(key)
|
|
262
|
+
if arrow_key?(key)
|
|
263
|
+
invoke_arrow(key)
|
|
264
|
+
return
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if paging_key?(key)
|
|
268
|
+
invoke_page_key(key)
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
if digit_key?(key) && count_digit_allowed?(key)
|
|
273
|
+
@editor.pending_count = (@editor.pending_count.to_s + key).to_i
|
|
274
|
+
@editor.echo(@editor.pending_count.to_s)
|
|
275
|
+
@pending_keys = []
|
|
276
|
+
return
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
token = normalize_key_token(key)
|
|
280
|
+
return if token.nil?
|
|
281
|
+
|
|
282
|
+
if @operator_pending
|
|
283
|
+
handle_operator_pending_key(token)
|
|
284
|
+
return
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
if @register_pending
|
|
288
|
+
finish_register_pending(token)
|
|
289
|
+
return
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
if @mark_pending
|
|
293
|
+
finish_mark_pending(token)
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if @jump_pending
|
|
298
|
+
finish_jump_pending(token)
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
if @macro_record_pending
|
|
303
|
+
finish_macro_record_pending(token)
|
|
304
|
+
return
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if @macro_play_pending
|
|
308
|
+
finish_macro_play_pending(token)
|
|
309
|
+
return
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
if @replace_pending
|
|
313
|
+
handle_replace_pending_key(token)
|
|
314
|
+
return
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
if @find_pending
|
|
318
|
+
finish_find_pending(token)
|
|
319
|
+
return
|
|
320
|
+
end
|
|
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
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
@pending_keys ||= []
|
|
397
|
+
@pending_keys << token
|
|
398
|
+
|
|
399
|
+
match = @keymaps.resolve_with_context(:normal, @pending_keys, editor: @editor)
|
|
400
|
+
case match.status
|
|
401
|
+
when :pending, :ambiguous
|
|
402
|
+
return
|
|
403
|
+
when :match
|
|
404
|
+
matched_keys = @pending_keys.dup
|
|
405
|
+
repeat_count = @editor.pending_count || 1
|
|
406
|
+
invocation = dup_invocation(match.invocation)
|
|
407
|
+
invocation.count = repeat_count
|
|
408
|
+
@dispatcher.dispatch(@editor, invocation)
|
|
409
|
+
maybe_record_simple_dot_change(invocation, matched_keys, repeat_count)
|
|
410
|
+
else
|
|
411
|
+
@editor.echo_error("Unknown key: #{@pending_keys.join}")
|
|
412
|
+
end
|
|
413
|
+
@editor.pending_count = nil
|
|
414
|
+
@pending_keys = []
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def handle_insert_key(key)
|
|
418
|
+
case key
|
|
419
|
+
when :escape
|
|
420
|
+
finish_insert_change_group
|
|
421
|
+
finish_dot_change_capture
|
|
422
|
+
clear_insert_completion
|
|
423
|
+
@editor.enter_normal_mode
|
|
424
|
+
@editor.echo("")
|
|
425
|
+
when :backspace
|
|
426
|
+
clear_insert_completion
|
|
427
|
+
y, x = @editor.current_buffer.backspace(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
|
|
428
|
+
@editor.current_window.cursor_y = y
|
|
429
|
+
@editor.current_window.cursor_x = x
|
|
430
|
+
when :ctrl_n
|
|
431
|
+
insert_complete(+1)
|
|
432
|
+
when :ctrl_p
|
|
433
|
+
insert_complete(-1)
|
|
434
|
+
when :ctrl_i
|
|
435
|
+
clear_insert_completion
|
|
436
|
+
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, "\t")
|
|
437
|
+
@editor.current_window.cursor_x += 1
|
|
438
|
+
when :enter
|
|
439
|
+
clear_insert_completion
|
|
440
|
+
y, x = @editor.current_buffer.insert_newline(@editor.current_window.cursor_y, @editor.current_window.cursor_x)
|
|
441
|
+
@editor.current_window.cursor_y = y
|
|
442
|
+
@editor.current_window.cursor_x = x
|
|
443
|
+
when :left
|
|
444
|
+
clear_insert_completion
|
|
445
|
+
@editor.current_window.move_left(@editor.current_buffer, 1)
|
|
446
|
+
when :right
|
|
447
|
+
clear_insert_completion
|
|
448
|
+
@editor.current_window.move_right(@editor.current_buffer, 1)
|
|
449
|
+
when :up
|
|
450
|
+
clear_insert_completion
|
|
451
|
+
@editor.current_window.move_up(@editor.current_buffer, 1)
|
|
452
|
+
when :down
|
|
453
|
+
clear_insert_completion
|
|
454
|
+
@editor.current_window.move_down(@editor.current_buffer, 1)
|
|
455
|
+
when :pageup, :pagedown
|
|
456
|
+
clear_insert_completion
|
|
457
|
+
invoke_page_key(key)
|
|
458
|
+
else
|
|
459
|
+
return unless key.is_a?(String)
|
|
460
|
+
|
|
461
|
+
clear_insert_completion
|
|
462
|
+
@editor.current_buffer.insert_char(@editor.current_window.cursor_y, @editor.current_window.cursor_x, key)
|
|
463
|
+
@editor.current_window.cursor_x += 1
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def handle_visual_key(key)
|
|
468
|
+
if arrow_key?(key)
|
|
469
|
+
invoke_arrow(key)
|
|
470
|
+
return
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
if paging_key?(key)
|
|
474
|
+
invoke_page_key(key)
|
|
475
|
+
return
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
token = normalize_key_token(key)
|
|
479
|
+
return if token.nil?
|
|
480
|
+
|
|
481
|
+
case token
|
|
482
|
+
when "\e"
|
|
483
|
+
@register_pending = false
|
|
484
|
+
@visual_pending = nil
|
|
485
|
+
@editor.enter_normal_mode
|
|
486
|
+
when "v"
|
|
487
|
+
if @editor.mode == :visual_char
|
|
488
|
+
@editor.enter_normal_mode
|
|
489
|
+
else
|
|
490
|
+
@editor.enter_visual(:visual_char)
|
|
491
|
+
end
|
|
492
|
+
when "V"
|
|
493
|
+
if @editor.mode == :visual_line
|
|
494
|
+
@editor.enter_normal_mode
|
|
495
|
+
else
|
|
496
|
+
@editor.enter_visual(:visual_line)
|
|
497
|
+
end
|
|
498
|
+
when "<C-v>"
|
|
499
|
+
if @editor.mode == :visual_block
|
|
500
|
+
@editor.enter_normal_mode
|
|
501
|
+
else
|
|
502
|
+
@editor.enter_visual(:visual_block)
|
|
503
|
+
end
|
|
504
|
+
when "y"
|
|
505
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_yank"))
|
|
506
|
+
when "d"
|
|
507
|
+
@visual_pending = nil
|
|
508
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id: "buffer.visual_delete"))
|
|
509
|
+
when "\""
|
|
510
|
+
start_register_pending
|
|
511
|
+
when "i", "a"
|
|
512
|
+
@visual_pending = token
|
|
513
|
+
else
|
|
514
|
+
if @register_pending
|
|
515
|
+
finish_register_pending(token)
|
|
516
|
+
return
|
|
517
|
+
end
|
|
518
|
+
if @visual_pending
|
|
519
|
+
if @editor.mode == :visual_block
|
|
520
|
+
@visual_pending = nil
|
|
521
|
+
@editor.echo_error("text object in Visual block not supported yet")
|
|
522
|
+
return
|
|
523
|
+
end
|
|
524
|
+
motion = "#{@visual_pending}#{token}"
|
|
525
|
+
@visual_pending = nil
|
|
526
|
+
inv = CommandInvocation.new(id: "buffer.visual_select_text_object", kwargs: { motion: motion })
|
|
527
|
+
@dispatcher.dispatch(@editor, inv)
|
|
528
|
+
else
|
|
529
|
+
handle_visual_motion_token(token)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
@editor.pending_count = nil
|
|
533
|
+
@pending_keys = []
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def handle_visual_motion_token(token)
|
|
537
|
+
id = {
|
|
538
|
+
"h" => "cursor.left",
|
|
539
|
+
"j" => "cursor.down",
|
|
540
|
+
"k" => "cursor.up",
|
|
541
|
+
"l" => "cursor.right",
|
|
542
|
+
"0" => "cursor.line_start",
|
|
543
|
+
"$" => "cursor.line_end",
|
|
544
|
+
"^" => "cursor.first_nonblank",
|
|
545
|
+
"w" => "cursor.word_forward",
|
|
546
|
+
"b" => "cursor.word_backward",
|
|
547
|
+
"e" => "cursor.word_end",
|
|
548
|
+
"G" => "cursor.buffer_end"
|
|
549
|
+
}[token]
|
|
550
|
+
|
|
551
|
+
if token == "g"
|
|
552
|
+
@pending_keys ||= []
|
|
553
|
+
@pending_keys << token
|
|
554
|
+
return
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
if @pending_keys == ["g"] && token == "g"
|
|
558
|
+
id = "cursor.buffer_start"
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
if id
|
|
562
|
+
count = @editor.pending_count || 1
|
|
563
|
+
@dispatcher.dispatch(@editor, CommandInvocation.new(id:, count: count))
|
|
564
|
+
else
|
|
565
|
+
@editor.echo_error("Unknown visual key: #{token}")
|
|
566
|
+
end
|
|
567
|
+
ensure
|
|
568
|
+
@pending_keys = [] unless token == "g"
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def handle_command_line_key(key)
|
|
572
|
+
cmd = @editor.command_line
|
|
573
|
+
case key
|
|
574
|
+
when :escape
|
|
575
|
+
@editor.cancel_command_line
|
|
576
|
+
when :enter
|
|
577
|
+
line = cmd.text.dup
|
|
578
|
+
push_command_line_history(cmd.prefix, line)
|
|
579
|
+
handle_command_line_submit(cmd.prefix, line)
|
|
580
|
+
when :backspace
|
|
581
|
+
cmd.backspace
|
|
582
|
+
when :up
|
|
583
|
+
command_line_history_move(-1)
|
|
584
|
+
when :down
|
|
585
|
+
command_line_history_move(1)
|
|
586
|
+
when :left
|
|
587
|
+
cmd.move_left
|
|
588
|
+
when :right
|
|
589
|
+
cmd.move_right
|
|
590
|
+
else
|
|
591
|
+
if key == :ctrl_i
|
|
592
|
+
command_line_complete
|
|
593
|
+
elsif key.is_a?(String)
|
|
594
|
+
@cmdline_history_index = nil
|
|
595
|
+
cmd.insert(key)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def arrow_key?(key)
|
|
601
|
+
%i[left right up down].include?(key)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def paging_key?(key)
|
|
605
|
+
%i[pageup pagedown].include?(key)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def invoke_arrow(key)
|
|
609
|
+
id = {
|
|
610
|
+
left: "cursor.left",
|
|
611
|
+
right: "cursor.right",
|
|
612
|
+
up: "cursor.up",
|
|
613
|
+
down: "cursor.down"
|
|
614
|
+
}.fetch(key)
|
|
615
|
+
inv = CommandInvocation.new(id:, count: @editor.pending_count || 1)
|
|
616
|
+
@dispatcher.dispatch(@editor, inv)
|
|
617
|
+
@editor.pending_count = nil
|
|
618
|
+
@pending_keys = []
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def invoke_page_key(key)
|
|
622
|
+
id = (key == :pageup ? "cursor.page_up" : "cursor.page_down")
|
|
623
|
+
inv = CommandInvocation.new(
|
|
624
|
+
id: id,
|
|
625
|
+
count: @editor.pending_count || 1,
|
|
626
|
+
kwargs: { page_lines: current_page_step_lines }
|
|
627
|
+
)
|
|
628
|
+
@dispatcher.dispatch(@editor, inv)
|
|
629
|
+
@editor.pending_count = nil
|
|
630
|
+
@pending_keys = []
|
|
631
|
+
end
|
|
632
|
+
|
|
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
|
+
def digit_key?(key)
|
|
641
|
+
key.is_a?(String) && key.match?(/\A\d\z/)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def count_digit_allowed?(key)
|
|
645
|
+
return false unless @editor.mode == :normal
|
|
646
|
+
return true unless @editor.pending_count.nil?
|
|
647
|
+
|
|
648
|
+
key != "0"
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def normalize_key_token(key)
|
|
652
|
+
case key
|
|
653
|
+
when String then key
|
|
654
|
+
when :escape then "\e"
|
|
655
|
+
when :ctrl_r then "<C-r>"
|
|
656
|
+
when :ctrl_v then "<C-v>"
|
|
657
|
+
when :ctrl_i then "<C-i>"
|
|
658
|
+
when :ctrl_o then "<C-o>"
|
|
659
|
+
when :ctrl_w then "<C-w>"
|
|
660
|
+
when :home then "<Home>"
|
|
661
|
+
when :end then "<End>"
|
|
662
|
+
when :pageup then "<PageUp>"
|
|
663
|
+
when :pagedown then "<PageDown>"
|
|
664
|
+
else nil
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def dup_invocation(inv)
|
|
669
|
+
CommandInvocation.new(
|
|
670
|
+
id: inv.id,
|
|
671
|
+
argv: inv.argv.dup,
|
|
672
|
+
kwargs: inv.kwargs.dup,
|
|
673
|
+
count: inv.count,
|
|
674
|
+
bang: inv.bang,
|
|
675
|
+
raw_keys: inv.raw_keys&.dup
|
|
676
|
+
)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def handle_ctrl_c
|
|
680
|
+
case @editor.mode
|
|
681
|
+
when :insert
|
|
682
|
+
finish_insert_change_group
|
|
683
|
+
finish_dot_change_capture
|
|
684
|
+
clear_insert_completion
|
|
685
|
+
@editor.enter_normal_mode
|
|
686
|
+
@editor.echo("")
|
|
687
|
+
when :command_line
|
|
688
|
+
@editor.cancel_command_line
|
|
689
|
+
when :visual_char, :visual_line, :visual_block
|
|
690
|
+
@visual_pending = nil
|
|
691
|
+
@register_pending = false
|
|
692
|
+
@mark_pending = false
|
|
693
|
+
@jump_pending = nil
|
|
694
|
+
@editor.enter_normal_mode
|
|
695
|
+
else
|
|
696
|
+
@editor.pending_count = nil
|
|
697
|
+
@pending_keys = []
|
|
698
|
+
@operator_pending = nil
|
|
699
|
+
@replace_pending = nil
|
|
700
|
+
@register_pending = false
|
|
701
|
+
@mark_pending = false
|
|
702
|
+
@jump_pending = nil
|
|
703
|
+
@macro_record_pending = false
|
|
704
|
+
@macro_play_pending = false
|
|
705
|
+
@editor.clear_message
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def finish_insert_change_group
|
|
710
|
+
@editor.current_buffer.end_change_group
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def handle_command_line_submit(prefix, line)
|
|
714
|
+
case prefix
|
|
715
|
+
when ":"
|
|
716
|
+
verbose_log(2, "ex: #{line}")
|
|
717
|
+
@dispatcher.dispatch_ex(@editor, line)
|
|
718
|
+
when "/"
|
|
719
|
+
verbose_log(2, "search(/): #{line}")
|
|
720
|
+
submit_search(line, direction: :forward)
|
|
721
|
+
when "?"
|
|
722
|
+
verbose_log(2, "search(?): #{line}")
|
|
723
|
+
submit_search(line, direction: :backward)
|
|
724
|
+
else
|
|
725
|
+
@editor.echo_error("Unknown command-line prefix: #{prefix}")
|
|
726
|
+
@editor.enter_normal_mode
|
|
727
|
+
end
|
|
728
|
+
@cmdline_history_index = nil
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def start_operator_pending(name)
|
|
732
|
+
@operator_pending = { name:, count: (@editor.pending_count || 1) }
|
|
733
|
+
@editor.pending_count = nil
|
|
734
|
+
@pending_keys = []
|
|
735
|
+
@editor.echo(name == :delete ? "d" : name.to_s)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def start_register_pending
|
|
739
|
+
@register_pending = true
|
|
740
|
+
@editor.echo('"')
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def finish_register_pending(token)
|
|
744
|
+
@register_pending = false
|
|
745
|
+
if token.is_a?(String) && token.length == 1
|
|
746
|
+
@editor.set_active_register(token)
|
|
747
|
+
@editor.echo(%("#{token}))
|
|
748
|
+
else
|
|
749
|
+
@editor.echo_error("Invalid register")
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def start_mark_pending
|
|
754
|
+
@mark_pending = true
|
|
755
|
+
@editor.echo("m")
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def finish_mark_pending(token)
|
|
759
|
+
@mark_pending = false
|
|
760
|
+
if token == "\e"
|
|
761
|
+
@editor.clear_message
|
|
762
|
+
return
|
|
763
|
+
end
|
|
764
|
+
unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
|
|
765
|
+
@editor.echo_error("Invalid mark")
|
|
766
|
+
return
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
inv = CommandInvocation.new(id: "mark.set", kwargs: { mark: token })
|
|
770
|
+
@dispatcher.dispatch(@editor, inv)
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def start_jump_pending(linewise:, repeat_token:)
|
|
774
|
+
@jump_pending = { linewise: linewise, repeat_token: repeat_token }
|
|
775
|
+
@editor.echo(repeat_token)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def finish_jump_pending(token)
|
|
779
|
+
pending = @jump_pending
|
|
780
|
+
@jump_pending = nil
|
|
781
|
+
return unless pending
|
|
782
|
+
if token == "\e"
|
|
783
|
+
@editor.clear_message
|
|
784
|
+
return
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
if token == pending[:repeat_token]
|
|
788
|
+
inv = CommandInvocation.new(id: "jump.older", kwargs: { linewise: pending[:linewise] })
|
|
789
|
+
@dispatcher.dispatch(@editor, inv)
|
|
790
|
+
return
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
unless token.is_a?(String) && token.match?(/\A[A-Za-z]\z/)
|
|
794
|
+
@editor.echo_error("Invalid mark")
|
|
795
|
+
return
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
inv = CommandInvocation.new(id: "mark.jump", kwargs: { mark: token, linewise: pending[:linewise] })
|
|
799
|
+
@dispatcher.dispatch(@editor, inv)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def start_macro_record_pending
|
|
803
|
+
@macro_record_pending = true
|
|
804
|
+
@editor.echo("q")
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def finish_macro_record_pending(token)
|
|
808
|
+
@macro_record_pending = false
|
|
809
|
+
if token == "\e"
|
|
810
|
+
@editor.clear_message
|
|
811
|
+
return
|
|
812
|
+
end
|
|
813
|
+
unless token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
|
|
814
|
+
@editor.echo_error("Invalid macro register")
|
|
815
|
+
return
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
unless @editor.start_macro_recording(token)
|
|
819
|
+
@editor.echo("Failed to start recording")
|
|
820
|
+
return
|
|
821
|
+
end
|
|
822
|
+
@skip_record_for_current_key = true
|
|
823
|
+
@editor.echo("recording @#{token}")
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def stop_macro_recording
|
|
827
|
+
reg = @editor.macro_recording_name
|
|
828
|
+
@editor.stop_macro_recording
|
|
829
|
+
@editor.echo("recording @#{reg} stopped")
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def start_macro_play_pending
|
|
833
|
+
@macro_play_pending = true
|
|
834
|
+
@editor.echo("@")
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def finish_macro_play_pending(token)
|
|
838
|
+
@macro_play_pending = false
|
|
839
|
+
if token == "\e"
|
|
840
|
+
@editor.clear_message
|
|
841
|
+
return
|
|
842
|
+
end
|
|
843
|
+
name =
|
|
844
|
+
if token == "@"
|
|
845
|
+
@last_macro_name
|
|
846
|
+
elsif token.is_a?(String) && token.match?(/\A[A-Za-z0-9]\z/)
|
|
847
|
+
token
|
|
848
|
+
end
|
|
849
|
+
unless name
|
|
850
|
+
@editor.echo_error("Invalid macro register")
|
|
851
|
+
return
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
count = @editor.pending_count || 1
|
|
855
|
+
@editor.pending_count = nil
|
|
856
|
+
play_macro(name, count:)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
def play_macro(name, count:)
|
|
860
|
+
reg = name.to_s.downcase
|
|
861
|
+
keys = @editor.macro_keys(reg)
|
|
862
|
+
if keys.nil? || keys.empty?
|
|
863
|
+
@editor.echo("Macro empty: #{reg}")
|
|
864
|
+
return
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
@macro_play_stack ||= []
|
|
868
|
+
if @macro_play_stack.include?(reg) || @macro_play_stack.length >= 20
|
|
869
|
+
@editor.echo("Macro recursion blocked: #{reg}")
|
|
870
|
+
return
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
@last_macro_name = reg
|
|
874
|
+
@macro_play_stack << reg
|
|
875
|
+
@suspend_macro_recording_depth = (@suspend_macro_recording_depth || 0) + 1
|
|
876
|
+
count.times do
|
|
877
|
+
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
878
|
+
end
|
|
879
|
+
@editor.echo("@#{reg}")
|
|
880
|
+
ensure
|
|
881
|
+
@suspend_macro_recording_depth = [(@suspend_macro_recording_depth || 1) - 1, 0].max
|
|
882
|
+
@macro_play_stack.pop if @macro_play_stack && !@macro_play_stack.empty?
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def record_macro_key_if_needed(key)
|
|
886
|
+
return if @skip_record_for_current_key
|
|
887
|
+
return unless @editor.macro_recording?
|
|
888
|
+
return if (@suspend_macro_recording_depth || 0).positive?
|
|
889
|
+
return if (@dot_replay_depth || 0).positive?
|
|
890
|
+
|
|
891
|
+
@editor.record_macro_key(key)
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def dup_macro_runtime_key(key)
|
|
895
|
+
case key
|
|
896
|
+
when String
|
|
897
|
+
key.dup
|
|
898
|
+
when Array
|
|
899
|
+
key.map { |v| v.is_a?(String) ? v.dup : v }
|
|
900
|
+
else
|
|
901
|
+
key
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def handle_operator_pending_key(token)
|
|
906
|
+
op = @operator_pending
|
|
907
|
+
if %w[i a].include?(token) && !op[:motion_prefix]
|
|
908
|
+
@operator_pending[:motion_prefix] = token
|
|
909
|
+
@editor.echo("#{op[:name].to_s[0]}#{token}")
|
|
910
|
+
return
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
motion = [op[:motion_prefix], token].compact.join
|
|
914
|
+
@operator_pending = nil
|
|
915
|
+
|
|
916
|
+
if token == "\e"
|
|
917
|
+
@editor.clear_message
|
|
918
|
+
return
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
if op[:name] == :delete && motion == "d"
|
|
922
|
+
inv = CommandInvocation.new(id: "buffer.delete_line", count: op[:count])
|
|
923
|
+
@dispatcher.dispatch(@editor, inv)
|
|
924
|
+
record_last_change_keys(count_prefixed_keys(op[:count], ["d", "d"]))
|
|
925
|
+
return
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
if op[:name] == :delete
|
|
929
|
+
inv = CommandInvocation.new(id: "buffer.delete_motion", count: op[:count], kwargs: { motion: motion })
|
|
930
|
+
@dispatcher.dispatch(@editor, inv)
|
|
931
|
+
record_last_change_keys(count_prefixed_keys(op[:count], ["d", *motion.each_char.to_a]))
|
|
932
|
+
return
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
if op[:name] == :yank && motion == "y"
|
|
936
|
+
inv = CommandInvocation.new(id: "buffer.yank_line", count: op[:count])
|
|
937
|
+
@dispatcher.dispatch(@editor, inv)
|
|
938
|
+
return
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
if op[:name] == :yank
|
|
942
|
+
inv = CommandInvocation.new(id: "buffer.yank_motion", count: op[:count], kwargs: { motion: motion })
|
|
943
|
+
@dispatcher.dispatch(@editor, inv)
|
|
944
|
+
return
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
if op[:name] == :change && motion == "c"
|
|
948
|
+
inv = CommandInvocation.new(id: "buffer.change_line", count: op[:count])
|
|
949
|
+
@dispatcher.dispatch(@editor, inv)
|
|
950
|
+
begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", "c"])) if @editor.mode == :insert
|
|
951
|
+
return
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
if op[:name] == :change
|
|
955
|
+
inv = CommandInvocation.new(id: "buffer.change_motion", count: op[:count], kwargs: { motion: motion })
|
|
956
|
+
@dispatcher.dispatch(@editor, inv)
|
|
957
|
+
begin_dot_change_capture(count_prefixed_keys(op[:count], ["c", *motion.each_char.to_a])) if @editor.mode == :insert
|
|
958
|
+
return
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
@editor.echo_error("Unknown operator")
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def start_replace_pending
|
|
965
|
+
@replace_pending = { count: (@editor.pending_count || 1) }
|
|
966
|
+
@editor.pending_count = nil
|
|
967
|
+
@pending_keys = []
|
|
968
|
+
@editor.echo("r")
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
def handle_replace_pending_key(token)
|
|
972
|
+
pending = @replace_pending
|
|
973
|
+
@replace_pending = nil
|
|
974
|
+
if token == "\e"
|
|
975
|
+
@editor.clear_message
|
|
976
|
+
return
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
if token.is_a?(String) && !token.empty?
|
|
980
|
+
inv = CommandInvocation.new(id: "buffer.replace_char", argv: [token], count: pending[:count])
|
|
981
|
+
@dispatcher.dispatch(@editor, inv)
|
|
982
|
+
record_last_change_keys(count_prefixed_keys(pending[:count], ["r", token]))
|
|
983
|
+
else
|
|
984
|
+
@editor.echo("r expects one character")
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
def repeat_last_change
|
|
989
|
+
keys = @last_change_keys
|
|
990
|
+
if keys.nil? || keys.empty?
|
|
991
|
+
@editor.echo("No previous change")
|
|
992
|
+
return
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
@dot_replay_depth = (@dot_replay_depth || 0) + 1
|
|
996
|
+
keys.each { |k| handle_key(dup_macro_runtime_key(k)) }
|
|
997
|
+
@editor.echo(".")
|
|
998
|
+
ensure
|
|
999
|
+
@dot_replay_depth = [(@dot_replay_depth || 1) - 1, 0].max
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
def maybe_record_simple_dot_change(invocation, matched_keys, count)
|
|
1003
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1004
|
+
|
|
1005
|
+
case invocation.id
|
|
1006
|
+
when "buffer.delete_char", "buffer.paste_after", "buffer.paste_before"
|
|
1007
|
+
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"
|
|
1009
|
+
begin_dot_change_capture(count_prefixed_keys(count, matched_keys)) if @editor.mode == :insert
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def begin_dot_change_capture(prefix_keys)
|
|
1014
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1015
|
+
|
|
1016
|
+
@dot_change_capture_keys = Array(prefix_keys).map { |k| dup_macro_runtime_key(k) }
|
|
1017
|
+
@dot_change_capture_active = true
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
def append_dot_change_capture_key(key)
|
|
1021
|
+
return unless @dot_change_capture_active
|
|
1022
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1023
|
+
|
|
1024
|
+
@dot_change_capture_keys ||= []
|
|
1025
|
+
@dot_change_capture_keys << dup_macro_runtime_key(key)
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def finish_dot_change_capture
|
|
1029
|
+
return unless @dot_change_capture_active
|
|
1030
|
+
|
|
1031
|
+
keys = Array(@dot_change_capture_keys)
|
|
1032
|
+
@dot_change_capture_active = false
|
|
1033
|
+
@dot_change_capture_keys = nil
|
|
1034
|
+
record_last_change_keys(keys)
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def record_last_change_keys(keys)
|
|
1038
|
+
return if (@dot_replay_depth || 0).positive?
|
|
1039
|
+
|
|
1040
|
+
@last_change_keys = Array(keys).map { |k| dup_macro_runtime_key(k) }
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
def count_prefixed_keys(count, keys)
|
|
1044
|
+
c = count.to_i
|
|
1045
|
+
prefix = c > 1 ? c.to_s.each_char.to_a : []
|
|
1046
|
+
prefix + Array(keys)
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def start_find_pending(token)
|
|
1050
|
+
@find_pending = {
|
|
1051
|
+
direction: (token == "f" || token == "t") ? :forward : :backward,
|
|
1052
|
+
till: (token == "t" || token == "T"),
|
|
1053
|
+
count: (@editor.pending_count || 1)
|
|
1054
|
+
}
|
|
1055
|
+
@editor.pending_count = nil
|
|
1056
|
+
@pending_keys = []
|
|
1057
|
+
@editor.echo(token)
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def finish_find_pending(token)
|
|
1061
|
+
pending = @find_pending
|
|
1062
|
+
@find_pending = nil
|
|
1063
|
+
if token == "\e"
|
|
1064
|
+
@editor.clear_message
|
|
1065
|
+
return
|
|
1066
|
+
end
|
|
1067
|
+
unless token.is_a?(String) && !token.empty?
|
|
1068
|
+
@editor.echo("find expects one character")
|
|
1069
|
+
return
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
moved = perform_find_on_line(
|
|
1073
|
+
char: token,
|
|
1074
|
+
direction: pending[:direction],
|
|
1075
|
+
till: pending[:till],
|
|
1076
|
+
count: pending[:count]
|
|
1077
|
+
)
|
|
1078
|
+
if moved
|
|
1079
|
+
@editor.set_last_find(char: token, direction: pending[:direction], till: pending[:till])
|
|
1080
|
+
else
|
|
1081
|
+
@editor.echo("Char not found: #{token}")
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
def repeat_last_find(reverse:)
|
|
1086
|
+
last = @editor.last_find
|
|
1087
|
+
unless last
|
|
1088
|
+
@editor.echo("No previous f/t")
|
|
1089
|
+
return
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
direction =
|
|
1093
|
+
if reverse
|
|
1094
|
+
last[:direction] == :forward ? :backward : :forward
|
|
1095
|
+
else
|
|
1096
|
+
last[:direction]
|
|
1097
|
+
end
|
|
1098
|
+
count = @editor.pending_count || 1
|
|
1099
|
+
@editor.pending_count = nil
|
|
1100
|
+
@pending_keys = []
|
|
1101
|
+
moved = perform_find_on_line(char: last[:char], direction:, till: last[:till], count:)
|
|
1102
|
+
@editor.echo("Char not found: #{last[:char]}") unless moved
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
def perform_find_on_line(char:, direction:, till:, count:)
|
|
1106
|
+
win = @editor.current_window
|
|
1107
|
+
buf = @editor.current_buffer
|
|
1108
|
+
line = buf.line_at(win.cursor_y)
|
|
1109
|
+
pos = win.cursor_x
|
|
1110
|
+
target = nil
|
|
1111
|
+
|
|
1112
|
+
count.times do
|
|
1113
|
+
idx =
|
|
1114
|
+
if direction == :forward
|
|
1115
|
+
line.index(char, pos + 1)
|
|
1116
|
+
else
|
|
1117
|
+
rindex_from(line, char, pos - 1)
|
|
1118
|
+
end
|
|
1119
|
+
return false if idx.nil?
|
|
1120
|
+
|
|
1121
|
+
target = idx
|
|
1122
|
+
pos = idx
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
if till
|
|
1126
|
+
target =
|
|
1127
|
+
if direction == :forward
|
|
1128
|
+
RuVim::TextMetrics.previous_grapheme_char_index(line, target)
|
|
1129
|
+
else
|
|
1130
|
+
RuVim::TextMetrics.next_grapheme_char_index(line, target)
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
win.cursor_x = target
|
|
1135
|
+
win.clamp_to_buffer(buf)
|
|
1136
|
+
true
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
def rindex_from(line, char, pos)
|
|
1140
|
+
return nil if pos.negative?
|
|
1141
|
+
|
|
1142
|
+
line.rindex(char, pos)
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def submit_search(line, direction:)
|
|
1146
|
+
inv = CommandInvocation.new(id: "__search_submit__", argv: [line], kwargs: { pattern: line, direction: direction })
|
|
1147
|
+
ctx = Context.new(editor: @editor, invocation: inv)
|
|
1148
|
+
GlobalCommands.instance.submit_search(ctx, pattern: line, direction: direction)
|
|
1149
|
+
@editor.enter_normal_mode
|
|
1150
|
+
rescue StandardError => e
|
|
1151
|
+
@editor.echo_error("Error: #{e.message}")
|
|
1152
|
+
@editor.enter_normal_mode
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def push_command_line_history(prefix, line)
|
|
1156
|
+
text = line.to_s
|
|
1157
|
+
return if text.empty?
|
|
1158
|
+
|
|
1159
|
+
hist = @cmdline_history[prefix]
|
|
1160
|
+
hist.delete(text)
|
|
1161
|
+
hist << text
|
|
1162
|
+
hist.shift while hist.length > 100
|
|
1163
|
+
@cmdline_history_index = nil
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
def command_line_history_move(delta)
|
|
1167
|
+
cmd = @editor.command_line
|
|
1168
|
+
hist = @cmdline_history[cmd.prefix]
|
|
1169
|
+
return if hist.empty?
|
|
1170
|
+
|
|
1171
|
+
@cmdline_history_index =
|
|
1172
|
+
if @cmdline_history_index.nil?
|
|
1173
|
+
delta.negative? ? hist.length - 1 : hist.length
|
|
1174
|
+
else
|
|
1175
|
+
@cmdline_history_index + delta
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
@cmdline_history_index = [[@cmdline_history_index, 0].max, hist.length].min
|
|
1179
|
+
if @cmdline_history_index == hist.length
|
|
1180
|
+
cmd.replace_text("")
|
|
1181
|
+
else
|
|
1182
|
+
cmd.replace_text(hist[@cmdline_history_index])
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def command_line_complete
|
|
1187
|
+
cmd = @editor.command_line
|
|
1188
|
+
return unless cmd.prefix == ":"
|
|
1189
|
+
|
|
1190
|
+
ctx = ex_completion_context(cmd)
|
|
1191
|
+
return unless ctx
|
|
1192
|
+
|
|
1193
|
+
matches = ex_completion_candidates(ctx)
|
|
1194
|
+
case matches.length
|
|
1195
|
+
when 0
|
|
1196
|
+
@editor.echo("No completion")
|
|
1197
|
+
when 1
|
|
1198
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], matches.first)
|
|
1199
|
+
else
|
|
1200
|
+
prefix = common_prefix(matches)
|
|
1201
|
+
cmd.replace_span(ctx[:token_start], ctx[:token_end], prefix) if prefix.length > ctx[:prefix].length
|
|
1202
|
+
@editor.echo(matches.join(" "))
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
def common_prefix(strings)
|
|
1207
|
+
return "" if strings.empty?
|
|
1208
|
+
|
|
1209
|
+
prefix = strings.first.dup
|
|
1210
|
+
strings[1..]&.each do |s|
|
|
1211
|
+
while !prefix.empty? && !s.start_with?(prefix)
|
|
1212
|
+
prefix = prefix[0...-1]
|
|
1213
|
+
end
|
|
1214
|
+
end
|
|
1215
|
+
prefix
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
def clear_insert_completion
|
|
1219
|
+
@insert_completion = nil
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def insert_complete(direction)
|
|
1223
|
+
state = ensure_insert_completion_state
|
|
1224
|
+
return unless state
|
|
1225
|
+
|
|
1226
|
+
matches = state[:matches]
|
|
1227
|
+
if matches.empty?
|
|
1228
|
+
@editor.echo("No completion")
|
|
1229
|
+
return
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
idx = state[:index]
|
|
1233
|
+
idx = idx.nil? ? (direction.positive? ? 0 : matches.length - 1) : (idx + direction) % matches.length
|
|
1234
|
+
replacement = matches[idx]
|
|
1235
|
+
|
|
1236
|
+
end_col = state[:current_end_col]
|
|
1237
|
+
start_col = state[:start_col]
|
|
1238
|
+
@editor.current_buffer.delete_span(state[:row], start_col, state[:row], end_col)
|
|
1239
|
+
_y, new_x = @editor.current_buffer.insert_text(state[:row], start_col, replacement)
|
|
1240
|
+
@editor.current_window.cursor_y = state[:row]
|
|
1241
|
+
@editor.current_window.cursor_x = new_x
|
|
1242
|
+
state[:index] = idx
|
|
1243
|
+
state[:current_end_col] = start_col + replacement.length
|
|
1244
|
+
@editor.echo(matches.length == 1 ? replacement : "#{replacement} (#{idx + 1}/#{matches.length})")
|
|
1245
|
+
rescue StandardError => e
|
|
1246
|
+
@editor.echo_error("Completion error: #{e.message}")
|
|
1247
|
+
clear_insert_completion
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
def ensure_insert_completion_state
|
|
1251
|
+
row = @editor.current_window.cursor_y
|
|
1252
|
+
col = @editor.current_window.cursor_x
|
|
1253
|
+
line = @editor.current_buffer.line_at(row)
|
|
1254
|
+
prefix = line[0...col].to_s[/[[:alnum:]_]+\z/]
|
|
1255
|
+
return nil if prefix.nil? || prefix.empty?
|
|
1256
|
+
|
|
1257
|
+
start_col = col - prefix.length
|
|
1258
|
+
current_token = line[start_col...col].to_s
|
|
1259
|
+
state = @insert_completion
|
|
1260
|
+
|
|
1261
|
+
if state &&
|
|
1262
|
+
state[:row] == row &&
|
|
1263
|
+
state[:start_col] == start_col &&
|
|
1264
|
+
state[:prefix] == prefix &&
|
|
1265
|
+
col == state[:current_end_col]
|
|
1266
|
+
return state
|
|
1267
|
+
end
|
|
1268
|
+
|
|
1269
|
+
matches = collect_buffer_word_completions(prefix, current_word: current_token)
|
|
1270
|
+
@insert_completion = {
|
|
1271
|
+
row: row,
|
|
1272
|
+
start_col: start_col,
|
|
1273
|
+
prefix: prefix,
|
|
1274
|
+
matches: matches,
|
|
1275
|
+
index: nil,
|
|
1276
|
+
current_end_col: col
|
|
1277
|
+
}
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def collect_buffer_word_completions(prefix, current_word:)
|
|
1281
|
+
words = []
|
|
1282
|
+
seen = {}
|
|
1283
|
+
@editor.buffers.values.each do |buf|
|
|
1284
|
+
buf.lines.each do |line|
|
|
1285
|
+
line.scan(/[[:alnum:]_]+/) do |w|
|
|
1286
|
+
next unless w.start_with?(prefix)
|
|
1287
|
+
next if w == current_word
|
|
1288
|
+
next if seen[w]
|
|
1289
|
+
|
|
1290
|
+
seen[w] = true
|
|
1291
|
+
words << w
|
|
1292
|
+
end
|
|
1293
|
+
end
|
|
1294
|
+
end
|
|
1295
|
+
words.sort
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
def ex_completion_context(cmd)
|
|
1299
|
+
text = cmd.text
|
|
1300
|
+
cursor = cmd.cursor
|
|
1301
|
+
token_start = token_start_index(text, cursor)
|
|
1302
|
+
token_end = token_end_index(text, cursor)
|
|
1303
|
+
prefix = text[token_start...cursor].to_s
|
|
1304
|
+
before = text[0...token_start].to_s
|
|
1305
|
+
argv_before = before.split(/\s+/).reject(&:empty?)
|
|
1306
|
+
|
|
1307
|
+
if argv_before.empty?
|
|
1308
|
+
{
|
|
1309
|
+
kind: :command,
|
|
1310
|
+
token_start: token_start,
|
|
1311
|
+
token_end: token_end,
|
|
1312
|
+
prefix: prefix
|
|
1313
|
+
}
|
|
1314
|
+
else
|
|
1315
|
+
{
|
|
1316
|
+
kind: :arg,
|
|
1317
|
+
command: argv_before.first,
|
|
1318
|
+
arg_index: argv_before.length - 1,
|
|
1319
|
+
token_start: token_start,
|
|
1320
|
+
token_end: token_end,
|
|
1321
|
+
prefix: prefix
|
|
1322
|
+
}
|
|
1323
|
+
end
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def ex_completion_candidates(ctx)
|
|
1327
|
+
case ctx[:kind]
|
|
1328
|
+
when :command
|
|
1329
|
+
ExCommandRegistry.instance.all.flat_map { |spec| [spec.name, *spec.aliases] }.uniq.sort.select { |n| n.start_with?(ctx[:prefix]) }
|
|
1330
|
+
when :arg
|
|
1331
|
+
ex_arg_completion_candidates(ctx[:command], ctx[:arg_index], ctx[:prefix])
|
|
1332
|
+
else
|
|
1333
|
+
[]
|
|
1334
|
+
end
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
def ex_arg_completion_candidates(command_name, arg_index, prefix)
|
|
1338
|
+
cmd = command_name.to_s
|
|
1339
|
+
return [] unless arg_index.zero?
|
|
1340
|
+
|
|
1341
|
+
if %w[e edit w write tabnew].include?(cmd)
|
|
1342
|
+
return path_completion_candidates(prefix)
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
if %w[buffer b].include?(cmd)
|
|
1346
|
+
return buffer_completion_candidates(prefix)
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
if %w[set setlocal setglobal].include?(cmd)
|
|
1350
|
+
return option_completion_candidates(prefix)
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
[]
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def path_completion_candidates(prefix)
|
|
1357
|
+
input = prefix.to_s
|
|
1358
|
+
base_dir =
|
|
1359
|
+
if input.empty?
|
|
1360
|
+
"."
|
|
1361
|
+
elsif input.end_with?("/")
|
|
1362
|
+
input
|
|
1363
|
+
else
|
|
1364
|
+
File.dirname(input)
|
|
1365
|
+
end
|
|
1366
|
+
base_dir = "." if base_dir == "."
|
|
1367
|
+
partial = input.end_with?("/") ? "" : File.basename(input)
|
|
1368
|
+
pattern = input.empty? ? "*" : File.join(base_dir, "#{partial}*")
|
|
1369
|
+
Dir.glob(pattern, File::FNM_DOTMATCH).sort.filter_map do |p|
|
|
1370
|
+
next if [".", ".."].include?(File.basename(p))
|
|
1371
|
+
next unless p.start_with?(input) || input.empty?
|
|
1372
|
+
File.directory?(p) ? "#{p}/" : p
|
|
1373
|
+
end
|
|
1374
|
+
rescue StandardError
|
|
1375
|
+
[]
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
def buffer_completion_candidates(prefix)
|
|
1379
|
+
pfx = prefix.to_s
|
|
1380
|
+
items = @editor.buffers.values.flat_map do |b|
|
|
1381
|
+
path = b.path.to_s
|
|
1382
|
+
base = path.empty? ? nil : File.basename(path)
|
|
1383
|
+
[b.id.to_s, path, base].compact
|
|
1384
|
+
end.uniq.sort
|
|
1385
|
+
items.select { |s| s.start_with?(pfx) }
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
def option_completion_candidates(prefix)
|
|
1389
|
+
pfx = prefix.to_s
|
|
1390
|
+
names = RuVim::Editor::OPTION_DEFS.keys
|
|
1391
|
+
tokens = names + names.map { |n| "no#{n}" } + names.map { |n| "inv#{n}" } + names.map { |n| "#{n}?" }
|
|
1392
|
+
tokens.uniq.sort.select { |s| s.start_with?(pfx) }
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1395
|
+
def token_start_index(text, cursor)
|
|
1396
|
+
i = [[cursor, 0].max, text.length].min
|
|
1397
|
+
i -= 1 while i.positive? && !whitespace_char?(text[i - 1])
|
|
1398
|
+
i
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
def token_end_index(text, cursor)
|
|
1402
|
+
i = [[cursor, 0].max, text.length].min
|
|
1403
|
+
i += 1 while i < text.length && !whitespace_char?(text[i])
|
|
1404
|
+
i
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
def whitespace_char?(ch)
|
|
1408
|
+
ch && ch.match?(/\s/)
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def install_signal_handlers
|
|
1412
|
+
Signal.trap("WINCH") do
|
|
1413
|
+
@screen.invalidate_cache! if @screen.respond_to?(:invalidate_cache!)
|
|
1414
|
+
@needs_redraw = true
|
|
1415
|
+
notify_signal_wakeup
|
|
1416
|
+
end
|
|
1417
|
+
rescue ArgumentError
|
|
1418
|
+
nil
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
def init_config_loader!
|
|
1422
|
+
@config_loader = ConfigLoader.new(
|
|
1423
|
+
command_registry: CommandRegistry.instance,
|
|
1424
|
+
ex_registry: ExCommandRegistry.instance,
|
|
1425
|
+
keymaps: @keymaps,
|
|
1426
|
+
command_host: GlobalCommands.instance
|
|
1427
|
+
)
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
def load_user_config!
|
|
1431
|
+
return if @clean_mode || @restricted_mode
|
|
1432
|
+
return if @skip_user_config
|
|
1433
|
+
|
|
1434
|
+
if @config_path
|
|
1435
|
+
@config_loader.load_file(@config_path)
|
|
1436
|
+
else
|
|
1437
|
+
@config_loader.load_default!
|
|
1438
|
+
end
|
|
1439
|
+
rescue StandardError => e
|
|
1440
|
+
@editor.echo_error("config error: #{e.message}")
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
def load_current_ftplugin!
|
|
1444
|
+
return if @clean_mode || @restricted_mode
|
|
1445
|
+
return unless @config_loader
|
|
1446
|
+
|
|
1447
|
+
@config_loader.load_ftplugin!(@editor, @editor.current_buffer)
|
|
1448
|
+
rescue StandardError => e
|
|
1449
|
+
@editor.echo_error("ftplugin error: #{e.message}")
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
def run_startup_action!(action, log_prefix: "startup")
|
|
1453
|
+
case action[:type]
|
|
1454
|
+
when :ex
|
|
1455
|
+
verbose_log(2, "#{log_prefix} ex: #{action[:value]}")
|
|
1456
|
+
@dispatcher.dispatch_ex(@editor, action[:value].to_s)
|
|
1457
|
+
when :line
|
|
1458
|
+
verbose_log(2, "#{log_prefix} line: #{action[:value]}")
|
|
1459
|
+
move_cursor_to_line(action[:value].to_i)
|
|
1460
|
+
when :line_end
|
|
1461
|
+
verbose_log(2, "#{log_prefix} line_end")
|
|
1462
|
+
move_cursor_to_line(@editor.current_buffer.line_count)
|
|
1463
|
+
end
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
def verbose_log(level, message)
|
|
1467
|
+
return if @verbose_level.to_i < level.to_i
|
|
1468
|
+
return unless @verbose_io
|
|
1469
|
+
|
|
1470
|
+
@verbose_io.puts("[ruvim:v#{@verbose_level}] #{message}")
|
|
1471
|
+
@verbose_io.flush if @verbose_io.respond_to?(:flush)
|
|
1472
|
+
rescue StandardError
|
|
1473
|
+
nil
|
|
1474
|
+
end
|
|
1475
|
+
|
|
1476
|
+
def startup_mark(label)
|
|
1477
|
+
return unless @startup_time_path
|
|
1478
|
+
|
|
1479
|
+
@startup_timeline << [label.to_s, monotonic_now]
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
def write_startuptime_log!
|
|
1483
|
+
return unless @startup_time_path
|
|
1484
|
+
|
|
1485
|
+
prev = @startup_time_origin
|
|
1486
|
+
lines = @startup_timeline.map do |label, t|
|
|
1487
|
+
total_ms = ((t - @startup_time_origin) * 1000.0)
|
|
1488
|
+
delta_ms = ((t - prev) * 1000.0)
|
|
1489
|
+
prev = t
|
|
1490
|
+
format("%9.3f %9.3f %s", total_ms, delta_ms, label)
|
|
1491
|
+
end
|
|
1492
|
+
File.write(@startup_time_path, lines.join("\n") + "\n")
|
|
1493
|
+
rescue StandardError => e
|
|
1494
|
+
verbose_log(1, "startuptime write error: #{e.message}")
|
|
1495
|
+
end
|
|
1496
|
+
|
|
1497
|
+
def monotonic_now
|
|
1498
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1499
|
+
rescue StandardError
|
|
1500
|
+
Time.now.to_f
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
def apply_startup_readonly!
|
|
1504
|
+
buf = @editor.current_buffer
|
|
1505
|
+
return unless buf&.file_buffer?
|
|
1506
|
+
|
|
1507
|
+
buf.readonly = true
|
|
1508
|
+
@editor.echo("readonly: #{buf.display_name}")
|
|
1509
|
+
end
|
|
1510
|
+
|
|
1511
|
+
def apply_startup_nomodifiable!
|
|
1512
|
+
buf = @editor.current_buffer
|
|
1513
|
+
return unless buf&.file_buffer?
|
|
1514
|
+
|
|
1515
|
+
buf.modifiable = false
|
|
1516
|
+
buf.readonly = true
|
|
1517
|
+
@editor.echo("nomodifiable: #{buf.display_name}")
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
def apply_startup_compat_mode_messages!
|
|
1521
|
+
if @startup_diff_mode
|
|
1522
|
+
verbose_log(1, "startup: -d requested (diff mode placeholder)")
|
|
1523
|
+
@editor.echo("diff mode (-d) is not implemented yet")
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
if @startup_quickfix_errorfile
|
|
1527
|
+
verbose_log(1, "startup: -q #{@startup_quickfix_errorfile} requested (quickfix placeholder)")
|
|
1528
|
+
@editor.echo("quickfix startup (-q #{@startup_quickfix_errorfile}) is not implemented yet")
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
if @startup_session_file
|
|
1532
|
+
verbose_log(1, "startup: -S #{@startup_session_file} requested (session placeholder)")
|
|
1533
|
+
@editor.echo("session startup (-S #{@startup_session_file}) is not implemented yet")
|
|
1534
|
+
end
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
def open_startup_paths!(paths)
|
|
1538
|
+
list = Array(paths).compact
|
|
1539
|
+
return if list.empty?
|
|
1540
|
+
|
|
1541
|
+
first, *rest = list
|
|
1542
|
+
@editor.open_path(first)
|
|
1543
|
+
apply_startup_readonly! if @startup_readonly
|
|
1544
|
+
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
1545
|
+
|
|
1546
|
+
case @startup_open_layout
|
|
1547
|
+
when :horizontal
|
|
1548
|
+
rest.each { |p| open_path_in_split!(p, layout: :horizontal) }
|
|
1549
|
+
when :vertical
|
|
1550
|
+
rest.each { |p| open_path_in_split!(p, layout: :vertical) }
|
|
1551
|
+
when :tab
|
|
1552
|
+
rest.each { |p| open_path_in_tab!(p) }
|
|
1553
|
+
else
|
|
1554
|
+
# No multi-file layout mode yet; ignore extras if called directly.
|
|
1555
|
+
end
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
def open_path_in_split!(path, layout:)
|
|
1559
|
+
@editor.split_current_window(layout:)
|
|
1560
|
+
buf = @editor.add_buffer_from_file(path)
|
|
1561
|
+
@editor.switch_to_buffer(buf.id)
|
|
1562
|
+
apply_startup_readonly! if @startup_readonly
|
|
1563
|
+
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
def open_path_in_tab!(path)
|
|
1567
|
+
@editor.tabnew(path:)
|
|
1568
|
+
apply_startup_readonly! if @startup_readonly
|
|
1569
|
+
apply_startup_nomodifiable! if @startup_nomodifiable
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
def move_cursor_to_line(line_number)
|
|
1573
|
+
win = @editor.current_window
|
|
1574
|
+
buf = @editor.current_buffer
|
|
1575
|
+
return unless win && buf
|
|
1576
|
+
|
|
1577
|
+
target = [[line_number.to_i - 1, 0].max, buf.line_count - 1].min
|
|
1578
|
+
win.cursor_y = target
|
|
1579
|
+
win.clamp_to_buffer(buf)
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
def notify_signal_wakeup
|
|
1583
|
+
@signal_w.write_nonblock(".")
|
|
1584
|
+
rescue IO::WaitWritable, Errno::EPIPE
|
|
1585
|
+
nil
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
def register_internal_unless(registry, id, **spec)
|
|
1589
|
+
return if registry.registered?(id)
|
|
1590
|
+
|
|
1591
|
+
registry.register(id, **spec)
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
def register_ex_unless(registry, name, **spec)
|
|
1595
|
+
return if registry.registered?(name)
|
|
1596
|
+
|
|
1597
|
+
registry.register(name, **spec)
|
|
1598
|
+
end
|
|
1599
|
+
end
|
|
1600
|
+
end
|