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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Canopus
|
|
6
|
+
module Editor::SnippetExpandable
|
|
7
|
+
SnippetField = Struct.new(:first, :last, :range, :parents, :index, :order)
|
|
8
|
+
private_constant :SnippetField
|
|
9
|
+
|
|
10
|
+
def snippet_variables(workspace_root: nil, clipboard: nil, now: Time.now, selection: primary, cursor_index: @selections.index(selection) || 0)
|
|
11
|
+
point = @buffer.rope.point_at(selection.start)
|
|
12
|
+
line = @buffer.line(point.row).delete_suffix("\n").delete_suffix("\r")
|
|
13
|
+
column = @buffer.rope.byteslice(@buffer.rope.line_start(point.row), selection.start - @buffer.rope.line_start(point.row)).to_s.length
|
|
14
|
+
word = line[0...column][/\w*\z/] + line[column..][/\A\w*/]
|
|
15
|
+
path = @buffer.path
|
|
16
|
+
root = workspace_root && File.expand_path(workspace_root)
|
|
17
|
+
comments = case language_document.definition.name
|
|
18
|
+
when "ruby" then ["=begin", "=end"]
|
|
19
|
+
when "html", "markdown" then ["<!--", "-->"]
|
|
20
|
+
when "javascript", "typescript", "rust", "go", "c", "cpp", "json", "css" then ["/*", "*/"]
|
|
21
|
+
else ["", ""]
|
|
22
|
+
end
|
|
23
|
+
variables = {
|
|
24
|
+
"TM_SELECTED_TEXT" => @buffer.rope.byteslice(selection.range).to_s,
|
|
25
|
+
"TM_CURRENT_LINE" => line, "TM_CURRENT_WORD" => word,
|
|
26
|
+
"TM_LINE_INDEX" => point.row.to_s, "TM_LINE_NUMBER" => (point.row + 1).to_s,
|
|
27
|
+
"TM_FILENAME" => path && File.basename(path), "TM_FILENAME_BASE" => path && File.basename(path, File.extname(path)),
|
|
28
|
+
"TM_DIRECTORY" => path && File.dirname(path), "TM_FILEPATH" => path,
|
|
29
|
+
"RELATIVE_FILEPATH" => path && (root ? snippet_relative_path(path, root) : File.basename(path)),
|
|
30
|
+
"CLIPBOARD" => clipboard, "WORKSPACE_NAME" => root && File.basename(root), "WORKSPACE_FOLDER" => root,
|
|
31
|
+
"CURSOR_INDEX" => cursor_index.to_s, "CURSOR_NUMBER" => (cursor_index + 1).to_s,
|
|
32
|
+
"CURRENT_MILLISECOND" => (now.nsec / 1_000_000).to_s.rjust(3, "0"),
|
|
33
|
+
"CURRENT_SECONDS_UNIX" => now.to_i.to_s, "CURRENT_MILLISECONDS_UNIX" => (now.to_r * 1000).floor.to_s,
|
|
34
|
+
"CURRENT_TIMEZONE_OFFSET" => now.strftime("%:z"),
|
|
35
|
+
"CURRENT_TIMEZONE_NAME" => ENV["TZ"]&.match?(/\A(?:UTC|[A-Za-z_]+\/[A-Za-z_\/+-]+)\z/) ? ENV["TZ"] : nil,
|
|
36
|
+
"RANDOM" => SecureRandom.random_number(1_000_000).to_s.rjust(6, "0"), "RANDOM_HEX" => SecureRandom.hex(3), "UUID" => SecureRandom.uuid,
|
|
37
|
+
"BLOCK_COMMENT_START" => comments[0], "BLOCK_COMMENT_END" => comments[1],
|
|
38
|
+
"LINE_COMMENT" => language_document.definition.comment == comments[0] ? "" : language_document.definition.comment
|
|
39
|
+
}
|
|
40
|
+
{"YEAR" => "%Y", "YEAR_SHORT" => "%y", "MONTH" => "%m", "MONTH_NAME" => "%B", "MONTH_NAME_SHORT" => "%b",
|
|
41
|
+
"DATE" => "%d", "DAY_NAME" => "%A", "DAY_NAME_SHORT" => "%a", "HOUR" => "%H", "MINUTE" => "%M", "SECOND" => "%S"}.each do |name, format|
|
|
42
|
+
variables["CURRENT_#{name}"] = now.strftime(format)
|
|
43
|
+
end
|
|
44
|
+
variables
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def insert_snippet(source, variables: {}, workspace_root: nil, clipboard: nil)
|
|
48
|
+
raise Error, "snippet variables must be a Hash" unless variables.is_a?(Hash)
|
|
49
|
+
# Parse every cursor expansion before changing text or disposing a live session.
|
|
50
|
+
snippets = @selections.each_with_index.map do |selection, index|
|
|
51
|
+
builtins = snippet_variables(workspace_root: workspace_root, clipboard: clipboard, selection: selection, cursor_index: index)
|
|
52
|
+
Snippet.new(source, variables: builtins.merge(variables.transform_keys(&:to_s)))
|
|
53
|
+
end
|
|
54
|
+
starts, delta = [], 0
|
|
55
|
+
@selections.zip(snippets).each do |selection, snippet|
|
|
56
|
+
starts << selection.start + delta
|
|
57
|
+
delta += snippet.text.bytesize - selection.range.size
|
|
58
|
+
end
|
|
59
|
+
replace_selections(snippets.map(&:text), kind: :snippet)
|
|
60
|
+
clear_snippet
|
|
61
|
+
@snippet_instances = snippets.zip(starts).map do |snippet, start|
|
|
62
|
+
fields, mirrors = {}, []
|
|
63
|
+
snippet.occurrences.each do |occurrence|
|
|
64
|
+
field = snippet_field(start + occurrence.range.begin...start + occurrence.range.end, occurrence.parents, occurrence.index)
|
|
65
|
+
if occurrence.transform
|
|
66
|
+
mirrors << {index: occurrence.index, field: field, transform: occurrence.transform}
|
|
67
|
+
else
|
|
68
|
+
(fields[occurrence.index] ||= []) << field
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
mirrors.each { |mirror| fields[mirror[:index]] ||= [mirror[:field]] }
|
|
72
|
+
fields[0] ||= [snippet_field(start + snippet.text.bytesize...start + snippet.text.bytesize)]
|
|
73
|
+
{snippet: snippet, fields: fields, mirrors: mirrors}
|
|
74
|
+
end
|
|
75
|
+
@snippet_order = @snippet_instances.flat_map { |instance| instance[:fields].keys }.uniq.sort_by { |index| index.zero? ? Float::INFINITY : index }
|
|
76
|
+
@snippet_position = -1
|
|
77
|
+
@snippet_subscription = @buffer.on_edit { |patch| snippet_edited(patch) }
|
|
78
|
+
next_snippet
|
|
79
|
+
snippets.last
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def snippet_active? = !@snippet_instances.nil?
|
|
83
|
+
|
|
84
|
+
def snippet_choices
|
|
85
|
+
return nil unless snippet_active?
|
|
86
|
+
@snippet_instances.lazy.map { |instance| instance[:snippet].choices[@snippet_order[@snippet_position]] }.find(&:itself)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def choose_snippet(value)
|
|
90
|
+
choices = snippet_choices
|
|
91
|
+
return false unless choices
|
|
92
|
+
value = choices[value] if value.is_a?(Integer) && value >= 0
|
|
93
|
+
return false unless choices.include?(value)
|
|
94
|
+
snippet_select(@snippet_order[@snippet_position])
|
|
95
|
+
replace_selections(value, kind: :snippet_choice)
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def next_snippet = snippet_move(1)
|
|
100
|
+
def previous_snippet = snippet_move(-1)
|
|
101
|
+
|
|
102
|
+
def clear_snippet
|
|
103
|
+
active = @snippet_instances
|
|
104
|
+
@snippet_subscription&.detach
|
|
105
|
+
@snippet_subscription = nil
|
|
106
|
+
snippet_fields.each { |field| snippet_release(field) } if @snippet_instances
|
|
107
|
+
@snippet_instances = @snippet_order = @snippet_position = nil
|
|
108
|
+
set_selections(@selections) if active
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def snippet_relative_path(path, root)
|
|
115
|
+
file_parts, root_parts = path.split(File::SEPARATOR), root.split(File::SEPARATOR)
|
|
116
|
+
return path if File::ALT_SEPARATOR && file_parts.first != root_parts.first
|
|
117
|
+
while !file_parts.empty? && file_parts.first == root_parts.first
|
|
118
|
+
file_parts.shift
|
|
119
|
+
root_parts.shift
|
|
120
|
+
end
|
|
121
|
+
([".."] * root_parts.length + file_parts).join(File::SEPARATOR)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def snippet_field(range, parents = [], index = 0)
|
|
125
|
+
@snippet_field_order = (@snippet_field_order || 0) + 1
|
|
126
|
+
SnippetField.new(@buffer.anchor(range.begin, bias: :right), @buffer.anchor(range.end, bias: range.size.zero? ? :right : :left), range, parents, index, @snippet_field_order)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def snippet_fields
|
|
130
|
+
@snippet_instances.flat_map { |instance| instance[:fields].values.flatten + instance[:mirrors].map { |mirror| mirror[:field] } }.uniq(&:object_id)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def snippet_release(field)
|
|
134
|
+
@buffer.release_anchor(field.first)
|
|
135
|
+
@buffer.release_anchor(field.last)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def snippet_range(field) = @buffer.resolve(field.first)...@buffer.resolve(field.last)
|
|
139
|
+
|
|
140
|
+
def snippet_move(direction)
|
|
141
|
+
return false unless snippet_active?
|
|
142
|
+
position = @snippet_position + direction
|
|
143
|
+
return false if position < 0 || position >= @snippet_order.length
|
|
144
|
+
snippet_transform if @snippet_position >= 0
|
|
145
|
+
while position >= 0 && position < @snippet_order.length
|
|
146
|
+
index = @snippet_order[position]
|
|
147
|
+
if @snippet_instances.any? { |instance| instance[:fields][index]&.any? }
|
|
148
|
+
@snippet_position = position
|
|
149
|
+
snippet_select(index)
|
|
150
|
+
clear_snippet if index.zero?
|
|
151
|
+
reveal_cursor
|
|
152
|
+
return true
|
|
153
|
+
end
|
|
154
|
+
position += direction
|
|
155
|
+
end
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def snippet_select(index)
|
|
160
|
+
active = @snippet_instances.flat_map { |instance| instance[:fields][index] || [] }
|
|
161
|
+
active_ids = active.to_h { |field| [field.object_id, true] }
|
|
162
|
+
ancestors = active.flat_map(&:parents).to_h { |number| [number, true] }
|
|
163
|
+
# Only the active placeholders and their enclosing placeholders grow at
|
|
164
|
+
# both edges. Adjacent/empty future fields must move past newly typed text.
|
|
165
|
+
snippet_fields.each do |field|
|
|
166
|
+
range = snippet_range(field)
|
|
167
|
+
enclosing = active_ids[field.object_id] || (ancestors[field.index] && active.any? { |other| other.parents.include?(field.index) && range.begin <= other.range.begin && range.end >= other.range.end })
|
|
168
|
+
snippet_release(field)
|
|
169
|
+
field.first = @buffer.anchor(range.begin, bias: enclosing ? :left : :right)
|
|
170
|
+
field.last = @buffer.anchor(range.end, bias: enclosing || range.size.zero? ? :right : :left)
|
|
171
|
+
field.range = range
|
|
172
|
+
end
|
|
173
|
+
set_selections(active.map do |field|
|
|
174
|
+
range = snippet_range(field)
|
|
175
|
+
selection = Selection.new(@next_selection, range.begin, range.end, nil)
|
|
176
|
+
@next_selection += 1
|
|
177
|
+
selection
|
|
178
|
+
end)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def snippet_transform
|
|
182
|
+
index = @snippet_order[@snippet_position]
|
|
183
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.25
|
|
184
|
+
replacements = @snippet_instances.flat_map do |instance|
|
|
185
|
+
source = instance[:fields][index]&.first
|
|
186
|
+
next [] unless source
|
|
187
|
+
value = @buffer.rope.byteslice(snippet_range(source)).to_s
|
|
188
|
+
instance[:mirrors].filter_map do |mirror|
|
|
189
|
+
next unless mirror[:index] == index
|
|
190
|
+
range = snippet_range(mirror[:field])
|
|
191
|
+
replacement = mirror[:transform].apply(value)
|
|
192
|
+
raise Error, "snippet transforms exceeded execution limit" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
193
|
+
[range, replacement, mirror[:field]] unless @buffer.rope.byteslice(range).to_s == replacement
|
|
194
|
+
end
|
|
195
|
+
end.sort_by { |range, _text, _field| range.begin }
|
|
196
|
+
return if replacements.empty?
|
|
197
|
+
@snippet_applying = true
|
|
198
|
+
@snippet_replacements = replacements
|
|
199
|
+
@buffer.edit(replacements.map { |range, text, _field| [range, text] }, kind: :snippet_transform)
|
|
200
|
+
delta = 0
|
|
201
|
+
replacements.each do |range, text, field|
|
|
202
|
+
start = range.begin + delta
|
|
203
|
+
snippet_reanchor(field, start...start + text.bytesize)
|
|
204
|
+
delta += text.bytesize - range.size
|
|
205
|
+
end
|
|
206
|
+
ensure
|
|
207
|
+
@snippet_applying = false
|
|
208
|
+
@snippet_replacements = nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def snippet_reanchor(field, range, first_bias: :left, last_bias: :right)
|
|
212
|
+
snippet_release(field)
|
|
213
|
+
field.first = @buffer.anchor(range.begin, bias: first_bias)
|
|
214
|
+
field.last = @buffer.anchor(range.end, bias: last_bias)
|
|
215
|
+
field.range = range
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def snippet_map_offset(patch, offset, bias)
|
|
219
|
+
delta = 0
|
|
220
|
+
patch.edits.each do |edit|
|
|
221
|
+
first, last = edit.old_range.begin, edit.old_range.end
|
|
222
|
+
break if offset < first || (offset == first && bias == :left)
|
|
223
|
+
if offset >= last
|
|
224
|
+
delta += edit.new_text.bytesize - edit.old_range.size
|
|
225
|
+
else
|
|
226
|
+
return bias == :left ? edit.new_range.begin : edit.new_range.end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
offset + delta
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def snippet_edited(patch)
|
|
233
|
+
if patch.is_a?(Patch::Composite) || patch.is_a?(Patch::Reload)
|
|
234
|
+
clear_snippet
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
unless @snippet_applying
|
|
238
|
+
index = @snippet_order[@snippet_position]
|
|
239
|
+
edits = patch.edits.dup
|
|
240
|
+
active_fields = @snippet_instances.flat_map { |instance| instance[:fields][index] || [] }
|
|
241
|
+
@snippet_instances.each do |instance|
|
|
242
|
+
active = (instance[:fields][index] || []).map(&:range)
|
|
243
|
+
removed = []
|
|
244
|
+
discard = lambda do |field|
|
|
245
|
+
range = field.range
|
|
246
|
+
field.parents.include?(index) && active.any? { |outer| outer.begin <= range.begin && outer.end >= range.end } &&
|
|
247
|
+
patch.edits.any? { |edit| edit.old_range.size.positive? && edit.old_range.begin <= range.begin && edit.old_range.end >= range.end }
|
|
248
|
+
end
|
|
249
|
+
instance[:fields].each do |number, fields|
|
|
250
|
+
next if number == index || number.zero?
|
|
251
|
+
fields.delete_if { |field| discard.call(field) && (removed << field) }
|
|
252
|
+
end
|
|
253
|
+
instance[:mirrors].delete_if { |mirror| mirror[:index] != index && discard.call(mirror[:field]) && (removed << mirror[:field]) }
|
|
254
|
+
removed.uniq(&:object_id).each { |field| snippet_release(field) }
|
|
255
|
+
end
|
|
256
|
+
# Multiple empty mirrors occupy one byte offset, but each insertion has
|
|
257
|
+
# its own new range. A generic point anchor cannot distinguish them.
|
|
258
|
+
precise = active_fields.sort_by { |field| field.range.begin }.filter_map do |field|
|
|
259
|
+
matching = edits.index { |edit| edit.old_range == field.range }
|
|
260
|
+
[field, edits.delete_at(matching).new_range] if matching
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
snippet_fields.each do |field|
|
|
264
|
+
range = field.range
|
|
265
|
+
first_bias, last_bias = field.first.bias, field.last.bias
|
|
266
|
+
if @snippet_applying
|
|
267
|
+
enclosing = @snippet_replacements.any? { |old, _text, changed| changed.parents.include?(field.index) && range.begin <= old.begin && range.end >= old.end }
|
|
268
|
+
first_bias, last_bias = enclosing ? [:left, :right] : [:right, :left]
|
|
269
|
+
if range.size.zero? && !enclosing
|
|
270
|
+
following = @snippet_replacements.any? { |old, _text, changed| old.begin == range.begin && changed.order > field.order }
|
|
271
|
+
first_bias = last_bias = following ? :left : :right
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
first = snippet_map_offset(patch, range.begin, first_bias)
|
|
275
|
+
last = snippet_map_offset(patch, range.end, last_bias)
|
|
276
|
+
snippet_reanchor(field, [first, last].min...last, first_bias: field.first.bias, last_bias: field.last.bias)
|
|
277
|
+
end
|
|
278
|
+
precise&.each { |field, range| snippet_reanchor(field, range) }
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|