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/editor.rb
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class Editor
|
|
3
|
+
OPTION_DEFS = {
|
|
4
|
+
"number" => { default_scope: :window, type: :bool, default: false },
|
|
5
|
+
"relativenumber" => { default_scope: :window, type: :bool, default: false },
|
|
6
|
+
"ignorecase" => { default_scope: :global, type: :bool, default: false },
|
|
7
|
+
"smartcase" => { default_scope: :global, type: :bool, default: false },
|
|
8
|
+
"hlsearch" => { default_scope: :global, type: :bool, default: true },
|
|
9
|
+
"tabstop" => { default_scope: :buffer, type: :int, default: 2 },
|
|
10
|
+
"filetype" => { default_scope: :buffer, type: :string, default: nil }
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :buffers, :windows
|
|
14
|
+
attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :window_layout, :restricted_mode
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@buffers = {}
|
|
18
|
+
@windows = {}
|
|
19
|
+
@window_order = []
|
|
20
|
+
@tabpages = []
|
|
21
|
+
@current_tabpage_index = nil
|
|
22
|
+
@next_tabpage_id = 1
|
|
23
|
+
@suspend_tab_autosave = false
|
|
24
|
+
@next_buffer_id = 1
|
|
25
|
+
@next_window_id = 1
|
|
26
|
+
@current_window_id = nil
|
|
27
|
+
@alternate_buffer_id = nil
|
|
28
|
+
@mode = :normal
|
|
29
|
+
@window_layout = :single
|
|
30
|
+
@message = ""
|
|
31
|
+
@message_kind = :info
|
|
32
|
+
@pending_count = nil
|
|
33
|
+
@restricted_mode = false
|
|
34
|
+
@running = true
|
|
35
|
+
@global_options = default_global_options
|
|
36
|
+
@command_line = CommandLine.new
|
|
37
|
+
@last_search = nil
|
|
38
|
+
@last_find = nil
|
|
39
|
+
@registers = {}
|
|
40
|
+
@active_register_name = nil
|
|
41
|
+
@local_marks = Hash.new { |h, k| h[k] = {} }
|
|
42
|
+
@global_marks = {}
|
|
43
|
+
@jumplist = []
|
|
44
|
+
@jump_index = nil
|
|
45
|
+
@macros = {}
|
|
46
|
+
@macro_recording = nil
|
|
47
|
+
@visual_state = nil
|
|
48
|
+
@quickfix_list = { items: [], index: nil }
|
|
49
|
+
@location_lists = Hash.new { |h, k| h[k] = { items: [], index: nil } }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def running?
|
|
53
|
+
@running
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def restricted_mode?
|
|
57
|
+
!!@restricted_mode
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def request_quit!
|
|
61
|
+
@running = false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def command_line
|
|
65
|
+
@command_line
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def global_options
|
|
69
|
+
@global_options
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def command_line_prefix
|
|
73
|
+
@command_line.prefix
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def last_search
|
|
77
|
+
@last_search
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def last_find
|
|
81
|
+
@last_find
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def set_last_search(pattern:, direction:)
|
|
85
|
+
@last_search = { pattern: pattern.to_s, direction: direction.to_sym }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def set_last_find(char:, direction:, till:)
|
|
89
|
+
@last_find = { char: char.to_s, direction: direction.to_sym, till: !!till }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def current_window
|
|
93
|
+
@windows.fetch(@current_window_id)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def current_buffer
|
|
97
|
+
@buffers.fetch(current_window.buffer_id)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def option_def(name)
|
|
101
|
+
OPTION_DEFS[name.to_s]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def option_default_scope(name)
|
|
105
|
+
option_def(name)&.fetch(:default_scope, :global) || :global
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def effective_option(name, window: current_window, buffer: current_buffer)
|
|
109
|
+
key = name.to_s
|
|
110
|
+
if window && window.options.key?(key)
|
|
111
|
+
window.options[key]
|
|
112
|
+
elsif buffer && buffer.options.key?(key)
|
|
113
|
+
buffer.options[key]
|
|
114
|
+
else
|
|
115
|
+
@global_options[key]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def get_option(name, scope: :effective, window: current_window, buffer: current_buffer)
|
|
120
|
+
key = name.to_s
|
|
121
|
+
case scope.to_sym
|
|
122
|
+
when :global
|
|
123
|
+
@global_options[key]
|
|
124
|
+
when :buffer
|
|
125
|
+
buffer&.options&.[](key)
|
|
126
|
+
when :window
|
|
127
|
+
window&.options&.[](key)
|
|
128
|
+
else
|
|
129
|
+
effective_option(key, window:, buffer:)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def set_option(name, value, scope: :auto, window: current_window, buffer: current_buffer)
|
|
134
|
+
key = name.to_s
|
|
135
|
+
value = coerce_option_value(key, value)
|
|
136
|
+
actual_scope = (scope.to_sym == :auto ? option_default_scope(key) : scope.to_sym)
|
|
137
|
+
case actual_scope
|
|
138
|
+
when :global
|
|
139
|
+
@global_options[key] = value
|
|
140
|
+
when :buffer
|
|
141
|
+
raise RuVim::CommandError, "No current buffer" unless buffer
|
|
142
|
+
buffer.options[key] = value
|
|
143
|
+
when :window
|
|
144
|
+
raise RuVim::CommandError, "No current window" unless window
|
|
145
|
+
window.options[key] = value
|
|
146
|
+
else
|
|
147
|
+
raise RuVim::CommandError, "Unknown option scope: #{actual_scope}"
|
|
148
|
+
end
|
|
149
|
+
value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def option_snapshot(window: current_window, buffer: current_buffer)
|
|
153
|
+
keys = (OPTION_DEFS.keys + @global_options.keys + (buffer&.options&.keys || []) + (window&.options&.keys || [])).uniq.sort
|
|
154
|
+
keys.map do |k|
|
|
155
|
+
{
|
|
156
|
+
name: k,
|
|
157
|
+
effective: get_option(k, scope: :effective, window:, buffer:),
|
|
158
|
+
global: get_option(k, scope: :global, window:, buffer:),
|
|
159
|
+
buffer: get_option(k, scope: :buffer, window:, buffer:),
|
|
160
|
+
window: get_option(k, scope: :window, window:, buffer:)
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def detect_filetype(path)
|
|
166
|
+
p = path.to_s
|
|
167
|
+
return nil if p.empty?
|
|
168
|
+
|
|
169
|
+
base = File.basename(p)
|
|
170
|
+
return "ruby" if %w[Gemfile Rakefile Guardfile].include?(base)
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
".rb" => "ruby",
|
|
174
|
+
".rake" => "ruby",
|
|
175
|
+
".ru" => "ruby",
|
|
176
|
+
".py" => "python",
|
|
177
|
+
".js" => "javascript",
|
|
178
|
+
".mjs" => "javascript",
|
|
179
|
+
".cjs" => "javascript",
|
|
180
|
+
".ts" => "typescript",
|
|
181
|
+
".tsx" => "typescriptreact",
|
|
182
|
+
".jsx" => "javascriptreact",
|
|
183
|
+
".json" => "json",
|
|
184
|
+
".yml" => "yaml",
|
|
185
|
+
".yaml" => "yaml",
|
|
186
|
+
".md" => "markdown",
|
|
187
|
+
".txt" => "text",
|
|
188
|
+
".html" => "html",
|
|
189
|
+
".css" => "css",
|
|
190
|
+
".sh" => "sh"
|
|
191
|
+
}[File.extname(base).downcase]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def registers
|
|
195
|
+
@registers
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def set_register(name = "\"", text:, type: :charwise)
|
|
199
|
+
key = name.to_s
|
|
200
|
+
return { text: text.to_s, type: type.to_sym } if key == "_"
|
|
201
|
+
|
|
202
|
+
payload = write_register_payload(key, text: text.to_s, type: type.to_sym)
|
|
203
|
+
write_clipboard_register(key, payload)
|
|
204
|
+
@registers["\""] = payload unless key == "\""
|
|
205
|
+
payload
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def store_operator_register(name = "\"", text:, type:, kind:)
|
|
209
|
+
key = (name || "\"").to_s
|
|
210
|
+
payload = { text: text.to_s, type: type.to_sym }
|
|
211
|
+
return payload if key == "_"
|
|
212
|
+
|
|
213
|
+
written = set_register(key, text: payload[:text], type: payload[:type])
|
|
214
|
+
op_payload = dup_register_payload(written)
|
|
215
|
+
|
|
216
|
+
case kind.to_sym
|
|
217
|
+
when :yank
|
|
218
|
+
@registers["0"] = dup_register_payload(op_payload)
|
|
219
|
+
when :delete, :change
|
|
220
|
+
rotate_delete_registers!(op_payload)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
written
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def get_register(name = "\"")
|
|
227
|
+
key = name.to_s.downcase
|
|
228
|
+
return read_clipboard_register(key) if clipboard_register?(key)
|
|
229
|
+
|
|
230
|
+
@registers[key]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def set_active_register(name)
|
|
234
|
+
@active_register_name = name.to_s
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def active_register_name
|
|
238
|
+
@active_register_name
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def consume_active_register(default = "\"")
|
|
242
|
+
name = @active_register_name || default
|
|
243
|
+
@active_register_name = nil
|
|
244
|
+
name
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def visual_state
|
|
248
|
+
@visual_state
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def macros
|
|
252
|
+
@macros
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def macro_recording?
|
|
256
|
+
!@macro_recording.nil?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def macro_recording_name
|
|
260
|
+
@macro_recording && @macro_recording[:name]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def start_macro_recording(name)
|
|
264
|
+
reg = name.to_s
|
|
265
|
+
return false unless reg.match?(/\A[A-Za-z0-9]\z/)
|
|
266
|
+
|
|
267
|
+
@macro_recording = { name: reg, keys: [] }
|
|
268
|
+
true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def stop_macro_recording
|
|
272
|
+
rec = @macro_recording
|
|
273
|
+
@macro_recording = nil
|
|
274
|
+
return nil unless rec
|
|
275
|
+
|
|
276
|
+
name = rec[:name]
|
|
277
|
+
keys = rec[:keys]
|
|
278
|
+
if name.match?(/\A[A-Z]\z/)
|
|
279
|
+
base = name.downcase
|
|
280
|
+
@macros[base] = [*(@macros[base] || []), *keys]
|
|
281
|
+
@macros[base]
|
|
282
|
+
else
|
|
283
|
+
@macros[name.downcase] = keys
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def record_macro_key(key)
|
|
288
|
+
return unless @macro_recording
|
|
289
|
+
|
|
290
|
+
@macro_recording[:keys] << dup_macro_key(key)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def macro_keys(name)
|
|
294
|
+
@macros[name.to_s.downcase]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def current_location
|
|
298
|
+
{ buffer_id: current_buffer.id, row: current_window.cursor_y, col: current_window.cursor_x }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def set_mark(name, window = current_window)
|
|
302
|
+
mark = name.to_s
|
|
303
|
+
return false unless mark.match?(/\A[A-Za-z]\z/)
|
|
304
|
+
|
|
305
|
+
loc = { buffer_id: window.buffer_id, row: window.cursor_y, col: window.cursor_x }
|
|
306
|
+
if mark.match?(/\A[a-z]\z/)
|
|
307
|
+
@local_marks[window.buffer_id][mark] = loc
|
|
308
|
+
else
|
|
309
|
+
@global_marks[mark] = loc
|
|
310
|
+
end
|
|
311
|
+
true
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def mark_location(name, buffer_id: current_buffer.id)
|
|
315
|
+
mark = name.to_s
|
|
316
|
+
return nil unless mark.match?(/\A[A-Za-z]\z/)
|
|
317
|
+
|
|
318
|
+
if mark.match?(/\A[a-z]\z/)
|
|
319
|
+
@local_marks[buffer_id][mark]
|
|
320
|
+
else
|
|
321
|
+
@global_marks[mark]
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def push_jump_location(location = current_location)
|
|
326
|
+
loc = normalize_location(location)
|
|
327
|
+
return nil unless loc
|
|
328
|
+
|
|
329
|
+
if @jump_index && @jump_index < @jumplist.length - 1
|
|
330
|
+
@jumplist = @jumplist[0..@jump_index]
|
|
331
|
+
end
|
|
332
|
+
@jumplist << loc unless same_location?(@jumplist.last, loc)
|
|
333
|
+
@jump_index = @jumplist.length - 1 unless @jumplist.empty?
|
|
334
|
+
loc
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def jump_older(linewise: false)
|
|
338
|
+
return nil if @jumplist.empty?
|
|
339
|
+
|
|
340
|
+
if @jump_index.nil?
|
|
341
|
+
push_jump_location(current_location)
|
|
342
|
+
else
|
|
343
|
+
@jump_index = [@jump_index - 1, 0].max
|
|
344
|
+
end
|
|
345
|
+
jump_to_location(@jumplist[@jump_index], linewise:)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def jump_newer(linewise: false)
|
|
349
|
+
return nil if @jumplist.empty? || @jump_index.nil?
|
|
350
|
+
|
|
351
|
+
next_idx = @jump_index + 1
|
|
352
|
+
return nil if next_idx >= @jumplist.length
|
|
353
|
+
|
|
354
|
+
@jump_index = next_idx
|
|
355
|
+
jump_to_location(@jumplist[@jump_index], linewise:)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def jump_to_mark(name, linewise: false)
|
|
359
|
+
loc = mark_location(name)
|
|
360
|
+
return nil unless loc
|
|
361
|
+
|
|
362
|
+
jump_to_location(loc, linewise:)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def visual_active?
|
|
366
|
+
!@visual_state.nil?
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def enter_visual(mode)
|
|
370
|
+
@mode = mode.to_sym
|
|
371
|
+
@visual_state = {
|
|
372
|
+
mode: mode.to_sym,
|
|
373
|
+
anchor_y: current_window.cursor_y,
|
|
374
|
+
anchor_x: current_window.cursor_x
|
|
375
|
+
}
|
|
376
|
+
@pending_count = nil
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def clear_visual
|
|
380
|
+
@visual_state = nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def visual_selection(window = current_window)
|
|
384
|
+
return nil unless @visual_state
|
|
385
|
+
|
|
386
|
+
ay = @visual_state[:anchor_y]
|
|
387
|
+
ax = @visual_state[:anchor_x]
|
|
388
|
+
cy = window.cursor_y
|
|
389
|
+
cx = window.cursor_x
|
|
390
|
+
case @visual_state[:mode]
|
|
391
|
+
when :visual_line
|
|
392
|
+
start_row, end_row = [ay, cy].minmax
|
|
393
|
+
{
|
|
394
|
+
mode: :linewise,
|
|
395
|
+
start_row: start_row,
|
|
396
|
+
start_col: 0,
|
|
397
|
+
end_row: end_row,
|
|
398
|
+
end_col: current_buffer.line_length(end_row)
|
|
399
|
+
}
|
|
400
|
+
when :visual_block
|
|
401
|
+
start_row, end_row = [ay, cy].minmax
|
|
402
|
+
start_col, end_col = [ax, cx].minmax
|
|
403
|
+
{
|
|
404
|
+
mode: :blockwise,
|
|
405
|
+
start_row: start_row,
|
|
406
|
+
start_col: start_col,
|
|
407
|
+
end_row: end_row,
|
|
408
|
+
end_col: end_col + 1
|
|
409
|
+
}
|
|
410
|
+
else
|
|
411
|
+
if ay < cy || (ay == cy && ax <= cx)
|
|
412
|
+
s_row, s_col, e_row, e_col = [ay, ax, cy, cx]
|
|
413
|
+
else
|
|
414
|
+
s_row, s_col, e_row, e_col = [cy, cx, ay, ax]
|
|
415
|
+
end
|
|
416
|
+
{
|
|
417
|
+
mode: :charwise,
|
|
418
|
+
start_row: s_row,
|
|
419
|
+
start_col: s_col,
|
|
420
|
+
end_row: e_row,
|
|
421
|
+
end_col: e_col + 1
|
|
422
|
+
}
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def add_empty_buffer(path: nil)
|
|
427
|
+
id = next_buffer_id
|
|
428
|
+
buffer = Buffer.new(id:, path:)
|
|
429
|
+
assign_detected_filetype(buffer)
|
|
430
|
+
@buffers[id] = buffer
|
|
431
|
+
buffer
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def add_virtual_buffer(kind:, name:, lines:, filetype: nil, readonly: true, modifiable: false)
|
|
435
|
+
id = next_buffer_id
|
|
436
|
+
buffer = Buffer.new(id:, lines:, kind:, name:, readonly:, modifiable:)
|
|
437
|
+
buffer.options["filetype"] = filetype if filetype
|
|
438
|
+
@buffers[id] = buffer
|
|
439
|
+
buffer
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def add_buffer_from_file(path)
|
|
443
|
+
id = next_buffer_id
|
|
444
|
+
buffer = Buffer.from_file(id:, path:)
|
|
445
|
+
assign_detected_filetype(buffer)
|
|
446
|
+
@buffers[id] = buffer
|
|
447
|
+
buffer
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def add_window(buffer_id:)
|
|
451
|
+
id = next_window_id
|
|
452
|
+
window = Window.new(id:, buffer_id:)
|
|
453
|
+
@windows[id] = window
|
|
454
|
+
@window_order << id
|
|
455
|
+
@current_window_id ||= id
|
|
456
|
+
ensure_initial_tabpage!
|
|
457
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
458
|
+
window
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def split_current_window(layout: :horizontal)
|
|
462
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
463
|
+
src = current_window
|
|
464
|
+
win = add_window(buffer_id: src.buffer_id)
|
|
465
|
+
win.cursor_x = src.cursor_x
|
|
466
|
+
win.cursor_y = src.cursor_y
|
|
467
|
+
win.row_offset = src.row_offset
|
|
468
|
+
win.col_offset = src.col_offset
|
|
469
|
+
@window_layout = layout.to_sym
|
|
470
|
+
@current_window_id = win.id
|
|
471
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
472
|
+
win
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def close_current_window
|
|
476
|
+
close_window(@current_window_id)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def close_window(id)
|
|
480
|
+
return nil if @window_order.empty?
|
|
481
|
+
return nil if @window_order.length <= 1
|
|
482
|
+
return nil unless @window_order.include?(id)
|
|
483
|
+
|
|
484
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
485
|
+
idx = @window_order.index(id) || 0
|
|
486
|
+
@windows.delete(id)
|
|
487
|
+
@window_order.delete(id)
|
|
488
|
+
@location_lists.delete(id)
|
|
489
|
+
@current_window_id = @window_order[[idx, @window_order.length - 1].min] if @current_window_id == id
|
|
490
|
+
@current_window_id ||= @window_order.first
|
|
491
|
+
@window_layout = :single if @window_order.length <= 1
|
|
492
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
493
|
+
current_window
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def close_current_tabpage
|
|
497
|
+
ensure_initial_tabpage!
|
|
498
|
+
return nil if @tabpages.length <= 1
|
|
499
|
+
|
|
500
|
+
save_current_tabpage_state!
|
|
501
|
+
removed = @tabpages.delete_at(@current_tabpage_index)
|
|
502
|
+
Array(removed && removed[:window_order]).each do |wid|
|
|
503
|
+
@windows.delete(wid)
|
|
504
|
+
@location_lists.delete(wid)
|
|
505
|
+
end
|
|
506
|
+
@current_tabpage_index = [@current_tabpage_index, @tabpages.length - 1].min
|
|
507
|
+
load_tabpage_state!(@tabpages[@current_tabpage_index])
|
|
508
|
+
current_window
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def focus_window(id)
|
|
512
|
+
return nil unless @windows.key?(id)
|
|
513
|
+
|
|
514
|
+
@current_window_id = id
|
|
515
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
516
|
+
current_window
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def focus_next_window
|
|
520
|
+
return current_window if @window_order.length <= 1
|
|
521
|
+
|
|
522
|
+
idx = @window_order.index(@current_window_id) || 0
|
|
523
|
+
focus_window(@window_order[(idx + 1) % @window_order.length])
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def focus_prev_window
|
|
527
|
+
return current_window if @window_order.length <= 1
|
|
528
|
+
|
|
529
|
+
idx = @window_order.index(@current_window_id) || 0
|
|
530
|
+
focus_window(@window_order[(idx - 1) % @window_order.length])
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def focus_window_direction(dir)
|
|
534
|
+
return current_window if @window_order.length <= 1
|
|
535
|
+
|
|
536
|
+
case @window_layout
|
|
537
|
+
when :vertical
|
|
538
|
+
if dir == :left
|
|
539
|
+
focus_prev_window
|
|
540
|
+
elsif dir == :right
|
|
541
|
+
focus_next_window
|
|
542
|
+
else
|
|
543
|
+
current_window
|
|
544
|
+
end
|
|
545
|
+
when :horizontal
|
|
546
|
+
if dir == :up
|
|
547
|
+
focus_prev_window
|
|
548
|
+
elsif dir == :down
|
|
549
|
+
focus_next_window
|
|
550
|
+
else
|
|
551
|
+
current_window
|
|
552
|
+
end
|
|
553
|
+
else
|
|
554
|
+
focus_next_window
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def switch_to_buffer(buffer_id)
|
|
559
|
+
prev_buffer_id = current_window&.buffer_id
|
|
560
|
+
current_window.buffer_id = buffer_id
|
|
561
|
+
current_window.cursor_x = 0
|
|
562
|
+
current_window.cursor_y = 0
|
|
563
|
+
current_window.row_offset = 0
|
|
564
|
+
current_window.col_offset = 0
|
|
565
|
+
if prev_buffer_id && prev_buffer_id != buffer_id
|
|
566
|
+
@alternate_buffer_id = prev_buffer_id
|
|
567
|
+
end
|
|
568
|
+
save_current_tabpage_state! unless @suspend_tab_autosave
|
|
569
|
+
current_window
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def open_path(path)
|
|
573
|
+
buffer = add_buffer_from_file(path)
|
|
574
|
+
switch_to_buffer(buffer.id)
|
|
575
|
+
echo(path && File.exist?(path) ? "\"#{path}\" #{buffer.line_count}L" : "\"#{path}\" [New File]")
|
|
576
|
+
buffer
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def ensure_bootstrap_buffer!
|
|
580
|
+
return unless @buffers.empty?
|
|
581
|
+
|
|
582
|
+
buffer = add_empty_buffer
|
|
583
|
+
add_window(buffer_id: buffer.id)
|
|
584
|
+
ensure_initial_tabpage!
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def show_help_buffer!(title:, lines:, filetype: "help")
|
|
588
|
+
buffer = add_virtual_buffer(
|
|
589
|
+
kind: :help,
|
|
590
|
+
name: title,
|
|
591
|
+
lines: Array(lines),
|
|
592
|
+
filetype: filetype,
|
|
593
|
+
readonly: true,
|
|
594
|
+
modifiable: false
|
|
595
|
+
)
|
|
596
|
+
switch_to_buffer(buffer.id)
|
|
597
|
+
echo(title)
|
|
598
|
+
buffer
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def show_intro_buffer_if_applicable!
|
|
602
|
+
return unless @buffers.length == 1
|
|
603
|
+
return unless current_buffer.file_buffer?
|
|
604
|
+
return unless current_buffer.path.nil?
|
|
605
|
+
return unless current_buffer.lines == [""]
|
|
606
|
+
return if current_buffer.modified?
|
|
607
|
+
|
|
608
|
+
current_buffer.replace_all_lines!(intro_lines)
|
|
609
|
+
current_buffer.configure_special!(kind: :intro, name: "[Intro]", readonly: true, modifiable: false)
|
|
610
|
+
current_buffer.modified = false
|
|
611
|
+
current_buffer.options["filetype"] = "help"
|
|
612
|
+
current_window.cursor_x = 0
|
|
613
|
+
current_window.cursor_y = 0
|
|
614
|
+
current_window.row_offset = 0
|
|
615
|
+
current_window.col_offset = 0
|
|
616
|
+
clear_message
|
|
617
|
+
current_buffer
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def materialize_intro_buffer!
|
|
621
|
+
return false unless current_buffer.intro_buffer?
|
|
622
|
+
|
|
623
|
+
current_buffer.become_normal_empty_buffer!
|
|
624
|
+
current_buffer.options["filetype"] = nil
|
|
625
|
+
current_window.cursor_x = 0
|
|
626
|
+
current_window.cursor_y = 0
|
|
627
|
+
current_window.row_offset = 0
|
|
628
|
+
current_window.col_offset = 0
|
|
629
|
+
true
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def buffer_ids
|
|
633
|
+
@buffers.keys
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def next_buffer_id_from(current_id, step = 1)
|
|
637
|
+
ids = buffer_ids
|
|
638
|
+
return nil if ids.empty?
|
|
639
|
+
|
|
640
|
+
idx = ids.index(current_id) || 0
|
|
641
|
+
ids[(idx + step) % ids.length]
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def window_order
|
|
645
|
+
@window_order
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def tabpages
|
|
649
|
+
@tabpages
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def current_tabpage_index
|
|
653
|
+
@current_tabpage_index || 0
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def current_tabpage_number
|
|
657
|
+
current_tabpage_index + 1
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def tabpage_count
|
|
661
|
+
@tabpages.length
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def window_count
|
|
665
|
+
@window_order.length
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def quickfix_items
|
|
669
|
+
@quickfix_list[:items]
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def quickfix_index
|
|
673
|
+
@quickfix_list[:index]
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def set_quickfix_list(items)
|
|
677
|
+
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
678
|
+
@quickfix_list = { items: ary, index: ary.empty? ? nil : 0 }
|
|
679
|
+
@quickfix_list
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def current_quickfix_item
|
|
683
|
+
idx = @quickfix_list[:index]
|
|
684
|
+
idx ? @quickfix_list[:items][idx] : nil
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def move_quickfix(step)
|
|
688
|
+
items = @quickfix_list[:items]
|
|
689
|
+
return nil if items.empty?
|
|
690
|
+
|
|
691
|
+
@quickfix_list[:index] = ((@quickfix_list[:index] || 0) + step.to_i) % items.length
|
|
692
|
+
current_quickfix_item
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def location_list(window_id = current_window_id)
|
|
696
|
+
@location_lists[window_id]
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def location_items(window_id = current_window_id)
|
|
700
|
+
location_list(window_id)[:items]
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def set_location_list(items, window_id: current_window_id)
|
|
704
|
+
ary = Array(items).map { |it| normalize_location(it)&.merge(text: (it[:text] || it["text"]).to_s) }.compact
|
|
705
|
+
@location_lists[window_id] = { items: ary, index: ary.empty? ? nil : 0 }
|
|
706
|
+
@location_lists[window_id]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def current_location_list_item(window_id = current_window_id)
|
|
710
|
+
list = location_list(window_id)
|
|
711
|
+
idx = list[:index]
|
|
712
|
+
idx ? list[:items][idx] : nil
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def move_location_list(step, window_id: current_window_id)
|
|
716
|
+
list = location_list(window_id)
|
|
717
|
+
items = list[:items]
|
|
718
|
+
return nil if items.empty?
|
|
719
|
+
|
|
720
|
+
list[:index] = ((list[:index] || 0) + step.to_i) % items.length
|
|
721
|
+
current_location_list_item(window_id)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def find_window_ids_by_buffer_kind(kind)
|
|
725
|
+
sym = kind.to_sym
|
|
726
|
+
@window_order.select do |wid|
|
|
727
|
+
win = @windows[wid]
|
|
728
|
+
buf = win && @buffers[win.buffer_id]
|
|
729
|
+
buf && buf.kind == sym
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def tabnew(path: nil)
|
|
734
|
+
ensure_initial_tabpage!
|
|
735
|
+
save_current_tabpage_state!
|
|
736
|
+
|
|
737
|
+
with_tab_autosave_suspended do
|
|
738
|
+
@window_order = []
|
|
739
|
+
@current_window_id = nil
|
|
740
|
+
@window_layout = :single
|
|
741
|
+
|
|
742
|
+
buffer = path ? add_buffer_from_file(path) : add_empty_buffer
|
|
743
|
+
add_window(buffer_id: buffer.id)
|
|
744
|
+
tab = new_tabpage_snapshot
|
|
745
|
+
@tabpages << tab
|
|
746
|
+
@current_tabpage_index = @tabpages.length - 1
|
|
747
|
+
load_tabpage_state!(tab)
|
|
748
|
+
return tab
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def tabnext(step = 1)
|
|
753
|
+
return nil if @tabpages.empty?
|
|
754
|
+
save_current_tabpage_state!
|
|
755
|
+
@current_tabpage_index = (@current_tabpage_index + step) % @tabpages.length
|
|
756
|
+
load_tabpage_state!(@tabpages[@current_tabpage_index])
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def tabprev(step = 1)
|
|
760
|
+
tabnext(-step)
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def enter_normal_mode
|
|
764
|
+
@mode = :normal
|
|
765
|
+
@pending_count = nil
|
|
766
|
+
clear_visual
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def enter_insert_mode
|
|
770
|
+
@mode = :insert
|
|
771
|
+
@pending_count = nil
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def enter_command_line_mode(prefix = ":")
|
|
775
|
+
@mode = :command_line
|
|
776
|
+
@command_line.reset(prefix:)
|
|
777
|
+
@pending_count = nil
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def cancel_command_line
|
|
781
|
+
@command_line.clear
|
|
782
|
+
enter_normal_mode
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def echo(msg)
|
|
786
|
+
@message_kind = :info
|
|
787
|
+
@message = msg.to_s
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def echo_error(msg)
|
|
791
|
+
@message_kind = :error
|
|
792
|
+
@message = msg.to_s
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def clear_message
|
|
796
|
+
@message_kind = :info
|
|
797
|
+
@message = ""
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def message_error?
|
|
801
|
+
!@message.to_s.empty? && @message_kind == :error
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def text_viewport_size(rows:, cols:)
|
|
805
|
+
text_rows = command_area_active? ? rows - 2 : rows - 1
|
|
806
|
+
[text_rows, cols]
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
def command_line_active?
|
|
810
|
+
@mode == :command_line
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def command_area_active?
|
|
814
|
+
command_line_active? || message_error?
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
private
|
|
818
|
+
|
|
819
|
+
def default_global_options
|
|
820
|
+
OPTION_DEFS.each_with_object({}) { |(k, v), h| h[k] = v[:default] }
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def coerce_option_value(name, value)
|
|
824
|
+
defn = option_def(name)
|
|
825
|
+
return value unless defn
|
|
826
|
+
|
|
827
|
+
case defn[:type]
|
|
828
|
+
when :bool
|
|
829
|
+
!!value
|
|
830
|
+
when :int
|
|
831
|
+
iv = value.is_a?(Integer) ? value : Integer(value)
|
|
832
|
+
raise RuVim::CommandError, "#{name} must be >= 0" if iv.negative?
|
|
833
|
+
iv
|
|
834
|
+
when :string
|
|
835
|
+
value.nil? ? nil : value.to_s
|
|
836
|
+
else
|
|
837
|
+
value
|
|
838
|
+
end
|
|
839
|
+
rescue ArgumentError, TypeError
|
|
840
|
+
raise RuVim::CommandError, "Invalid value for #{name}: #{value.inspect}"
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def write_register_payload(key, text:, type:)
|
|
844
|
+
if key.match?(/\A[A-Z]\z/)
|
|
845
|
+
base = key.downcase
|
|
846
|
+
prev = @registers[base]
|
|
847
|
+
payload = { text: "#{prev ? prev[:text] : ""}#{text}", type: type }
|
|
848
|
+
@registers[base] = payload
|
|
849
|
+
else
|
|
850
|
+
payload = { text: text, type: type }
|
|
851
|
+
@registers[key.downcase] = payload
|
|
852
|
+
end
|
|
853
|
+
payload
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def rotate_delete_registers!(payload)
|
|
857
|
+
9.downto(2) do |i|
|
|
858
|
+
prev = @registers[(i - 1).to_s]
|
|
859
|
+
if prev
|
|
860
|
+
@registers[i.to_s] = dup_register_payload(prev)
|
|
861
|
+
else
|
|
862
|
+
@registers.delete(i.to_s)
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
@registers["1"] = dup_register_payload(payload)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def dup_register_payload(payload)
|
|
869
|
+
return nil unless payload
|
|
870
|
+
|
|
871
|
+
{ text: payload[:text].to_s.dup, type: payload[:type].to_sym }
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def assign_detected_filetype(buffer)
|
|
875
|
+
ft = detect_filetype(buffer.path)
|
|
876
|
+
buffer.options["filetype"] = ft if ft && !ft.empty?
|
|
877
|
+
buffer
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def intro_lines
|
|
881
|
+
[
|
|
882
|
+
"RuVim - Vi IMproved (Ruby edition)",
|
|
883
|
+
"",
|
|
884
|
+
"type :help for help",
|
|
885
|
+
"type :help regex for Ruby Regexp search/substitute help",
|
|
886
|
+
"type :q to quit",
|
|
887
|
+
"",
|
|
888
|
+
"keys i a o to start editing",
|
|
889
|
+
"keys / ? to search",
|
|
890
|
+
"keys : to enter command-line",
|
|
891
|
+
"",
|
|
892
|
+
"This is the intro screen (Vim-style).",
|
|
893
|
+
"It will be replaced with an empty buffer when you start editing."
|
|
894
|
+
]
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
public
|
|
898
|
+
|
|
899
|
+
def jump_to_location(loc, linewise: false)
|
|
900
|
+
location = normalize_location(loc)
|
|
901
|
+
return nil unless location
|
|
902
|
+
return nil unless @buffers.key?(location[:buffer_id])
|
|
903
|
+
|
|
904
|
+
switch_to_buffer(location[:buffer_id]) if current_buffer.id != location[:buffer_id]
|
|
905
|
+
current_window.cursor_y = location[:row]
|
|
906
|
+
current_window.cursor_x = linewise ? 0 : location[:col]
|
|
907
|
+
current_window.clamp_to_buffer(current_buffer)
|
|
908
|
+
current_window.cursor_x = first_nonblank_col(current_buffer, current_window.cursor_y) if linewise
|
|
909
|
+
current_window.clamp_to_buffer(current_buffer)
|
|
910
|
+
current_location
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
private
|
|
914
|
+
|
|
915
|
+
def first_nonblank_col(buffer, row)
|
|
916
|
+
line = buffer.line_at(row)
|
|
917
|
+
line.index(/\S/) || 0
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def normalize_location(loc)
|
|
921
|
+
return nil unless loc
|
|
922
|
+
|
|
923
|
+
{
|
|
924
|
+
buffer_id: Integer(loc[:buffer_id] || loc["buffer_id"]),
|
|
925
|
+
row: [Integer(loc[:row] || loc["row"]), 0].max,
|
|
926
|
+
col: [Integer(loc[:col] || loc["col"]), 0].max
|
|
927
|
+
}
|
|
928
|
+
rescue StandardError
|
|
929
|
+
nil
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def same_location?(a, b)
|
|
933
|
+
return false unless a && b
|
|
934
|
+
|
|
935
|
+
a[:buffer_id] == b[:buffer_id] && a[:row] == b[:row] && a[:col] == b[:col]
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def dup_macro_key(key)
|
|
939
|
+
case key
|
|
940
|
+
when String
|
|
941
|
+
key.dup
|
|
942
|
+
when Array
|
|
943
|
+
key.map { |v| v.is_a?(String) ? v.dup : v }
|
|
944
|
+
else
|
|
945
|
+
key
|
|
946
|
+
end
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def clipboard_register?(key)
|
|
950
|
+
key == "+" || key == "*"
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def write_clipboard_register(key, payload)
|
|
954
|
+
return unless clipboard_register?(key.downcase)
|
|
955
|
+
|
|
956
|
+
RuVim::Clipboard.write(payload[:text])
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def read_clipboard_register(key)
|
|
960
|
+
text = RuVim::Clipboard.read
|
|
961
|
+
if text
|
|
962
|
+
payload = { text: text, type: text.end_with?("\n") ? :linewise : :charwise }
|
|
963
|
+
@registers[key] = payload
|
|
964
|
+
end
|
|
965
|
+
@registers[key]
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def ensure_initial_tabpage!
|
|
969
|
+
return unless @tabpages.empty?
|
|
970
|
+
return if @window_order.empty?
|
|
971
|
+
|
|
972
|
+
@tabpages << new_tabpage_snapshot
|
|
973
|
+
@current_tabpage_index = 0
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def save_current_tabpage_state!
|
|
977
|
+
return if @current_tabpage_index.nil?
|
|
978
|
+
return if @tabpages.empty?
|
|
979
|
+
|
|
980
|
+
@tabpages[@current_tabpage_index] = new_tabpage_snapshot(id: @tabpages[@current_tabpage_index][:id])
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def load_tabpage_state!(tab)
|
|
984
|
+
@window_order = tab[:window_order].dup
|
|
985
|
+
@current_window_id = tab[:current_window_id]
|
|
986
|
+
@window_layout = tab[:window_layout]
|
|
987
|
+
current_window
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
def new_tabpage_snapshot(id: nil)
|
|
991
|
+
{
|
|
992
|
+
id: id || next_tabpage_id,
|
|
993
|
+
window_order: @window_order.dup,
|
|
994
|
+
current_window_id: @current_window_id,
|
|
995
|
+
window_layout: @window_layout
|
|
996
|
+
}
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def next_buffer_id
|
|
1000
|
+
id = @next_buffer_id
|
|
1001
|
+
@next_buffer_id += 1
|
|
1002
|
+
id
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def next_window_id
|
|
1006
|
+
id = @next_window_id
|
|
1007
|
+
@next_window_id += 1
|
|
1008
|
+
id
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def next_tabpage_id
|
|
1012
|
+
id = @next_tabpage_id
|
|
1013
|
+
@next_tabpage_id += 1
|
|
1014
|
+
id
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def with_tab_autosave_suspended
|
|
1018
|
+
prev = @suspend_tab_autosave
|
|
1019
|
+
@suspend_tab_autosave = true
|
|
1020
|
+
yield
|
|
1021
|
+
ensure
|
|
1022
|
+
@suspend_tab_autosave = prev
|
|
1023
|
+
end
|
|
1024
|
+
end
|
|
1025
|
+
end
|