canopus 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "zaniah/ui"
|
|
5
|
+
|
|
6
|
+
module Canopus
|
|
7
|
+
module Workspace::HierarchyAware
|
|
8
|
+
HIERARCHY_ITEM_LIMIT = 1_000
|
|
9
|
+
HIERARCHY_TOTAL_LIMIT = 10_000
|
|
10
|
+
HIERARCHY_DEPTH_LIMIT = 32
|
|
11
|
+
HIERARCHY_REQUEST_LIMIT = 64
|
|
12
|
+
HIERARCHY_TEXT_LIMIT = 4_096
|
|
13
|
+
HIERARCHY_URI_LIMIT = 16_384
|
|
14
|
+
HIERARCHY_ITEM_BYTES_LIMIT = 1 << 20
|
|
15
|
+
|
|
16
|
+
def hierarchy_tree
|
|
17
|
+
@hierarchy_tree ||= Zaniah::UI::TreeView.new([], height: 320).on_select do |value, _event, _context|
|
|
18
|
+
select_hierarchy_item(value)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show_call_hierarchy(current = editor) = show_hierarchy(:call, current)
|
|
23
|
+
def show_type_hierarchy(current = editor) = show_hierarchy(:type, current)
|
|
24
|
+
|
|
25
|
+
def show_hierarchy(kind, current = editor)
|
|
26
|
+
raise ArgumentError, "invalid hierarchy kind" unless %i[call type].include?(kind)
|
|
27
|
+
|
|
28
|
+
self.palette = nil
|
|
29
|
+
invalidate_hierarchy
|
|
30
|
+
unless current&.buffer&.path
|
|
31
|
+
@message = "Save the document before requesting a hierarchy"
|
|
32
|
+
return false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
snapshot = hierarchy_snapshot(kind, current)
|
|
36
|
+
if snapshot[:client] && !snapshot[:capability]
|
|
37
|
+
release_hierarchy_snapshot(snapshot)
|
|
38
|
+
@message = "#{hierarchy_title(kind)} is not supported by this language server"
|
|
39
|
+
return false
|
|
40
|
+
end
|
|
41
|
+
requests = @hierarchy_prepare_requests ||= {}
|
|
42
|
+
id = @hierarchy_prepare_request_id = @hierarchy_prepare_request_id.to_i + 1
|
|
43
|
+
requests[id] = snapshot
|
|
44
|
+
start_hierarchy_prepare(id, snapshot)
|
|
45
|
+
@message = "Preparing #{hierarchy_title(kind).downcase}…"
|
|
46
|
+
nil
|
|
47
|
+
rescue StandardError => error
|
|
48
|
+
@hierarchy_prepare_requests&.delete_if { |_id, request| request.equal?(snapshot) }
|
|
49
|
+
release_hierarchy_snapshot(snapshot) if snapshot
|
|
50
|
+
@message = error.message
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def accept_hierarchy_root(palette, index)
|
|
55
|
+
state = palette[:hierarchy]
|
|
56
|
+
item = index && palette[:items]&.[](index)
|
|
57
|
+
unless item && hierarchy_context_valid?(state)
|
|
58
|
+
@message = "Hierarchy context changed; request again"
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
install_hierarchy(state, item)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def invalidate_hierarchy(buffer = nil, client: nil, editor: nil)
|
|
66
|
+
invalidated = false
|
|
67
|
+
@hierarchy_prepare_requests&.delete_if do |_id, snapshot|
|
|
68
|
+
matches = (!buffer || snapshot[:buffer].equal?(buffer)) &&
|
|
69
|
+
(!client || snapshot[:client]&.equal?(client)) && (!editor || snapshot[:editor].equal?(editor))
|
|
70
|
+
if matches
|
|
71
|
+
snapshot[:future]&.cancel
|
|
72
|
+
release_hierarchy_snapshot(snapshot)
|
|
73
|
+
invalidated = true
|
|
74
|
+
end
|
|
75
|
+
matches
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
candidate = @palette&.dig(:kind) == :hierarchy_roots && @palette[:hierarchy]
|
|
79
|
+
candidate_matches = candidate && (!buffer || candidate[:buffer].equal?(buffer)) &&
|
|
80
|
+
(!client || candidate[:client]&.equal?(client)) && (!editor || candidate[:editor].equal?(editor))
|
|
81
|
+
self.palette = nil if candidate_matches
|
|
82
|
+
|
|
83
|
+
state = @hierarchy_state
|
|
84
|
+
clear_state = state && (!buffer || state[:buffer].equal?(buffer)) &&
|
|
85
|
+
(!client || state[:client].equal?(client)) && (!editor || state[:editor].equal?(editor))
|
|
86
|
+
if clear_state
|
|
87
|
+
cancel_hierarchy_children(state)
|
|
88
|
+
@hierarchy_state = nil
|
|
89
|
+
hierarchy_tree.replace([])
|
|
90
|
+
@panels.hide("hierarchy") if @panels.key?("hierarchy")
|
|
91
|
+
invalidated = true
|
|
92
|
+
end
|
|
93
|
+
@hierarchy_generation = @hierarchy_generation.to_i + 1 if invalidated || candidate_matches
|
|
94
|
+
@window&.request_frame unless @closed
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def hierarchy_snapshot(kind, current)
|
|
101
|
+
buffer, offset = current.buffer, current.primary.head
|
|
102
|
+
language = current.language_document.definition.name
|
|
103
|
+
client = active_language_client(language, kind == :call ? "callHierarchy" : "typeHierarchy")
|
|
104
|
+
generation = @hierarchy_generation = @hierarchy_generation.to_i + 1
|
|
105
|
+
snapshot = {kind: kind, generation: generation, editor: current,
|
|
106
|
+
document: current.language_document, buffer: buffer, version: buffer.version,
|
|
107
|
+
selections: current.selections, offset: offset, rope: buffer.rope,
|
|
108
|
+
uri: Sadr::Protocol.uri(buffer.path), position: Sadr::Protocol.position(buffer.rope, offset),
|
|
109
|
+
language: language, client: client, capability: hierarchy_capability(client, kind)}
|
|
110
|
+
unless Sadr::Protocol.offset(snapshot[:rope], snapshot[:position]) == offset
|
|
111
|
+
raise Error, "invalid hierarchy position"
|
|
112
|
+
end
|
|
113
|
+
snapshot[:selection_subscription] = current.on_selection do
|
|
114
|
+
invalidate_hierarchy(editor: current) unless current.selections == snapshot[:selections]
|
|
115
|
+
end
|
|
116
|
+
snapshot[:edit_subscription] = buffer.on_edit { invalidate_hierarchy(buffer) }
|
|
117
|
+
snapshot
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def start_hierarchy_prepare(id, snapshot)
|
|
121
|
+
job = Thread.new do
|
|
122
|
+
begin
|
|
123
|
+
feature = snapshot[:kind] == :call ? "callHierarchy" : "typeHierarchy"
|
|
124
|
+
owner = language_client(snapshot[:buffer], feature: feature)
|
|
125
|
+
unless @hierarchy_prepare_requests&.[](id).equal?(snapshot) && hierarchy_editor_valid?(snapshot)
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
if snapshot[:client] && !snapshot[:client].equal?(owner)
|
|
129
|
+
raise Error, "Hierarchy cancelled because the language server changed"
|
|
130
|
+
end
|
|
131
|
+
snapshot[:client] = owner
|
|
132
|
+
snapshot[:capability] = hierarchy_capability(owner, snapshot[:kind])
|
|
133
|
+
unless snapshot[:capability]
|
|
134
|
+
post do
|
|
135
|
+
next unless take_hierarchy_prepare(id, snapshot)
|
|
136
|
+
valid = hierarchy_editor_valid?(snapshot)
|
|
137
|
+
release_hierarchy_snapshot(snapshot)
|
|
138
|
+
@message = "#{hierarchy_title(snapshot[:kind])} is not supported by this language server" if valid
|
|
139
|
+
end
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
method = snapshot[:kind] == :call ? :prepare_call_hierarchy : :prepare_type_hierarchy
|
|
144
|
+
future = owner.public_send(method, snapshot[:uri], snapshot[:position])
|
|
145
|
+
snapshot[:future] = future
|
|
146
|
+
result = future.await(timeout: 10) if hierarchy_prepare_valid?(id, snapshot, owner)
|
|
147
|
+
unless hierarchy_prepare_valid?(id, snapshot, owner)
|
|
148
|
+
future.cancel
|
|
149
|
+
next
|
|
150
|
+
end
|
|
151
|
+
roots = normalize_hierarchy_items(result)
|
|
152
|
+
post do
|
|
153
|
+
next unless take_hierarchy_prepare(id, snapshot)
|
|
154
|
+
unless hierarchy_context_valid?(snapshot)
|
|
155
|
+
release_hierarchy_snapshot(snapshot)
|
|
156
|
+
next
|
|
157
|
+
end
|
|
158
|
+
release_hierarchy_snapshot(snapshot)
|
|
159
|
+
if roots.empty?
|
|
160
|
+
@message = "No #{hierarchy_title(snapshot[:kind]).downcase} is available here"
|
|
161
|
+
elsif roots.length == 1
|
|
162
|
+
install_hierarchy(snapshot, roots.first)
|
|
163
|
+
else
|
|
164
|
+
self.palette = {kind: :hierarchy_roots, query: +"", index: 0,
|
|
165
|
+
matches: roots.map { |item| hierarchy_label(item) }, items: roots, hierarchy: snapshot}
|
|
166
|
+
update_palette
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
rescue StandardError => error
|
|
170
|
+
post do
|
|
171
|
+
next unless take_hierarchy_prepare(id, snapshot)
|
|
172
|
+
valid = hierarchy_editor_valid?(snapshot) && !(owner && @retired_language_clients&.[](owner))
|
|
173
|
+
release_hierarchy_snapshot(snapshot)
|
|
174
|
+
@message = error.message if valid
|
|
175
|
+
end
|
|
176
|
+
ensure
|
|
177
|
+
worker = Thread.current
|
|
178
|
+
post do
|
|
179
|
+
release_hierarchy_snapshot(snapshot) if take_hierarchy_prepare(id, snapshot)
|
|
180
|
+
@language_jobs&.delete(worker)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
(@language_jobs ||= []) << job
|
|
185
|
+
@language_jobs.reject! { |thread| !thread.alive? }
|
|
186
|
+
true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def install_hierarchy(state, root)
|
|
190
|
+
return false unless hierarchy_context_valid?(state)
|
|
191
|
+
|
|
192
|
+
state[:remaining] = HIERARCHY_TOTAL_LIMIT - 1
|
|
193
|
+
state[:node_id] = 0
|
|
194
|
+
@hierarchy_state = state
|
|
195
|
+
directions = hierarchy_directions(state[:kind]).map do |direction, label|
|
|
196
|
+
id = next_hierarchy_node_id(state)
|
|
197
|
+
{id: id, label: label, value: nil,
|
|
198
|
+
children: ->(_value) { request_hierarchy_children(state, id, root, direction,
|
|
199
|
+
[hierarchy_item_key(root)].to_set, 1) }}
|
|
200
|
+
end
|
|
201
|
+
item = {id: next_hierarchy_node_id(state), label: hierarchy_label(root),
|
|
202
|
+
value: {generation: state[:generation], item: root}.freeze, children: directions.freeze}
|
|
203
|
+
hierarchy_tree.replace([item.freeze])
|
|
204
|
+
@panels.show("hierarchy")
|
|
205
|
+
@message = hierarchy_title(state[:kind])
|
|
206
|
+
@window&.request_frame
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def request_hierarchy_children(state, node_id, item, direction, ancestry, depth)
|
|
211
|
+
return [] unless hierarchy_state_valid?(state)
|
|
212
|
+
if (@hierarchy_child_requests || {}).length >= HIERARCHY_REQUEST_LIMIT
|
|
213
|
+
return [hierarchy_status_node(state, "Too many pending hierarchy requests")]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
id = @hierarchy_child_request_id = @hierarchy_child_request_id.to_i + 1
|
|
217
|
+
request = {state: state, node_id: node_id, item: item, direction: direction,
|
|
218
|
+
ancestry: ancestry, depth: depth, client: state[:client], capability: state[:capability]}
|
|
219
|
+
(@hierarchy_child_requests ||= {})[id] = request
|
|
220
|
+
placeholder = hierarchy_status_node(state, "Loading…")
|
|
221
|
+
job = Thread.new do
|
|
222
|
+
begin
|
|
223
|
+
next unless hierarchy_child_valid?(id, request)
|
|
224
|
+
future = hierarchy_followup_future(request[:client], state[:kind], direction, item)
|
|
225
|
+
request[:future] = future
|
|
226
|
+
result = future.await(timeout: 10) if hierarchy_child_valid?(id, request)
|
|
227
|
+
unless hierarchy_child_valid?(id, request)
|
|
228
|
+
future.cancel
|
|
229
|
+
next
|
|
230
|
+
end
|
|
231
|
+
values = normalize_hierarchy_followup(state[:kind], direction, result, within: item)
|
|
232
|
+
post do
|
|
233
|
+
next unless take_hierarchy_child(id, request) && hierarchy_state_valid?(state)
|
|
234
|
+
children = hierarchy_children(state, values, direction, ancestry, depth)
|
|
235
|
+
hierarchy_tree.replace_children(node_id,
|
|
236
|
+
children.empty? ? [hierarchy_status_node(state, "No results")] : children)
|
|
237
|
+
end
|
|
238
|
+
rescue StandardError => error
|
|
239
|
+
post do
|
|
240
|
+
next unless take_hierarchy_child(id, request) && hierarchy_state_valid?(state)
|
|
241
|
+
hierarchy_tree.replace_children(node_id, [hierarchy_status_node(state, "Unavailable")])
|
|
242
|
+
@message = error.message unless @retired_language_clients&.[](request[:client])
|
|
243
|
+
end
|
|
244
|
+
ensure
|
|
245
|
+
worker = Thread.current
|
|
246
|
+
post do
|
|
247
|
+
@hierarchy_child_requests&.delete(id) if @hierarchy_child_requests&.[](id).equal?(request)
|
|
248
|
+
@language_jobs&.delete(worker)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
(@language_jobs ||= []) << job
|
|
253
|
+
@language_jobs.reject! { |thread| !thread.alive? }
|
|
254
|
+
[placeholder]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def hierarchy_children(state, values, direction, ancestry, depth)
|
|
258
|
+
unique = {}
|
|
259
|
+
values.each do |item|
|
|
260
|
+
key = hierarchy_item_key(item)
|
|
261
|
+
unique[key] ||= item unless ancestry.include?(key)
|
|
262
|
+
end
|
|
263
|
+
count = [unique.length, state[:remaining]].min
|
|
264
|
+
state[:remaining] -= count
|
|
265
|
+
unique.first(count).map do |key, item|
|
|
266
|
+
id = next_hierarchy_node_id(state)
|
|
267
|
+
node = {id: id, label: hierarchy_label(item),
|
|
268
|
+
value: {generation: state[:generation], item: item}.freeze}
|
|
269
|
+
if depth < HIERARCHY_DEPTH_LIMIT && state[:remaining].positive?
|
|
270
|
+
branch = ancestry.dup.add(key).freeze
|
|
271
|
+
node[:children] = ->(_value) { request_hierarchy_children(state, id, item, direction, branch, depth + 1) }
|
|
272
|
+
end
|
|
273
|
+
node.freeze
|
|
274
|
+
end.freeze
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def hierarchy_followup_future(client, kind, direction, item)
|
|
278
|
+
method = if kind == :call
|
|
279
|
+
direction == :incoming ? :call_hierarchy_incoming_calls : :call_hierarchy_outgoing_calls
|
|
280
|
+
else
|
|
281
|
+
direction == :supertypes ? :type_hierarchy_supertypes : :type_hierarchy_subtypes
|
|
282
|
+
end
|
|
283
|
+
client.public_send(method, item)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def normalize_hierarchy_followup(kind, direction, result, within:)
|
|
287
|
+
return normalize_hierarchy_items(result) if kind == :type
|
|
288
|
+
raise Error, "invalid call hierarchy" unless result.nil? || result.is_a?(Array)
|
|
289
|
+
raise Error, "too many call hierarchy items" if result && result.length > HIERARCHY_ITEM_LIMIT
|
|
290
|
+
|
|
291
|
+
key = direction == :incoming ? "from" : "to"
|
|
292
|
+
Array(result).map do |call|
|
|
293
|
+
raise Error, "invalid call hierarchy item" unless call.is_a?(Hash) && call.keys.all? { |name| name.is_a?(String) }
|
|
294
|
+
ranges = call.fetch("fromRanges")
|
|
295
|
+
unless ranges.is_a?(Array) && ranges.length <= HIERARCHY_ITEM_LIMIT
|
|
296
|
+
raise Error, "invalid call hierarchy ranges"
|
|
297
|
+
end
|
|
298
|
+
target = normalize_hierarchy_item(call.fetch(key))
|
|
299
|
+
outer = direction == :incoming ? target.fetch("range") : within.fetch("range")
|
|
300
|
+
ranges.each do |range|
|
|
301
|
+
edge = Sadr::Protocol.range_value(range)
|
|
302
|
+
parent = Sadr::Protocol.range_value(outer)
|
|
303
|
+
unless (hierarchy_position(parent.start) <=> hierarchy_position(edge.start)) <= 0 &&
|
|
304
|
+
(hierarchy_position(edge.end) <=> hierarchy_position(parent.end)) <= 0
|
|
305
|
+
raise Error, "call hierarchy range is outside its item"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
target
|
|
309
|
+
end.freeze
|
|
310
|
+
rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError, KeyError, TypeError,
|
|
311
|
+
ArgumentError, Sadr::Error => error
|
|
312
|
+
raise Error, "invalid call hierarchy: #{error.message}"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def normalize_hierarchy_items(result)
|
|
316
|
+
raise Error, "invalid hierarchy items" unless result.nil? || result.is_a?(Array)
|
|
317
|
+
raise Error, "too many hierarchy items" if result && result.length > HIERARCHY_ITEM_LIMIT
|
|
318
|
+
|
|
319
|
+
Array(result).map { |item| normalize_hierarchy_item(item) }.freeze
|
|
320
|
+
rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError, KeyError, TypeError,
|
|
321
|
+
ArgumentError, Sadr::Error => error
|
|
322
|
+
raise Error, "invalid hierarchy items: #{error.message}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def normalize_hierarchy_item(value)
|
|
326
|
+
raise Error, "invalid hierarchy item" unless value.is_a?(Hash) && value.keys.all? { |key| key.is_a?(String) }
|
|
327
|
+
encoded = JSON.generate(value)
|
|
328
|
+
raise Error, "hierarchy item exceeds 1 MiB" if encoded.bytesize > HIERARCHY_ITEM_BYTES_LIMIT
|
|
329
|
+
item = JSON.parse(encoded)
|
|
330
|
+
hierarchy_text(item.fetch("name"), "name")
|
|
331
|
+
kind = item.fetch("kind")
|
|
332
|
+
raise Error, "invalid hierarchy kind" unless kind.is_a?(Integer) && kind.between?(1, 26)
|
|
333
|
+
uri = hierarchy_text(item.fetch("uri"), "URI", maximum: HIERARCHY_URI_LIMIT)
|
|
334
|
+
Sadr::Protocol.path(uri)
|
|
335
|
+
range = Sadr::Protocol.range_value(item.fetch("range"))
|
|
336
|
+
selection = Sadr::Protocol.range_value(item.fetch("selectionRange"))
|
|
337
|
+
unless (hierarchy_position(range.start) <=> hierarchy_position(selection.start)) <= 0 &&
|
|
338
|
+
(hierarchy_position(selection.end) <=> hierarchy_position(range.end)) <= 0
|
|
339
|
+
raise Error, "hierarchy selection is outside its range"
|
|
340
|
+
end
|
|
341
|
+
hierarchy_text(item["detail"], "detail", empty: true) if item.key?("detail")
|
|
342
|
+
unless !item.key?("tags") || item["tags"].is_a?(Array) && item["tags"].all? { |tag| tag == 1 }
|
|
343
|
+
raise Error, "invalid hierarchy tags"
|
|
344
|
+
end
|
|
345
|
+
freeze_hierarchy_value(item)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def hierarchy_text(value, label, maximum: HIERARCHY_TEXT_LIMIT, empty: false)
|
|
349
|
+
valid = value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? &&
|
|
350
|
+
value.bytesize <= maximum && !value.include?("\0")
|
|
351
|
+
valid &&= !value.empty? unless empty
|
|
352
|
+
raise Error, "invalid hierarchy #{label}" unless valid
|
|
353
|
+
value
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def freeze_hierarchy_value(value)
|
|
357
|
+
value.each { |key, child| key.freeze; freeze_hierarchy_value(child) } if value.is_a?(Hash)
|
|
358
|
+
value.each { |child| freeze_hierarchy_value(child) } if value.is_a?(Array)
|
|
359
|
+
value.freeze
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def hierarchy_position(position) = [position.line, position.character]
|
|
363
|
+
|
|
364
|
+
def hierarchy_item_key(item)
|
|
365
|
+
range = item.fetch("selectionRange")
|
|
366
|
+
[item.fetch("uri"), item.fetch("kind"), item.fetch("name"),
|
|
367
|
+
range.fetch("start").fetch("line"), range.fetch("start").fetch("character"),
|
|
368
|
+
range.fetch("end").fetch("line"), range.fetch("end").fetch("character")].freeze
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def hierarchy_label(item)
|
|
372
|
+
path = Sadr::Protocol.path(item.fetch("uri"))
|
|
373
|
+
line = item.dig("selectionRange", "start", "line") + 1
|
|
374
|
+
detail = item["detail"].to_s.gsub(/\s+/, " ").strip
|
|
375
|
+
label = [item.fetch("name").gsub(/\s+/, " ").strip, detail].reject(&:empty?).join(" — ")
|
|
376
|
+
value = "#{label} · #{File.basename(path)}:#{line}"
|
|
377
|
+
value.length > 200 ? value.each_char.first(199).join + "…" : value
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def hierarchy_directions(kind)
|
|
381
|
+
kind == :call ? [[:incoming, "Incoming calls"], [:outgoing, "Outgoing calls"]] :
|
|
382
|
+
[[:supertypes, "Supertypes"], [:subtypes, "Subtypes"]]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def hierarchy_title(kind) = kind == :call ? "Call hierarchy" : "Type hierarchy"
|
|
386
|
+
|
|
387
|
+
def hierarchy_capability(client, kind)
|
|
388
|
+
return unless client&.respond_to?(:capabilities)
|
|
389
|
+
provider = client.capabilities[kind == :call ? "callHierarchyProvider" : "typeHierarchyProvider"]
|
|
390
|
+
return true if provider == true
|
|
391
|
+
JSON.generate(provider) if provider.is_a?(Hash)
|
|
392
|
+
rescue JSON::GeneratorError, JSON::NestingError
|
|
393
|
+
nil
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def hierarchy_editor_valid?(snapshot)
|
|
397
|
+
current, buffer = snapshot.values_at(:editor, :buffer)
|
|
398
|
+
!@closed && current.language_document.equal?(snapshot[:document]) && current.buffer.equal?(buffer) &&
|
|
399
|
+
buffer.version == snapshot[:version] && current.selections == snapshot[:selections] &&
|
|
400
|
+
current.primary.head == snapshot[:offset] && buffer.path && Sadr::Protocol.uri(buffer.path) == snapshot[:uri] &&
|
|
401
|
+
@panes.any? { |pane| pane.active.equal?(current) }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def hierarchy_context_valid?(state)
|
|
405
|
+
return false unless state && hierarchy_editor_valid?(state)
|
|
406
|
+
feature = state[:kind] == :call ? "callHierarchy" : "typeHierarchy"
|
|
407
|
+
client = active_language_client(state[:language], feature)
|
|
408
|
+
client.equal?(state[:client]) && hierarchy_capability(client, state[:kind]) == state[:capability]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def hierarchy_prepare_valid?(id, snapshot, owner)
|
|
412
|
+
@hierarchy_prepare_requests&.[](id).equal?(snapshot) && hierarchy_context_valid?(snapshot) &&
|
|
413
|
+
snapshot[:client].equal?(owner) && @opened_lsp_documents&.key?([owner, snapshot[:buffer]])
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def hierarchy_state_valid?(state)
|
|
417
|
+
client = state[:client]
|
|
418
|
+
!@closed && @hierarchy_state.equal?(state) && state[:generation] == @hierarchy_generation &&
|
|
419
|
+
active_language_client(state[:language], state[:kind] == :call ? "callHierarchy" : "typeHierarchy").equal?(client) &&
|
|
420
|
+
hierarchy_capability(client, state[:kind]) == state[:capability]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def hierarchy_child_valid?(id, request)
|
|
424
|
+
@hierarchy_child_requests&.[](id).equal?(request) && hierarchy_state_valid?(request[:state]) &&
|
|
425
|
+
request[:client].equal?(request[:state][:client]) && request[:capability] == request[:state][:capability]
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def take_hierarchy_prepare(id, snapshot) = @hierarchy_prepare_requests&.delete(id).equal?(snapshot)
|
|
429
|
+
def take_hierarchy_child(id, request) = @hierarchy_child_requests&.delete(id).equal?(request)
|
|
430
|
+
|
|
431
|
+
def release_hierarchy_snapshot(snapshot)
|
|
432
|
+
snapshot&.delete(:selection_subscription)&.detach
|
|
433
|
+
snapshot&.delete(:edit_subscription)&.detach
|
|
434
|
+
snapshot&.delete(:future)
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def cancel_hierarchy_children(state = nil)
|
|
439
|
+
@hierarchy_child_requests&.delete_if do |_id, request|
|
|
440
|
+
matches = !state || request[:state].equal?(state)
|
|
441
|
+
request[:future]&.cancel if matches
|
|
442
|
+
matches
|
|
443
|
+
end
|
|
444
|
+
nil
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def next_hierarchy_node_id(state)
|
|
448
|
+
state[:node_id] += 1
|
|
449
|
+
[:hierarchy, state[:generation], state[:node_id]].freeze
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def hierarchy_status_node(state, label)
|
|
453
|
+
{id: next_hierarchy_node_id(state), label: label, value: nil}.freeze
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def select_hierarchy_item(value)
|
|
457
|
+
state = @hierarchy_state
|
|
458
|
+
return false unless value.is_a?(Hash) && state && value[:generation] == state[:generation] && hierarchy_state_valid?(state)
|
|
459
|
+
|
|
460
|
+
item = value[:item]
|
|
461
|
+
path = Sadr::Protocol.path(item.fetch("uri"))
|
|
462
|
+
raise Error, "Hierarchy target is not a file" unless File.file?(path)
|
|
463
|
+
target = open(path)
|
|
464
|
+
position = Sadr::Protocol.range_value(item.fetch("selectionRange")).start
|
|
465
|
+
offset = Sadr::Protocol.offset(target.buffer.rope, position)
|
|
466
|
+
unless Sadr::Protocol.position(target.buffer.rope, offset) == position
|
|
467
|
+
raise Error, "Hierarchy target position is no longer valid"
|
|
468
|
+
end
|
|
469
|
+
target.select(offset)
|
|
470
|
+
target.reveal_cursor
|
|
471
|
+
true
|
|
472
|
+
rescue Error, KeyError, RangeError, TypeError, SystemCallError, Sadr::Error => error
|
|
473
|
+
@message = "Cannot open hierarchy item: #{error.message}"
|
|
474
|
+
false
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Canopus
|
|
6
|
+
module Workspace::KeymapAware
|
|
7
|
+
KEYMAP_PRESETS = %w[vscode sublime jetbrains emacs].freeze
|
|
8
|
+
|
|
9
|
+
def keymap_gui
|
|
10
|
+
entries = keymap_gui_entries
|
|
11
|
+
self.palette = {kind: :keymap_gui, query: +"", index: 0,
|
|
12
|
+
matches: entries.map { |entry| entry[:label] }, entries: entries, settings_file: settings_path}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def keymap_presets
|
|
16
|
+
self.palette = {kind: :keymap_presets, query: +"", index: 0, matches: KEYMAP_PRESETS}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def keymap_gui_entries
|
|
22
|
+
collisions = keymap_collisions
|
|
23
|
+
@commands.each.map do |definition|
|
|
24
|
+
bindings = definition.keybinding.is_a?(String) ? [definition.keybinding] : definition.keybinding&.keys || []
|
|
25
|
+
collision = bindings.any? { |keys| collisions.include?([keys, definition.when]) }
|
|
26
|
+
label = "#{definition.title} [#{definition.id}] — #{bindings.join(", ")}"
|
|
27
|
+
label = "⚠ #{label}" if collision
|
|
28
|
+
{id: definition.id, label: label, keys: bindings.first.to_s, default: bindings.first.to_s, collision: collision}.freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def keymap_collisions
|
|
33
|
+
values = Hash.new { |hash, key| hash[key] = [] }
|
|
34
|
+
@commands.each do |definition|
|
|
35
|
+
bindings = definition.keybinding.is_a?(String) ? {definition.keybinding => ""} : definition.keybinding || {}
|
|
36
|
+
bindings.each_key { |keys| values[[keys, definition.when]] << definition.id }
|
|
37
|
+
end
|
|
38
|
+
@settings["keymap"].each do |group|
|
|
39
|
+
group.fetch("bindings").each_key { |keys| values[[keys, group.fetch("context")]] << :settings }
|
|
40
|
+
end
|
|
41
|
+
values.filter_map { |key, ids| key if ids.length > 1 }.to_set
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_keymap_binding(entry, keys)
|
|
45
|
+
keys = keys.strip
|
|
46
|
+
raise Error, "key binding must not be empty" if keys.empty?
|
|
47
|
+
keys.split.each { |key| Zaniah::Input::Keystroke.normalize(key) }
|
|
48
|
+
groups = @settings["keymap"].map { |group| JSON.parse(JSON.generate(group)) }
|
|
49
|
+
group = groups.find { |item| item.fetch("context", "").empty? } || groups.unshift({"context" => "", "bindings" => {}}).first
|
|
50
|
+
group.fetch("bindings")[keys] = entry.fetch(:id)
|
|
51
|
+
@settings.set_file(settings_path, "keymap", groups)
|
|
52
|
+
poll_settings(force: true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_keymap_preset(name)
|
|
56
|
+
raise Error, "unknown keymap preset" unless KEYMAP_PRESETS.include?(name)
|
|
57
|
+
path = File.expand_path("../../../assets/keymaps/#{name}.jsonc", __dir__)
|
|
58
|
+
document = Kochab.parse(File.read(path))
|
|
59
|
+
raise Error, "invalid keymap preset #{name}" unless document.valid? && document.value.is_a?(Hash)
|
|
60
|
+
@settings.set_file(settings_path, "keymap", document.value.fetch("keymap"))
|
|
61
|
+
poll_settings(force: true)
|
|
62
|
+
rescue Errno::ENOENT, KeyError
|
|
63
|
+
raise Error, "invalid keymap preset #{name}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|