canopus 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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/docs/adr/001-runtime-gem-components.md +24 -0
- data/docs/adr/002-bounded-diff-engine.md +22 -0
- data/docs/adr/003-background-language-analysis.md +22 -0
- data/docs/adr/004-persistent-display-map.md +23 -0
- data/docs/adr/005-windows-runtime.md +27 -0
- data/docs/adr/README.md +36 -0
- data/docs/distribution.md +27 -0
- data/docs/lsp.md +38 -0
- data/docs/performance.md +40 -0
- data/docs/snippets.md +32 -0
- data/docs/vim.md +24 -0
- data/docs/workspace_edits.md +32 -0
- data/examples/native_smoke.rb +25 -0
- data/examples/plugins/word_count.rb +11 -0
- data/exe/canopus +19 -0
- data/lib/canopus/block_map.rb +19 -0
- data/lib/canopus/buffer.rb +293 -0
- data/lib/canopus/cli.rb +171 -0
- data/lib/canopus/controller.rb +507 -0
- data/lib/canopus/data_compat.rb +25 -0
- data/lib/canopus/display_map/line_builder.rb +54 -0
- data/lib/canopus/display_map/line_set.rb +8 -0
- data/lib/canopus/display_map/pending_line_set.rb +9 -0
- data/lib/canopus/display_map/summary.rb +23 -0
- data/lib/canopus/display_map/worker.rb +89 -0
- data/lib/canopus/display_map.rb +350 -0
- data/lib/canopus/display_point.rb +5 -0
- data/lib/canopus/editor/snippet_expandable.rb +281 -0
- data/lib/canopus/editor.rb +445 -0
- data/lib/canopus/error.rb +5 -0
- data/lib/canopus/fold_map.rb +87 -0
- data/lib/canopus/git/blame.rb +50 -0
- data/lib/canopus/git/commit.rb +7 -0
- data/lib/canopus/git/corrupt_object.rb +7 -0
- data/lib/canopus/git/diff.rb +131 -0
- data/lib/canopus/git/index.rb +94 -0
- data/lib/canopus/git/object_database.rb +45 -0
- data/lib/canopus/git/pack.rb +186 -0
- data/lib/canopus/git/repository.rb +313 -0
- data/lib/canopus/git/status.rb +74 -0
- data/lib/canopus/git/tree_entry.rb +7 -0
- data/lib/canopus/git.rb +15 -0
- data/lib/canopus/icon_theme.rb +43 -0
- data/lib/canopus/language/background_analysis/job.rb +12 -0
- data/lib/canopus/language/background_analysis/scheduler.rb +70 -0
- data/lib/canopus/language/background_analysis.rb +280 -0
- data/lib/canopus/language/definition.rb +7 -0
- data/lib/canopus/language/document.rb +143 -0
- data/lib/canopus/language/symbol.rb +7 -0
- data/lib/canopus/language/syntax_worker.rb +91 -0
- data/lib/canopus/language.rb +42 -0
- data/lib/canopus/lazy_rope.rb +218 -0
- data/lib/canopus/lsp/client.rb +299 -0
- data/lib/canopus/lsp/error.rb +7 -0
- data/lib/canopus/lsp/future/subscription.rb +9 -0
- data/lib/canopus/lsp/future.rb +87 -0
- data/lib/canopus/lsp/protocol.rb +83 -0
- data/lib/canopus/lsp/server_error.rb +13 -0
- data/lib/canopus/lsp/timeout.rb +7 -0
- data/lib/canopus/lsp/transport.rb +123 -0
- data/lib/canopus/lsp.rb +19 -0
- data/lib/canopus/markdown.rb +69 -0
- data/lib/canopus/match_data_compat.rb +17 -0
- data/lib/canopus/multi_buffer.rb +266 -0
- data/lib/canopus/pane.rb +61 -0
- data/lib/canopus/patch/composite.rb +12 -0
- data/lib/canopus/patch/reload.rb +18 -0
- data/lib/canopus/patch.rb +27 -0
- data/lib/canopus/performance_recorder.rb +207 -0
- data/lib/canopus/plugins/api.rb +50 -0
- data/lib/canopus/plugins/isolated_runtime.rb +167 -0
- data/lib/canopus/plugins/local_runtime.rb +25 -0
- data/lib/canopus/plugins/permission_denied.rb +7 -0
- data/lib/canopus/plugins/registry.rb +30 -0
- data/lib/canopus/plugins.rb +11 -0
- data/lib/canopus/project/ignore_matcher.rb +104 -0
- data/lib/canopus/project/search.rb +164 -0
- data/lib/canopus/project/search_worker/cancelled.rb +3 -0
- data/lib/canopus/project/search_worker/runner.rb +9 -0
- data/lib/canopus/project/search_worker.rb +108 -0
- data/lib/canopus/project/tree.rb +48 -0
- data/lib/canopus/project/watcher.rb +59 -0
- data/lib/canopus/project.rb +127 -0
- data/lib/canopus/regexp_compat.rb +30 -0
- data/lib/canopus/save_conflict.rb +5 -0
- data/lib/canopus/selection.rb +11 -0
- data/lib/canopus/settings.rb +118 -0
- data/lib/canopus/snippet/transform.rb +209 -0
- data/lib/canopus/snippet.rb +183 -0
- data/lib/canopus/tab_map.rb +30 -0
- data/lib/canopus/terminal/cell.rb +7 -0
- data/lib/canopus/terminal/grid.rb +321 -0
- data/lib/canopus/terminal/pty.rb +103 -0
- data/lib/canopus/terminal/scrollback.rb +38 -0
- data/lib/canopus/terminal/vt.rb +398 -0
- data/lib/canopus/terminal.rb +13 -0
- data/lib/canopus/theme.rb +43 -0
- data/lib/canopus/version.rb +5 -0
- data/lib/canopus/vim/commandable.rb +148 -0
- data/lib/canopus/vim/motionable.rb +321 -0
- data/lib/canopus/vim/operator_capable.rb +377 -0
- data/lib/canopus/vim/text_object_selectable.rb +141 -0
- data/lib/canopus/vim.rb +426 -0
- data/lib/canopus/workspace/edit/executable.rb +139 -0
- data/lib/canopus/workspace/edit/node.rb +8 -0
- data/lib/canopus/workspace/edit/plan.rb +170 -0
- data/lib/canopus/workspace/edit/resource_preparable.rb +98 -0
- data/lib/canopus/workspace/edit.rb +6 -0
- data/lib/canopus/workspace/file_change_aware.rb +40 -0
- data/lib/canopus/workspace/file_previewable.rb +31 -0
- data/lib/canopus/workspace/git_aware.rb +140 -0
- data/lib/canopus/workspace/language_aware.rb +413 -0
- data/lib/canopus/workspace/language_server_configurable.rb +181 -0
- data/lib/canopus/workspace/project_searchable.rb +208 -0
- data/lib/canopus/workspace/project_tree_editable.rb +82 -0
- data/lib/canopus/workspace/session_persistable.rb +153 -0
- data/lib/canopus/workspace/settings_aware.rb +91 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +185 -0
- data/lib/canopus/workspace/view.rb +643 -0
- data/lib/canopus/workspace.rb +525 -0
- data/lib/canopus/wrap_map.rb +108 -0
- data/lib/canopus.rb +24 -0
- data/sig/canopus.rbs +375 -0
- data/sig/controller.rbs +55 -0
- data/sig/display_stages.rbs +49 -0
- data/sig/git.rbs +140 -0
- data/sig/lsp.rbs +99 -0
- data/sig/markdown.rbs +11 -0
- data/sig/performance_recorder.rbs +23 -0
- data/sig/search_services.rbs +8 -0
- data/sig/services.rbs +81 -0
- data/sig/snippet.rbs +42 -0
- data/sig/terminal.rbs +125 -0
- data/sig/vim.rbs +27 -0
- data/sig/workspace_edits.rbs +11 -0
- data/sig/workspace_services.rbs +117 -0
- data/tools/certify_snippet_regex.rb +35 -0
- data/tools/check_dependencies.rb +80 -0
- data/tools/native_check.rb +70 -0
- data/tools/package.rb +113 -0
- data/tools/package_test.rb +32 -0
- metadata +314 -0
data/lib/canopus/vim.rb
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
class Vim
|
|
5
|
+
OPERATORS = %w[d c y > < = gu gU g~].freeze
|
|
6
|
+
VISUAL_MODES = %i[visual visual_line visual_block].freeze
|
|
7
|
+
attr_reader :mode, :registers, :marks, :macros, :command_line, :status
|
|
8
|
+
attr_accessor :on_command
|
|
9
|
+
|
|
10
|
+
def initialize(editor)
|
|
11
|
+
@editor, @mode = editor, :normal
|
|
12
|
+
@registers, @marks, @macros = {}, {}, {}
|
|
13
|
+
@count, @command, @last_change = +"", [], []
|
|
14
|
+
@register, @status, @play_depth = '"', "NORMAL", 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cursor_position = visual? ? @visual_head : @editor.primary.head
|
|
18
|
+
|
|
19
|
+
# A pane losing focus must not keep a shared buffer's undo transaction open.
|
|
20
|
+
# Return to Normal mode, retaining marks, registers and the last change.
|
|
21
|
+
def deactivate
|
|
22
|
+
%i[insert replace].include?(@mode) ? insert_key("esc") : escape
|
|
23
|
+
@command_line = nil
|
|
24
|
+
@status = "NORMAL"
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dispose
|
|
29
|
+
end_group
|
|
30
|
+
(@marks.values + [@block_insert_start]).compact.uniq.each { |anchor| @editor.buffer.release_anchor(anchor) }
|
|
31
|
+
@marks.clear
|
|
32
|
+
@block_insert_start = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def feed(key)
|
|
36
|
+
@message = nil
|
|
37
|
+
key = "esc" if key == "\e"
|
|
38
|
+
key = "enter" if ["\r", "\n"].include?(key)
|
|
39
|
+
@macros[@recording] << key if @recording && !(key == "q" && @mode == :normal && !@prefix)
|
|
40
|
+
return command_key(key) if @command_line
|
|
41
|
+
return insert_key(key) if %i[insert replace].include?(@mode)
|
|
42
|
+
@visual_keys << key if visual? && @visual_keys
|
|
43
|
+
@command << key
|
|
44
|
+
return escape if %w[esc ctrl-c].include?(key)
|
|
45
|
+
return prefix_key(key) if @prefix
|
|
46
|
+
if key.match?(/\A\d\z/) && (key != "0" || !@count.empty?)
|
|
47
|
+
@count << key
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
explicit = !@count.empty?
|
|
51
|
+
count = explicit ? @count.to_i.clamp(1, 10_000) : 1
|
|
52
|
+
@count.clear
|
|
53
|
+
return operate_key(key, count, explicit) if @operator
|
|
54
|
+
if visual? && (%w[d c y > < = x s D C Y].include?(key))
|
|
55
|
+
return operate_visual({"x" => "d", "s" => "c", "D" => "d", "C" => "c", "Y" => "y"}.fetch(key, key), count: count)
|
|
56
|
+
end
|
|
57
|
+
if visual? && %w[i a].include?(key)
|
|
58
|
+
@prefix, @prefix_count = key, count
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
case key
|
|
62
|
+
when "i", "a", "I", "A", "o", "O", "R"
|
|
63
|
+
enter_insert(key, count)
|
|
64
|
+
when "v", "V", "ctrl-v"
|
|
65
|
+
requested = {"v" => :visual, "V" => :visual_line, "ctrl-v" => :visual_block}.fetch(key)
|
|
66
|
+
if @mode == requested
|
|
67
|
+
escape
|
|
68
|
+
else
|
|
69
|
+
@visual_anchor = @visual_head = cursor_position unless visual?
|
|
70
|
+
@mode = requested
|
|
71
|
+
@visual_keys = @command.dup
|
|
72
|
+
update_visual
|
|
73
|
+
@command.clear
|
|
74
|
+
end
|
|
75
|
+
when "d", "c", "y", ">", "<", "="
|
|
76
|
+
@operator, @operator_count, @operator_explicit = key, count, explicit
|
|
77
|
+
when "g", "f", "F", "t", "T", "m", "'", "`", '"', "@", "r"
|
|
78
|
+
@prefix, @prefix_count, @prefix_explicit = key, count, explicit
|
|
79
|
+
when "q"
|
|
80
|
+
if @recording
|
|
81
|
+
save_macro_register(@recording)
|
|
82
|
+
@recording = nil
|
|
83
|
+
@command.clear
|
|
84
|
+
else
|
|
85
|
+
@prefix = "q"
|
|
86
|
+
end
|
|
87
|
+
when "x", "X", "s"
|
|
88
|
+
first = cursor_position
|
|
89
|
+
endpoint = horizontal(first, key == "X" ? -count : count, insertion: true)
|
|
90
|
+
operate(key == "s" ? "c" : "d", [first, endpoint].min...[first, endpoint].max)
|
|
91
|
+
when "D", "C", "Y", "S"
|
|
92
|
+
if %w[Y S].include?(key)
|
|
93
|
+
operate(key == "Y" ? "y" : "c", line_range(count), linewise: true)
|
|
94
|
+
else
|
|
95
|
+
ending = line_end([row_at(cursor_position) + count - 1, last_row].min)
|
|
96
|
+
operate(key == "D" ? "d" : "c", cursor_position...ending)
|
|
97
|
+
end
|
|
98
|
+
when "p", "P" then paste(key == "p", count)
|
|
99
|
+
when "u", "ctrl-r"
|
|
100
|
+
groups = @macro_groups || 0
|
|
101
|
+
groups.times { @editor.buffer.end_undo_group }
|
|
102
|
+
begin
|
|
103
|
+
count.times { key == "u" ? @editor.undo : @editor.redo }
|
|
104
|
+
ensure
|
|
105
|
+
groups.times { @editor.buffer.begin_undo_group }
|
|
106
|
+
end
|
|
107
|
+
@editor.select(normal_offset(@editor.primary.head))
|
|
108
|
+
@command.clear
|
|
109
|
+
when "."
|
|
110
|
+
change = @last_change.dup
|
|
111
|
+
@command.clear
|
|
112
|
+
count.times { replay(change) }
|
|
113
|
+
when "J" then join_lines(count)
|
|
114
|
+
when "~"
|
|
115
|
+
if visual?
|
|
116
|
+
operate_visual("g~")
|
|
117
|
+
else
|
|
118
|
+
first = cursor_position
|
|
119
|
+
ending = horizontal(first, count, insertion: true)
|
|
120
|
+
operate("g~", first...ending, destination: normal_offset(ending))
|
|
121
|
+
end
|
|
122
|
+
when ":", "/", "?"
|
|
123
|
+
@command_line, @command_type = +"", key
|
|
124
|
+
@command_line = +"'<,'>" if key == ":" && visual?
|
|
125
|
+
when "n", "N"
|
|
126
|
+
search_next(reverse: key == "N", count: count)
|
|
127
|
+
@command.clear
|
|
128
|
+
when "*", "#"
|
|
129
|
+
range = text_object("w", inner: true, count: 1)
|
|
130
|
+
if range
|
|
131
|
+
@search = "\\b#{Regexp.escape(@editor.buffer.rope.byteslice(range).to_s)}\\b"
|
|
132
|
+
@search_direction = key == "*" ? 1 : -1
|
|
133
|
+
search_next(count: count)
|
|
134
|
+
end
|
|
135
|
+
@command.clear
|
|
136
|
+
else
|
|
137
|
+
move(key, count, explicit: explicit)
|
|
138
|
+
@command.clear
|
|
139
|
+
end
|
|
140
|
+
rescue Error
|
|
141
|
+
end_group
|
|
142
|
+
@mode, @operator, @prefix = :normal, nil, nil
|
|
143
|
+
raise
|
|
144
|
+
ensure
|
|
145
|
+
@status = @message || (@command_line ? "#{@command_type}#{@command_line}" : @mode.to_s.upcase)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def visual? = VISUAL_MODES.include?(@mode)
|
|
151
|
+
def start_group
|
|
152
|
+
return if @insert_group
|
|
153
|
+
@editor.buffer.begin_undo_group
|
|
154
|
+
@insert_group = true
|
|
155
|
+
end
|
|
156
|
+
def end_group
|
|
157
|
+
return unless @insert_group
|
|
158
|
+
@editor.buffer.end_undo_group
|
|
159
|
+
@insert_group = false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def escape
|
|
163
|
+
position = cursor_position
|
|
164
|
+
@last_visual = [@mode, @visual_anchor, @visual_head] if visual?
|
|
165
|
+
@mode, @operator, @prefix, @count, @command = :normal, nil, nil, +"", []
|
|
166
|
+
@register = '"'
|
|
167
|
+
@editor.select(normal_offset(position))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def insert_key(key)
|
|
171
|
+
@change_keys << key
|
|
172
|
+
if @insert_register
|
|
173
|
+
@insert_register = false
|
|
174
|
+
value = @registers[key]&.first
|
|
175
|
+
@editor.replace_selections(value, kind: :vim_insert) if value
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
case key
|
|
179
|
+
when "esc", "ctrl-c"
|
|
180
|
+
recorded = @change_keys.dup
|
|
181
|
+
if @insert_count > 1
|
|
182
|
+
keys = recorded.drop(@insert_prefix_length)[0...-1]
|
|
183
|
+
repeats, @insert_count = @insert_count - 1, 1
|
|
184
|
+
repeats.times { keys.each { |item| insert_key(item) } }
|
|
185
|
+
end
|
|
186
|
+
position = if @block_insert_start
|
|
187
|
+
@block_insert_keep_start ? @editor.buffer.resolve(@block_insert_start) : @editor.selections.first.head
|
|
188
|
+
else
|
|
189
|
+
@editor.primary.head
|
|
190
|
+
end
|
|
191
|
+
position = previous_offset(position) if !@block_insert_keep_start && position > line_start(row_at(position))
|
|
192
|
+
@editor.buffer.release_anchor(@block_insert_start) if @block_insert_start
|
|
193
|
+
@block_insert_start = nil
|
|
194
|
+
@block_insert_keep_start = false
|
|
195
|
+
@mode = :normal
|
|
196
|
+
@editor.select(normal_offset(position))
|
|
197
|
+
end_group
|
|
198
|
+
@last_change = recorded
|
|
199
|
+
@command.clear
|
|
200
|
+
when "ctrl-r" then @insert_register = true
|
|
201
|
+
when "backspace"
|
|
202
|
+
if @mode == :replace && @replace_stack.last && @replace_stack.last[1] == @editor.primary.head
|
|
203
|
+
first, ending, original = @replace_stack.pop
|
|
204
|
+
@editor.select(first, ending)
|
|
205
|
+
@editor.replace_selections(original, kind: :vim_insert)
|
|
206
|
+
@editor.select(first)
|
|
207
|
+
else
|
|
208
|
+
@editor.delete_backward
|
|
209
|
+
end
|
|
210
|
+
when "delete" then @editor.delete_forward
|
|
211
|
+
when "left", "right", "up", "down" then @editor.move(key.to_sym)
|
|
212
|
+
when "enter" then @editor.insert_text("\n")
|
|
213
|
+
when "tab" then @editor.insert_text(@editor.use_tabs ? "\t" : " " * @editor.tab_size)
|
|
214
|
+
else
|
|
215
|
+
return unless key.grapheme_clusters.length == 1
|
|
216
|
+
auto_pairs = @editor.auto_pairs
|
|
217
|
+
begin
|
|
218
|
+
@editor.auto_pairs = false
|
|
219
|
+
if @mode == :replace
|
|
220
|
+
first = @editor.primary.head
|
|
221
|
+
ending = horizontal(first, 1, insertion: true)
|
|
222
|
+
original = @editor.buffer.rope.byteslice(first...ending).to_s
|
|
223
|
+
@editor.select(first, ending)
|
|
224
|
+
@replace_stack << [first, first + key.bytesize, original]
|
|
225
|
+
end
|
|
226
|
+
@editor.insert_text(key)
|
|
227
|
+
ensure
|
|
228
|
+
@editor.auto_pairs = auto_pairs
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def enter_insert(key, count = 1)
|
|
234
|
+
start_group
|
|
235
|
+
if @mode == :visual_block && %w[I A].include?(key)
|
|
236
|
+
return enter_block_insert(key, count)
|
|
237
|
+
end
|
|
238
|
+
@change_keys, @insert_count, @replace_stack = @command.dup, count, []
|
|
239
|
+
@insert_prefix_length = @change_keys.length
|
|
240
|
+
case key
|
|
241
|
+
when "a" then @editor.select(horizontal(cursor_position, 1, insertion: true))
|
|
242
|
+
when "I" then @editor.select(first_nonblank(row_at(cursor_position)))
|
|
243
|
+
when "A" then @editor.select(line_end(row_at(cursor_position)))
|
|
244
|
+
when "o"
|
|
245
|
+
@editor.select(line_end(row_at(cursor_position)))
|
|
246
|
+
@editor.insert_text("\n")
|
|
247
|
+
when "O"
|
|
248
|
+
row = row_at(cursor_position)
|
|
249
|
+
indent = @editor.buffer.line(row)[/\A[ \t]*/]
|
|
250
|
+
first = line_start(row)
|
|
251
|
+
@editor.select(first)
|
|
252
|
+
@editor.replace_selections(indent + @editor.buffer.line_ending, kind: :vim_insert)
|
|
253
|
+
@editor.select(first + indent.bytesize)
|
|
254
|
+
end
|
|
255
|
+
@mode = key == "R" ? :replace : :insert
|
|
256
|
+
@command.clear
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def prefix_key(key)
|
|
260
|
+
prefix, @prefix = @prefix, nil
|
|
261
|
+
count = @prefix_count || 1
|
|
262
|
+
case prefix
|
|
263
|
+
when "g"
|
|
264
|
+
if %w[u U ~].include?(key)
|
|
265
|
+
return operate_visual("g#{key}") if visual?
|
|
266
|
+
@operator, @operator_count, @operator_explicit = "g#{key}", count, @prefix_explicit
|
|
267
|
+
return
|
|
268
|
+
elsif key == "g"
|
|
269
|
+
move("gg", count, explicit: true)
|
|
270
|
+
elsif %w[e E].include?(key)
|
|
271
|
+
move("g#{key}", count)
|
|
272
|
+
elsif key == "v" && @last_visual
|
|
273
|
+
@mode, @visual_anchor, @visual_head = @last_visual
|
|
274
|
+
@visual_anchor, @visual_head = normal_offset(@visual_anchor), normal_offset(@visual_head)
|
|
275
|
+
@visual_keys = ["g", "v"]
|
|
276
|
+
update_visual
|
|
277
|
+
end
|
|
278
|
+
when "m"
|
|
279
|
+
@editor.buffer.release_anchor(@marks[key]) if @marks[key]
|
|
280
|
+
@marks[key] = @editor.buffer.anchor(cursor_position, bias: :left) if key.match?(/\A[a-zA-Z]\z/)
|
|
281
|
+
when "'", "`"
|
|
282
|
+
if @marks[key]
|
|
283
|
+
target = @editor.buffer.resolve(@marks[key])
|
|
284
|
+
target = first_nonblank(row_at(target)) if prefix == "'"
|
|
285
|
+
return operate_motion(target, inclusive: false, linewise: prefix == "'") if @operator
|
|
286
|
+
move_to(target)
|
|
287
|
+
else
|
|
288
|
+
@operator = nil
|
|
289
|
+
end
|
|
290
|
+
when '"'
|
|
291
|
+
@register = key
|
|
292
|
+
return
|
|
293
|
+
when "q"
|
|
294
|
+
if key.match?(/\A[a-zA-Z]\z/)
|
|
295
|
+
@recording = key.downcase
|
|
296
|
+
@macros[@recording] = [] unless key.match?(/[A-Z]/) && @macros[@recording]
|
|
297
|
+
end
|
|
298
|
+
when "@"
|
|
299
|
+
name = key == "@" ? @last_macro : key.downcase
|
|
300
|
+
@last_macro = name
|
|
301
|
+
@command.clear
|
|
302
|
+
keys = @macros[name] || register_keys(register_entry(name)&.first.to_s)
|
|
303
|
+
replay(keys, group: true, count: count)
|
|
304
|
+
when "f", "F", "t", "T"
|
|
305
|
+
target = find_character(key, prefix, count)
|
|
306
|
+
if target
|
|
307
|
+
@last_find = [key, prefix]
|
|
308
|
+
return operate_motion(target, inclusive: %w[f t].include?(prefix)) if @operator
|
|
309
|
+
move_to(target)
|
|
310
|
+
else
|
|
311
|
+
@operator = nil
|
|
312
|
+
end
|
|
313
|
+
when "r"
|
|
314
|
+
return replace_visual(key) if visual?
|
|
315
|
+
if key.grapheme_clusters.length == 1 || key == "enter"
|
|
316
|
+
first = cursor_position
|
|
317
|
+
ending = horizontal(first, count, insertion: true)
|
|
318
|
+
if @editor.buffer.rope.byteslice(first...ending).to_s.grapheme_clusters.length == count
|
|
319
|
+
@editor.select(first, ending)
|
|
320
|
+
@editor.replace_selections(key == "enter" ? @editor.buffer.line_ending : key * count, kind: :vim)
|
|
321
|
+
@editor.select(normal_offset(key == "enter" ? first + @editor.buffer.line_ending.bytesize : previous_offset(first + key.bytesize * count)))
|
|
322
|
+
finish_change
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
when "i", "a"
|
|
326
|
+
range = text_object(key, inner: prefix == "i", count: count)
|
|
327
|
+
if range
|
|
328
|
+
if visual?
|
|
329
|
+
@visual_anchor, @visual_head = range.begin, previous_offset(range.end)
|
|
330
|
+
update_visual
|
|
331
|
+
else
|
|
332
|
+
operate(@operator, range, linewise: key == "p", destination: @operator == "y" ? range.begin : nil)
|
|
333
|
+
end
|
|
334
|
+
else
|
|
335
|
+
@operator = nil
|
|
336
|
+
end
|
|
337
|
+
return
|
|
338
|
+
when "operator_g"
|
|
339
|
+
if %w[g e E].include?(key)
|
|
340
|
+
return operate_key("g#{key}", count, @prefix_explicit, multiplied: true)
|
|
341
|
+
end
|
|
342
|
+
@operator = nil
|
|
343
|
+
end
|
|
344
|
+
@command.clear
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def operate_key(key, count, explicit = false, multiplied: false)
|
|
348
|
+
count *= @operator_count unless multiplied
|
|
349
|
+
if key == @operator || (%w[gu gU g~].include?(@operator) && key == @operator[-1])
|
|
350
|
+
operate(@operator, line_range(count), linewise: true)
|
|
351
|
+
elsif %w[i a f F t T ' `].include?(key)
|
|
352
|
+
@prefix, @prefix_count = key, count
|
|
353
|
+
elsif key == "g"
|
|
354
|
+
@prefix, @prefix_count, @prefix_explicit = "operator_g", count, explicit || @operator_explicit
|
|
355
|
+
elsif %w[/ ?].include?(key)
|
|
356
|
+
@command_line, @command_type, @search_count = +"", key, count
|
|
357
|
+
else
|
|
358
|
+
effective = @operator == "c" && %w[w W].include?(key) && !character_at(cursor_position).match?(/\s/) ? (key == "w" ? "ce" : "cE") : key
|
|
359
|
+
motion = motion_target(effective, count, explicit: explicit || @operator_explicit)
|
|
360
|
+
if motion
|
|
361
|
+
operate_motion(*motion)
|
|
362
|
+
else
|
|
363
|
+
@operator = nil
|
|
364
|
+
@command.clear
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def operate_motion(target, options = {}, inclusive: options.fetch(:inclusive, false), linewise: options.fetch(:linewise, false))
|
|
370
|
+
first, last = [cursor_position, target].minmax
|
|
371
|
+
if linewise
|
|
372
|
+
ending_row = row_at(last)
|
|
373
|
+
ending = ending_row + 1 < @editor.buffer.line_count ? line_start(ending_row + 1) : @editor.buffer.rope.bytesize
|
|
374
|
+
destination = first if %w[y gu gU g~].include?(@operator)
|
|
375
|
+
return operate(@operator, line_start(row_at(first))...ending, linewise: true, destination: destination)
|
|
376
|
+
end
|
|
377
|
+
if inclusive
|
|
378
|
+
last = next_offset(last)
|
|
379
|
+
elsif first == last && %w[gu gU g~].include?(@operator)
|
|
380
|
+
last = next_offset(last)
|
|
381
|
+
elsif !linewise && target > cursor_position && row_at(target) > row_at(cursor_position) && target == line_start(row_at(target))
|
|
382
|
+
if cursor_position <= first_nonblank(row_at(cursor_position))
|
|
383
|
+
linewise = true
|
|
384
|
+
else
|
|
385
|
+
last = line_end(row_at(target) - 1)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
operate(@operator, first...last, linewise: linewise)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def finish_change
|
|
392
|
+
@last_change = @command.dup unless @command.empty?
|
|
393
|
+
@command.clear
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def replay(keys, group: false, count: 1)
|
|
397
|
+
raise Error, "macro recursion limit" if @play_depth >= 10
|
|
398
|
+
@replay_remaining = 10_000 if @play_depth.zero?
|
|
399
|
+
@play_depth += 1
|
|
400
|
+
@editor.buffer.begin_undo_group if group
|
|
401
|
+
@macro_groups = (@macro_groups || 0) + 1 if group
|
|
402
|
+
begin
|
|
403
|
+
count.times do
|
|
404
|
+
keys.each do |key|
|
|
405
|
+
raise Error, "macro execution limit" if @replay_remaining.zero?
|
|
406
|
+
@replay_remaining -= 1
|
|
407
|
+
feed(key)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
ensure
|
|
411
|
+
@editor.buffer.end_undo_group if group
|
|
412
|
+
@macro_groups -= 1 if group
|
|
413
|
+
@play_depth -= 1
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
require_relative "vim/motionable"
|
|
420
|
+
require_relative "vim/text_object_selectable"
|
|
421
|
+
require_relative "vim/operator_capable"
|
|
422
|
+
require_relative "vim/commandable"
|
|
423
|
+
Canopus::Vim.include Canopus::Vim::Motionable
|
|
424
|
+
Canopus::Vim.include Canopus::Vim::TextObjectSelectable
|
|
425
|
+
Canopus::Vim.include Canopus::Vim::OperatorCapable
|
|
426
|
+
Canopus::Vim.include Canopus::Vim::Commandable
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
module Workspace::Edit
|
|
5
|
+
module Executable
|
|
6
|
+
def apply
|
|
7
|
+
return {"applied" => false, "failureReason" => "workspace edit plan has already been used"} if @used
|
|
8
|
+
@used = true
|
|
9
|
+
verify_unchanged
|
|
10
|
+
@operations.each do |operation|
|
|
11
|
+
@failed_change = operation[:index]
|
|
12
|
+
execute_resource(operation)
|
|
13
|
+
end
|
|
14
|
+
# All text edits were checked against the ordered virtual filesystem. They
|
|
15
|
+
# remain unsaved buffer edits; no temporary draft is written into a file.
|
|
16
|
+
@memory_started = true
|
|
17
|
+
@failed_change = nil
|
|
18
|
+
update_buffers
|
|
19
|
+
@workspace.refresh_files
|
|
20
|
+
result = {"applied" => true}
|
|
21
|
+
result["recoveryPaths"] = @recoveries.dup unless @recoveries.empty?
|
|
22
|
+
announce_recovery
|
|
23
|
+
result
|
|
24
|
+
rescue StandardError => error
|
|
25
|
+
failures = @memory_started ? [] : rollback
|
|
26
|
+
announce_recovery
|
|
27
|
+
result = {"applied" => false, "failureReason" => ([error.message] + failures).join("; "),
|
|
28
|
+
"recoveryPaths" => @recoveries.select { |path| File.exist?(path) || File.symlink?(path) }}
|
|
29
|
+
result["failureReason"] += "; recoverable files: #{File.join(@root, '.canopus', 'trash')}" unless result["recoveryPaths"].empty?
|
|
30
|
+
result["failedChange"] = @failed_change if @failed_change
|
|
31
|
+
result
|
|
32
|
+
ensure
|
|
33
|
+
close
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def verify_unchanged
|
|
39
|
+
raise Error, "resource changed while preparing workspace edit" unless @observed.all? { |path, value| fingerprint(path) == value }
|
|
40
|
+
unless @originals.all? { |buffer, (rope, version, path)| buffer.rope.equal?(rope) && buffer.version == version && buffer.path == path }
|
|
41
|
+
raise Error, "document changed while preparing workspace edit"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def recovery_path(path)
|
|
46
|
+
@workspace.project_path(".canopus/trash")
|
|
47
|
+
directory = File.join(@root, ".canopus", "trash")
|
|
48
|
+
FileUtils.mkdir_p(directory)
|
|
49
|
+
name = File.basename(path).byteslice(0, 160).scrub
|
|
50
|
+
File.join(directory, "lsp-#{Time.now.utc.strftime('%Y%m%dT%H%M%S')}-#{SecureRandom.hex(8)}-#{name}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def backup(path)
|
|
54
|
+
destination = recovery_path(path)
|
|
55
|
+
File.rename(path, destination)
|
|
56
|
+
@recoveries << destination
|
|
57
|
+
@journal << [:restore, destination, path]
|
|
58
|
+
destination
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def execute_resource(operation)
|
|
62
|
+
path = operation.fetch(:path)
|
|
63
|
+
[path, operation[:source]].compact.each do |target|
|
|
64
|
+
parent = File.realpath(File.dirname(target))
|
|
65
|
+
raise Error, "resource parent moved outside project" unless parent == @root || parent.start_with?(@root + File::SEPARATOR)
|
|
66
|
+
end
|
|
67
|
+
backup(path) if operation[:backup]
|
|
68
|
+
case operation[:kind]
|
|
69
|
+
when :create
|
|
70
|
+
operation[:directory] ? Dir.mkdir(path) : File.open(path, File::CREAT | File::EXCL | File::WRONLY, 0o644, &:close)
|
|
71
|
+
@journal << [:created, path]
|
|
72
|
+
when :rename
|
|
73
|
+
raise Error, "rename destination appeared during workspace edit" if File.exist?(path) || File.symlink?(path)
|
|
74
|
+
File.rename(operation.fetch(:source), path)
|
|
75
|
+
@journal << [:restore, path, operation.fetch(:source)]
|
|
76
|
+
when :delete then backup(path)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rollback
|
|
81
|
+
failures = []
|
|
82
|
+
@journal.reverse_each do |kind, source, target|
|
|
83
|
+
next unless File.exist?(source) || File.symlink?(source)
|
|
84
|
+
if kind == :created
|
|
85
|
+
target = recovery_path(source)
|
|
86
|
+
@recoveries << target
|
|
87
|
+
elsif File.exist?(target) || File.symlink?(target)
|
|
88
|
+
raise Error, "rollback destination now exists: #{target}"
|
|
89
|
+
end
|
|
90
|
+
File.rename(source, target)
|
|
91
|
+
rescue StandardError => error
|
|
92
|
+
failures << "recovery required: #{error.message}"
|
|
93
|
+
end
|
|
94
|
+
failures
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update_buffers
|
|
98
|
+
deleted = @operations.select { |operation| operation[:kind] == :delete }.flat_map { |operation| operation[:nodes] }.filter_map(&:buffer)
|
|
99
|
+
deleted.uniq.each do |buffer|
|
|
100
|
+
@workspace.panes.flat_map(&:editors).select { |editor| editor.buffer.equal?(buffer) }.each { |editor| @workspace.close_editor(editor, discard: true) }
|
|
101
|
+
@workspace.close_language_documents(buffer)
|
|
102
|
+
@workspace.buffers.delete_if { |_, value| value.equal?(buffer) }
|
|
103
|
+
buffer.close
|
|
104
|
+
end
|
|
105
|
+
@nodes.values.compact.uniq.each do |node|
|
|
106
|
+
buffer = node.buffer
|
|
107
|
+
next unless buffer
|
|
108
|
+
if buffer.path != node.path
|
|
109
|
+
@workspace.invalidate_git
|
|
110
|
+
@workspace.close_language_documents(buffer)
|
|
111
|
+
@workspace.buffers.delete_if { |_, value| value.equal?(buffer) }
|
|
112
|
+
buffer.relocate(node.path)
|
|
113
|
+
@workspace.panes.flat_map(&:editors).each do |editor|
|
|
114
|
+
next unless editor.buffer.equal?(buffer)
|
|
115
|
+
editor.language = @workspace.send(:definition_for, node.path)
|
|
116
|
+
@workspace.send(:apply_editor_settings, editor)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
@workspace.buffers[node.path] = buffer
|
|
120
|
+
buffer.reload if node.reset || (node.origin.nil? && node.exists)
|
|
121
|
+
buffer.begin_undo_group
|
|
122
|
+
begin
|
|
123
|
+
node.edits.each do |changes, index|
|
|
124
|
+
@failed_change = index
|
|
125
|
+
buffer.edit(changes, kind: :lsp)
|
|
126
|
+
end
|
|
127
|
+
ensure
|
|
128
|
+
buffer.end_undo_group
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def announce_recovery
|
|
134
|
+
paths = @recoveries.select { |path| File.exist?(path) || File.symlink?(path) }
|
|
135
|
+
@workspace.message = "Language server resource backups: #{paths.join(', ')}" unless paths.empty?
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|