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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +191 -19
  4. data/assets/keymaps/emacs.jsonc +5 -0
  5. data/assets/keymaps/jetbrains.jsonc +5 -0
  6. data/assets/keymaps/sublime.jsonc +5 -0
  7. data/assets/keymaps/vscode.jsonc +5 -0
  8. data/docs/debugging.md +143 -0
  9. data/docs/distribution.md +40 -9
  10. data/docs/lsp.md +131 -0
  11. data/docs/performance.md +18 -0
  12. data/docs/providers.md +36 -0
  13. data/docs/release-log.md +12 -0
  14. data/docs/tasks.md +64 -0
  15. data/docs/testing.md +49 -0
  16. data/docs/vim.md +3 -0
  17. data/exe/canopus +4 -1
  18. data/lib/canopus/buffer.rb +291 -50
  19. data/lib/canopus/cli.rb +5 -2
  20. data/lib/canopus/command.rb +13 -4
  21. data/lib/canopus/controller.rb +183 -40
  22. data/lib/canopus/debug/breakpoints.rb +550 -0
  23. data/lib/canopus/debug/configuration.rb +291 -0
  24. data/lib/canopus/debug/console.rb +243 -0
  25. data/lib/canopus/debug/panel.rb +487 -0
  26. data/lib/canopus/debug/session.rb +354 -0
  27. data/lib/canopus/decoration.rb +4 -2
  28. data/lib/canopus/denebola_compat.rb +27 -0
  29. data/lib/canopus/diagnostics.rb +173 -0
  30. data/lib/canopus/display_map/overlay_map.rb +6 -2
  31. data/lib/canopus/editor/snippet_expandable.rb +3 -2
  32. data/lib/canopus/editor.rb +122 -31
  33. data/lib/canopus/git/state.rb +100 -0
  34. data/lib/canopus/language/background_analysis.rb +93 -14
  35. data/lib/canopus/language/document.rb +70 -34
  36. data/lib/canopus/language/symbol.rb +1 -0
  37. data/lib/canopus/language/syntax_worker.rb +61 -9
  38. data/lib/canopus/language.rb +3 -0
  39. data/lib/canopus/minimap.rb +201 -0
  40. data/lib/canopus/patch.rb +3 -1
  41. data/lib/canopus/plugins/api.rb +4 -2
  42. data/lib/canopus/plugins/isolated_runtime.rb +64 -7
  43. data/lib/canopus/plugins/registry.rb +114 -3
  44. data/lib/canopus/plugins.rb +8 -7
  45. data/lib/canopus/project.rb +15 -6
  46. data/lib/canopus/provider.rb +192 -0
  47. data/lib/canopus/regexp_compat.rb +36 -4
  48. data/lib/canopus/selection.rb +12 -0
  49. data/lib/canopus/settings/schema.rb +201 -0
  50. data/lib/canopus/settings.rb +328 -49
  51. data/lib/canopus/task/configuration.rb +356 -0
  52. data/lib/canopus/task/problem_matcher.rb +264 -0
  53. data/lib/canopus/task/runner.rb +314 -0
  54. data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
  55. data/lib/canopus/test_runner/discovery.rb +97 -0
  56. data/lib/canopus/test_runner/execution.rb +194 -0
  57. data/lib/canopus/test_runner/minitest.rb +116 -0
  58. data/lib/canopus/test_runner/rspec.rb +103 -0
  59. data/lib/canopus/test_runner/safe_walker.rb +37 -0
  60. data/lib/canopus/test_runner.rb +54 -0
  61. data/lib/canopus/theme/import.rb +131 -0
  62. data/lib/canopus/theme.rb +28 -4
  63. data/lib/canopus/version.rb +1 -1
  64. data/lib/canopus/vim/motionable.rb +1 -1
  65. data/lib/canopus/vim.rb +10 -16
  66. data/lib/canopus/workspace/auto_savable.rb +83 -0
  67. data/lib/canopus/workspace/debug_aware.rb +600 -0
  68. data/lib/canopus/workspace/edit/plan.rb +2 -2
  69. data/lib/canopus/workspace/editor_configurable.rb +113 -0
  70. data/lib/canopus/workspace/file_change_aware.rb +11 -3
  71. data/lib/canopus/workspace/git_aware.rb +80 -34
  72. data/lib/canopus/workspace/git_blame.rb +175 -0
  73. data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
  74. data/lib/canopus/workspace/git_diff_view.rb +641 -0
  75. data/lib/canopus/workspace/git_history.rb +171 -0
  76. data/lib/canopus/workspace/git_remote.rb +267 -0
  77. data/lib/canopus/workspace/git_staging.rb +583 -0
  78. data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
  79. data/lib/canopus/workspace/keymap_aware.rb +66 -0
  80. data/lib/canopus/workspace/language_aware.rb +3721 -135
  81. data/lib/canopus/workspace/language_server_configurable.rb +287 -71
  82. data/lib/canopus/workspace/persistent_undo.rb +169 -0
  83. data/lib/canopus/workspace/problems_aware.rb +131 -0
  84. data/lib/canopus/workspace/project_searchable.rb +88 -11
  85. data/lib/canopus/workspace/project_tree_editable.rb +3 -1
  86. data/lib/canopus/workspace/recoverable.rb +204 -0
  87. data/lib/canopus/workspace/session_persistable.rb +85 -13
  88. data/lib/canopus/workspace/settings_aware.rb +76 -2
  89. data/lib/canopus/workspace/task_aware.rb +194 -0
  90. data/lib/canopus/workspace/test_aware.rb +394 -0
  91. data/lib/canopus/workspace/trust.rb +73 -0
  92. data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
  93. data/lib/canopus/workspace/view.rb +399 -74
  94. data/lib/canopus/workspace.rb +688 -44
  95. data/lib/canopus.rb +20 -2
  96. data/sig/canopus.rbs +138 -31
  97. data/sig/controller.rbs +10 -3
  98. data/sig/debug.rbs +121 -0
  99. data/sig/decoration.rbs +12 -2
  100. data/sig/diagnostics.rbs +26 -0
  101. data/sig/minimap.rbs +20 -0
  102. data/sig/provider.rbs +29 -0
  103. data/sig/services.rbs +1 -16
  104. data/sig/task.rbs +92 -0
  105. data/sig/test_runner.rbs +109 -0
  106. data/sig/trust.rbs +17 -0
  107. data/sig/workspace_edits.rbs +2 -2
  108. data/sig/workspace_services.rbs +234 -2
  109. data/tools/check_dependencies.rb +21 -2
  110. data/tools/package.rb +172 -14
  111. data/tools/package_signature_test.rb +33 -0
  112. data/tools/package_test.rb +29 -0
  113. data/tools/sign_package.rb +123 -0
  114. data/tools/update.rb +189 -0
  115. data/tools/update_test.rb +71 -0
  116. metadata +206 -31
  117. data/lib/canopus/lsp/client.rb +0 -307
  118. data/lib/canopus/lsp/error.rb +0 -7
  119. data/lib/canopus/lsp/future/subscription.rb +0 -9
  120. data/lib/canopus/lsp/future.rb +0 -87
  121. data/lib/canopus/lsp/protocol.rb +0 -83
  122. data/lib/canopus/lsp/server_error.rb +0 -13
  123. data/lib/canopus/lsp/timeout.rb +0 -7
  124. data/lib/canopus/lsp/transport.rb +0 -123
  125. data/lib/canopus/lsp.rb +0 -19
  126. data/lib/canopus/project/search.rb +0 -164
  127. data/lib/canopus/project/search_worker/cancelled.rb +0 -3
  128. data/lib/canopus/project/search_worker/runner.rb +0 -9
  129. data/lib/canopus/project/search_worker.rb +0 -108
  130. data/lib/canopus/terminal/cell.rb +0 -7
  131. data/lib/canopus/terminal/grid.rb +0 -321
  132. data/lib/canopus/terminal/pty.rb +0 -178
  133. data/lib/canopus/terminal/scrollback.rb +0 -38
  134. data/lib/canopus/terminal/vt.rb +0 -398
  135. data/lib/canopus/terminal.rb +0 -13
  136. data/sig/lsp.rbs +0 -99
  137. 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