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
@@ -1,17 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "strscan"
4
+
3
5
  module Canopus
4
6
  module Workspace::LanguageAware
7
+ DIAGNOSTIC_SEVERITIES = {"error" => 1, "warning" => 2, "information" => 3, "hint" => 4}.freeze
8
+ DIAGNOSTIC_COLORS = {1 => :"diagnostic.error", 2 => :"diagnostic.warning",
9
+ 3 => :"diagnostic.information", 4 => :"diagnostic.hint"}.freeze
10
+ CODE_LENS_REQUEST_LIMIT = 64
11
+ DOCUMENT_HIGHLIGHT_LIMIT = 10_000
12
+ DOCUMENT_HIGHLIGHT_REQUEST_LIMIT = 64
13
+ DOCUMENT_LINK_LIMIT = 10_000
14
+ DOCUMENT_LINK_REQUEST_LIMIT = 64
15
+ DOCUMENT_LINK_RESOLVE_LIMIT = 32
16
+ DOCUMENT_LINK_URI_LIMIT = 16_384
17
+ DOCUMENT_LINK_TEXT_LIMIT = 4_096
18
+ FOLDING_RANGE_LIMIT = 10_000
19
+ FOLDING_RANGE_REQUEST_LIMIT = 64
20
+ SELECTION_RANGE_LIMIT = 10_000
21
+ SELECTION_RANGE_POSITION_LIMIT = 256
22
+ SELECTION_RANGE_DEPTH_LIMIT = 256
23
+ SELECTION_RANGE_REQUEST_LIMIT = 64
24
+ PREPARE_RENAME_REQUEST_LIMIT = 1
25
+ RENAME_VALUE_LIMIT = 4096
26
+ LINKED_EDITING_RANGE_LIMIT = 256
27
+ LINKED_EDITING_REQUEST_LIMIT = 1
28
+ LINKED_EDITING_PATTERN_LIMIT = 4_096
29
+ LINKED_EDITING_MARKUP_BYTE_LIMIT = 1 << 20
30
+ LINKED_EDITING_MARKUP_TAG_LIMIT = 10_000
31
+ LINKED_EDITING_MARKUP_DEPTH_LIMIT = 256
32
+ LINKED_EDITING_MARKUP_NAME_LIMIT = 1_024
33
+ LINKED_EDITING_HTML_VOID_TAGS = %w[area base br col embed hr img input link meta param source track wbr].freeze
34
+ DOCUMENT_HIGHLIGHT_STYLES = {
35
+ 1 => {color: :selection}.freeze,
36
+ 2 => {color: :accent, underline: true, thickness: 2}.freeze,
37
+ 3 => {color: :muted, underline: true}.freeze
38
+ }.freeze
39
+ DOCUMENT_LINK_STYLE = {color: :accent, underline: true}.freeze
40
+ CODE_LENS_RESOLVE_LIMIT = 32
41
+ INDENT_GUIDE_LIMIT = 4096
42
+ STICKY_SYMBOL_LIMIT = 10_000
43
+ STICKY_DEPTH_LIMIT = 64
44
+ STICKY_REQUEST_LIMIT = 16
45
+ WORKSPACE_SYMBOL_LIMIT = 10_000
46
+ WORKSPACE_SYMBOL_QUERY_LIMIT = 256
47
+ WORKSPACE_SYMBOL_TIMEOUT = 10
48
+ WORKSPACE_SYMBOL_LABEL_BYTES = 512
49
+ WORKSPACE_SYMBOL_PREVIEW_BYTES = 512
50
+ WORKSPACE_SYMBOL_UTF16_CHUNK_BYTES = 64 * 1024
51
+ BREADCRUMB_CONTAINER_KINDS = [2, 3, 4, 5, 10, 11, 23].freeze
52
+ BREADCRUMB_CALLABLE_KINDS = [6, 9, 12].freeze
53
+
5
54
  attr_reader :hover_card, :hover_markup, :semantic_styles
6
55
  def dismiss_hover = @hover_card = nil
7
56
  def close_language_documents(buffer = nil)
57
+ invalidate_hierarchy(buffer)
8
58
  @opened_lsp_documents&.keys&.each do |key|
9
59
  client, document = key
10
60
  next if buffer && !document.equal?(buffer)
11
- @opened_lsp_documents.delete(key)
12
61
  begin
13
- client.close_document(LSP::Protocol.uri(document.path)) if document.path
62
+ close_language_document(client, document) if document.path
14
63
  rescue StandardError => error
64
+ forget_language_document(client, document)
15
65
  self.message = "Language server close failed: #{error.message}"
16
66
  end
17
67
  end
@@ -31,64 +81,1013 @@ module Canopus
31
81
  end
32
82
 
33
83
  def post(&block)
34
- (@main_queue ||= Queue.new) << block
84
+ @main_queue << block
35
85
  @window&.request_frame
36
86
  end
37
- def language_client(buffer = editor.buffer)
87
+ def language_clients(buffer = editor.buffer, feature: nil, timeout: nil)
38
88
  raise Error, "language servers are disabled for large read-only documents" if buffer.read_only
39
89
  language = definition_for(buffer.path)
90
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
40
91
  (@client_lock ||= Mutex.new).synchronize do
41
92
  options = language_server_options(language.name)
42
93
  raise Error, "No language server configured for #{language.name}" unless options
43
- client = ensure_language_server(language.name, options)
94
+ remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
95
+ raise Sadr::Timeout, "language server startup timed out" if remaining && remaining <= 0
96
+ ensure_language_server(language.name, options, timeout: remaining)
97
+ clients = language_client_list(language.name)
44
98
  @opened_lsp_documents ||= {}
45
- unless @opened_lsp_documents[[client, buffer]]
46
- client.open_document(buffer, language_id: language.name)
47
- @opened_lsp_documents[[client, buffer]] = true
99
+ clients.each do |client|
100
+ open_language_document(client, buffer, language.name) unless @opened_lsp_documents[[client, buffer]]
48
101
  end
49
- client
102
+ route_language_clients(language.name, clients, feature)
50
103
  end
51
104
  end
105
+
106
+ def language_client(buffer = editor.buffer, feature: nil, timeout: nil)
107
+ client = language_clients(buffer, feature: feature, timeout: timeout).first
108
+ raise Error, "No language server supports #{feature}" if !client && feature
109
+ client || raise(Error, "No language server configured for #{definition_for(buffer.path).name}")
110
+ end
52
111
  def language_request(kind, **options)
112
+ return request_workspace_symbols(options.fetch(:query, "")) if kind == :workspace_symbols
113
+
53
114
  current, buffer, offset = editor, editor.buffer, editor.primary.head
115
+ if kind == :inlayHint
116
+ requested = request_visible_inlay_hints(current)
117
+ @message = "#{kind}…" if requested
118
+ return requested
119
+ end
120
+ return request_completions(current, buffer, offset) if kind == :completion
121
+
54
122
  version = buffer.version
55
123
  (@language_jobs ||= []) << Thread.new do
56
124
  begin
57
- client = language_client(buffer)
58
- result = case kind
59
- when :completion, :hover, :definition, :typeDefinition, :implementation, :signatureHelp
60
- client.public_send(kind, buffer, offset).await
61
- when :references then client.references(buffer, offset, context: {includeDeclaration: true}).await
62
- when :rename then client.rename(buffer, offset, newName: options.fetch(:name)).await
63
- when :formatting then client.formatting(buffer, options: {tabSize: current.tab_size, insertSpaces: !current.use_tabs}).await
64
- when :codeAction
65
- client.codeAction(buffer, range: LSP::Protocol.range(buffer.rope, current.primary.range), context: {diagnostics: diagnostics_for(buffer)}).await
66
- when :documentSymbol, :codeLens, :diagnostic then client.public_send(kind, buffer).await
67
- when :inlayHint then client.inlayHint(buffer, range: LSP::Protocol.range(buffer.rope, 0...buffer.rope.bytesize)).await
68
- when :semantic_tokens then client.semantic_tokens(buffer)
69
- when :workspace_symbols then client.workspace_symbols(options.fetch(:query, "")).await
70
- else raise Error, "unknown language request #{kind}"
125
+ feature = language_request_feature(kind)
126
+ language_name = definition_for(buffer.path).name
127
+ aggregate = %i[codeAction diagnostic].include?(kind)
128
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10 if aggregate
129
+ clients = language_clients(buffer, feature: feature,
130
+ timeout: deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC))
131
+ uri = Sadr::Protocol.uri(buffer.path)
132
+ position = Sadr::Protocol.position(buffer.rope, offset)
133
+ raise Error, "No language server supports #{feature}" if clients.empty?
134
+ if kind == :codeAction
135
+ pending = clients.map do |owner|
136
+ [owner, owner.code_action(uri, Sadr::Protocol.range(buffer.rope, current.primary.range),
137
+ {diagnostics: diagnostics_for(buffer)}), nil]
138
+ rescue StandardError => error
139
+ [owner, nil, error]
140
+ end
141
+ pairs, failures, successes = [], [], 0
142
+ pending.each do |owner, future, failure|
143
+ if failure
144
+ failures << failure
145
+ next
146
+ end
147
+ begin
148
+ result = language_request_await(future, deadline)
149
+ successes += 1
150
+ pairs.concat(Array(result).map { |item| [item, owner] })
151
+ rescue StandardError => error
152
+ failures << error
153
+ end
154
+ end
155
+ raise failures.first if successes.zero? && !failures.empty?
156
+ pairs = deduplicate_client_items(pairs)
157
+ client, result, item_clients = clients.first, pairs.map(&:first), pairs.map(&:last)
158
+ elsif kind == :diagnostic
159
+ pending = clients.map do |owner|
160
+ [owner, owner.diagnostic(uri), nil]
161
+ rescue StandardError => error
162
+ [owner, nil, error]
163
+ end
164
+ failures, successes = [], 0
165
+ results = pending.filter_map do |owner, future, failure|
166
+ if failure
167
+ failures << failure
168
+ next
169
+ end
170
+ begin
171
+ value = language_request_await(future, deadline)
172
+ successes += 1
173
+ [owner, value]
174
+ rescue StandardError => error
175
+ failures << error
176
+ nil
177
+ end
178
+ end
179
+ raise failures.first if successes.zero? && !failures.empty?
180
+ client, result = clients.first, results
181
+ else
182
+ client = clients.first
183
+ result = case kind
184
+ when :hover, :definition, :typeDefinition, :implementation, :signatureHelp
185
+ method = {typeDefinition: :type_definition, signatureHelp: :signature_help}.fetch(kind, kind)
186
+ client.public_send(method, uri, position).await(timeout: 10)
187
+ when :references then client.references(uri, position, include_declaration: true).await(timeout: 10)
188
+ when :rename then client.rename(uri, position, options.fetch(:name)).await(timeout: 10)
189
+ when :formatting then client.formatting(uri, {tabSize: current.tab_size, insertSpaces: !current.use_tabs}).await(timeout: 10)
190
+ when :documentSymbol then client.document_symbol(uri).await(timeout: 10)
191
+ when :codeLens then client.code_lens(uri).await(timeout: 10)
192
+ when :semantic_tokens then client.semantic_tokens(uri, version: buffer.version)
193
+ when :workspace_symbols then client.workspace_symbols(options.fetch(:query, "")).await(timeout: 10)
194
+ else raise Error, "unknown language request #{kind}"
195
+ end
71
196
  end
72
197
  post do
73
- next unless @clients.value?(client)
198
+ routed = route_language_clients(language_name, language_client_list(language_name), feature)
199
+ if %i[codeAction diagnostic].include?(kind)
200
+ next unless clients == routed
201
+ else
202
+ next unless routed.first.equal?(client)
203
+ end
74
204
  next unless @panes.any? { |pane| pane.editors.include?(current) }
75
205
  if buffer.version == version
76
- display_language_result(kind, result, client, current)
206
+ if kind == :diagnostic
207
+ result.each { |owner, value| display_language_result(kind, value, owner, current) }
208
+ else
209
+ display_language_result(kind, result, client, current, item_clients: item_clients)
210
+ end
77
211
  else
78
212
  @message = "Document changed; request #{kind} again"
79
213
  end
80
214
  end
81
215
  rescue StandardError => error
82
216
  post { @message = error.message unless client && @retired_language_clients&.[](client) }
217
+ ensure
218
+ cancel_language_requests(pending)
83
219
  end
84
220
  end
85
221
  @language_jobs.reject! { |thread| !thread.alive? }
86
222
  @message = "#{kind}…"
87
223
  end
224
+
225
+ def language_request_feature(kind)
226
+ {diagnostic: "diagnostics", semantic_tokens: "semanticTokens", workspace_symbols: "workspaceSymbol"}.fetch(kind, kind.to_s)
227
+ end
228
+
229
+ def route_language_clients(language, clients, feature)
230
+ return clients if feature.nil?
231
+ feature = feature.to_s
232
+ options = server_options_list(@client_options&.[](language))
233
+ clients.each_with_index.filter_map do |client, index|
234
+ client if language_server_feature?(options[index], client, feature)
235
+ end
236
+ end
237
+
238
+ def language_request_await(future, deadline)
239
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
240
+ raise Sadr::Timeout, "language server request timed out" if remaining <= 0
241
+ future.await(timeout: remaining)
242
+ end
243
+
244
+ def cancel_language_requests(pending)
245
+ Array(pending).each do |_client, future, _failure|
246
+ future.cancel if future&.respond_to?(:cancel) && (!future.respond_to?(:done?) || !future.done?)
247
+ end
248
+ end
249
+
250
+ def deduplicate_client_items(pairs)
251
+ seen = {}
252
+ pairs.each_with_object([]) do |pair, values|
253
+ item = pair.first
254
+ next if seen.key?(item)
255
+ seen[item] = true
256
+ values << pair
257
+ end
258
+ end
259
+
260
+ def active_language_client(language, feature)
261
+ route_language_clients(language, language_client_list(language), feature).first
262
+ end
263
+ private :language_request_feature, :route_language_clients, :active_language_client,
264
+ :language_request_await, :cancel_language_requests, :deduplicate_client_items
265
+
266
+ def request_workspace_symbols(query)
267
+ valid = query.is_a?(String) && query.valid_encoding? && query.bytesize.between?(1, WORKSPACE_SYMBOL_QUERY_LIMIT) &&
268
+ !query.match?(/[\0\r\n]/) && !query.strip.empty?
269
+ raise Error, "workspace symbol query must be 1 to #{WORKSPACE_SYMBOL_QUERY_LIMIT} bytes" unless valid
270
+
271
+ cancel_workspace_symbol_search
272
+ generation = @workspace_symbol_generation = @workspace_symbol_generation.to_i + 1
273
+ query = query.dup.freeze
274
+ request = {generation: generation, futures: [], clients: []}
275
+ (@workspace_symbol_lock ||= Mutex.new).synchronize { @workspace_symbol_request = request }
276
+ empty = [].freeze
277
+ self.palette = {kind: :workspace_symbol_results, query: +"", index: 0, matches: empty, items: empty,
278
+ all_matches: empty, indices: empty, workspace_symbol_groups: {}.freeze,
279
+ workspace_symbol_index: Spica::Index.new(empty, tie_break: :index), workspace_symbol_generation: generation}
280
+ job = Thread.new do
281
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WORKSPACE_SYMBOL_TIMEOUT
282
+ clients = workspace_symbol_clients
283
+ (@workspace_symbol_lock ||= Mutex.new).synchronize do
284
+ request[:clients] = clients if @workspace_symbol_request.equal?(request)
285
+ end
286
+ pending = clients.filter_map do |client|
287
+ next unless workspace_symbol_request_current?(request)
288
+ entry = begin
289
+ [client, client.workspace_symbols(query), nil]
290
+ rescue StandardError => error
291
+ [client, nil, error]
292
+ end
293
+ tracked = (@workspace_symbol_lock ||= Mutex.new).synchronize do
294
+ if @workspace_symbol_request.equal?(request)
295
+ request[:futures] << entry
296
+ true
297
+ end
298
+ end
299
+ cancel_language_requests([entry]) unless tracked
300
+ entry if tracked
301
+ end
302
+ symbols, seen, successes = [], {}, 0
303
+ pending.each do |client, future, failure|
304
+ next if failure || !workspace_symbol_request_current?(request)
305
+ begin
306
+ response = language_request_await(future, deadline)
307
+ next if client.respond_to?(:running?) && !client.running?
308
+ values = normalize_workspace_symbols(response)
309
+ successes += 1
310
+ values.each do |symbol|
311
+ break if symbols.length >= WORKSPACE_SYMBOL_LIMIT
312
+ next if seen.key?(symbol)
313
+ seen[symbol] = true
314
+ symbols << symbol
315
+ end
316
+ rescue StandardError
317
+ nil
318
+ end
319
+ end
320
+ if successes.zero? && workspace_symbol_request_current?(request)
321
+ fallback_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WORKSPACE_SYMBOL_TIMEOUT
322
+ symbols = workspace_symbol_fallback(query, generation, fallback_deadline)
323
+ end
324
+ next unless workspace_symbol_request_current?(request)
325
+ symbols = deduplicate_workspace_symbols(symbols).first(WORKSPACE_SYMBOL_LIMIT).freeze
326
+ prepared = prepare_workspace_symbol_palette(symbols, generation)
327
+ post do
328
+ install_workspace_symbol_palette(request, prepared)
329
+ end
330
+ rescue StandardError => error
331
+ post { @message = error.message if workspace_symbol_request_current?(request) }
332
+ ensure
333
+ cancel_language_requests(pending)
334
+ (@workspace_symbol_lock ||= Mutex.new).synchronize do
335
+ request[:futures].clear if @workspace_symbol_request.equal?(request)
336
+ end
337
+ end
338
+ (@language_jobs ||= []) << job
339
+ @language_jobs.reject! { |thread| !thread.alive? }
340
+ @message = "Searching workspace symbols…"
341
+ job
342
+ end
343
+
344
+ def cancel_workspace_symbol_search
345
+ request = futures = nil
346
+ (@workspace_symbol_lock ||= Mutex.new).synchronize do
347
+ request = @workspace_symbol_request
348
+ if request
349
+ @workspace_symbol_request = nil
350
+ @workspace_symbol_generation = @workspace_symbol_generation.to_i + 1
351
+ futures = request[:futures].dup
352
+ request[:futures].clear
353
+ end
354
+ end
355
+ cancel_language_requests(futures) if request
356
+ nil
357
+ end
358
+
359
+ def workspace_symbol_clients
360
+ (@client_lock ||= Mutex.new).synchronize do
361
+ configured = if @language_clients&.any?
362
+ @language_clients
363
+ else
364
+ @clients.to_h { |language, client| [language, [client]] }
365
+ end
366
+ configured.flat_map do |language, clients|
367
+ route_language_clients(language, clients, "workspaceSymbol")
368
+ end.select { |client| !client.respond_to?(:running?) || client.running? }.uniq
369
+ end
370
+ end
371
+
372
+ def normalize_workspace_symbols(value)
373
+ return [] if value.nil?
374
+ raise Error, "invalid workspace symbol response" unless value.is_a?(Array)
375
+ raise Error, "too many workspace symbols" if value.length > WORKSPACE_SYMBOL_LIMIT
376
+
377
+ value.filter_map do |symbol|
378
+ raise Error, "invalid workspace symbol" unless symbol.is_a?(Hash)
379
+ name, kind, container = symbol.values_at("name", "kind", "containerName")
380
+ valid_name = name.is_a?(String) && name.valid_encoding? && name.bytesize.between?(1, 4_096)
381
+ valid_container = container.nil? || container.is_a?(String) && container.valid_encoding? && container.bytesize <= 4_096
382
+ raise Error, "invalid workspace symbol" unless valid_name && kind.is_a?(Integer) && kind.between?(1, 26) && valid_container
383
+
384
+ location = symbol["location"]
385
+ next unless location.is_a?(Hash) && location["range"]
386
+ uri = location["uri"]
387
+ Sadr::Protocol.path(uri)
388
+ range = Sadr::Protocol.range_value(location["range"])
389
+ normalized = {"name" => name.dup.freeze, "kind" => kind,
390
+ "location" => {"uri" => uri.dup.freeze, "range" => {
391
+ "start" => {"line" => range.start.line, "character" => range.start.character}.freeze,
392
+ "end" => {"line" => range.end.line, "character" => range.end.character}.freeze
393
+ }.freeze}.freeze}
394
+ normalized["containerName"] = container.dup.freeze if container
395
+ normalized.freeze
396
+ rescue Sadr::Error, KeyError, TypeError
397
+ nil
398
+ end
399
+ end
400
+
401
+ def deduplicate_workspace_symbols(symbols)
402
+ seen = {}
403
+ symbols.each_with_object([]) do |symbol, values|
404
+ next if seen.key?(symbol)
405
+ seen[symbol] = true
406
+ values << symbol
407
+ end
408
+ end
409
+
410
+ def workspace_symbol_cancelled?(generation, deadline)
411
+ @closed || generation != @workspace_symbol_generation ||
412
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
413
+ end
414
+
415
+ def workspace_symbol_request_current?(request)
416
+ return false if @closed || request[:generation] != @workspace_symbol_generation ||
417
+ !@workspace_symbol_request.equal?(request)
418
+ request[:clients].none? { |client| @retired_language_clients&.[](client) }
419
+ end
420
+
421
+ def prepare_workspace_symbol_palette(items, generation)
422
+ labels = items.map do |item|
423
+ location = item.fetch("location")
424
+ path = Sadr::Protocol.path(location.fetch("uri"))
425
+ container = item["containerName"]
426
+ name = bounded_workspace_symbol_text(item.fetch("name"), 256)
427
+ container = bounded_workspace_symbol_text(container, 128) if container
428
+ basename = bounded_workspace_symbol_text(File.basename(path), 96)
429
+ label = "#{name}#{container ? " — #{container}" : ""} #{basename}:#{location.dig('range', 'start', 'line') + 1}".freeze
430
+ bounded_workspace_symbol_text(label, WORKSPACE_SYMBOL_LABEL_BYTES)
431
+ end
432
+ groups = labels.each_with_index.each_with_object({}) do |(label, index), values|
433
+ (values[label] ||= []) << index
434
+ end
435
+ groups.each_value(&:freeze)
436
+ groups.freeze
437
+ visible = [items.length, 12].min
438
+ # Index construction is worker-safe; its mutable, thread-confined Session is created by update_palette.
439
+ {kind: :workspace_symbol_results, query: +"", index: 0, matches: labels.first(visible).freeze,
440
+ all_matches: labels.freeze, items: items, indices: (0...visible).to_a.freeze,
441
+ workspace_symbol_groups: groups, workspace_symbol_index: Spica::Index.new(groups.keys, tie_break: :index),
442
+ workspace_symbol_generation: generation}
443
+ end
444
+
445
+ def bounded_workspace_symbol_text(value, maximum)
446
+ return value if value.frozen? && value.bytesize <= maximum
447
+ return value.dup.freeze if value.bytesize <= maximum
448
+ result = +""
449
+ value.each_grapheme_cluster do |cluster|
450
+ break if result.bytesize + cluster.bytesize + "…".bytesize > maximum
451
+ result << cluster
452
+ end
453
+ result << "…" if maximum >= "…".bytesize
454
+ result.freeze
455
+ end
456
+
457
+ def display_workspace_symbols(prepared)
458
+ query = if @palette&.dig(:kind) == :workspace_symbol_results &&
459
+ @palette[:workspace_symbol_generation] == prepared[:workspace_symbol_generation]
460
+ @palette.fetch(:query).dup
461
+ else
462
+ +""
463
+ end
464
+ prepared = prepared.merge(query: query)
465
+ self.palette = prepared
466
+ update_palette unless query.empty?
467
+ count = prepared.fetch(:items).length
468
+ @message = count.zero? ? "No workspace symbols found" : "#{count} workspace symbols"
469
+ end
470
+
471
+ def install_workspace_symbol_palette(request, prepared)
472
+ (@workspace_symbol_lock ||= Mutex.new).synchronize do
473
+ return false unless workspace_symbol_request_current?(request)
474
+ display_workspace_symbols(prepared)
475
+ true
476
+ end
477
+ end
478
+ private :workspace_symbol_clients, :normalize_workspace_symbols, :deduplicate_workspace_symbols,
479
+ :workspace_symbol_cancelled?, :workspace_symbol_request_current?, :prepare_workspace_symbol_palette,
480
+ :bounded_workspace_symbol_text, :display_workspace_symbols, :install_workspace_symbol_palette
481
+
482
+ def run_save_actions(buffer)
483
+ language = definition_for(buffer.path)
484
+ values = settings_for_path(buffer.path, language: language.name)
485
+ kinds = values["code_actions_on_save"]
486
+ return false unless values["format_on_save"] || !kinds.empty?
487
+ return false unless language_server_options(language.name)
488
+
489
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + values["format_on_save_timeout"] / 1_000.0
490
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
491
+ raise Sadr::Timeout, "save actions timed out" if remaining <= 0
492
+ clients = language_clients(buffer, timeout: remaining)
493
+ raise Sadr::Timeout, "save actions timed out" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
494
+ buffer.begin_undo_group
495
+ ran = false
496
+ begin
497
+ formatter = route_language_clients(language.name, clients, "formatting").find do |client|
498
+ capability = client.capabilities["documentFormattingProvider"]
499
+ capability == true || capability.is_a?(Hash)
500
+ end
501
+ if values["format_on_save"] && formatter
502
+ version = buffer.version
503
+ edits = save_action_await(formatter.formatting(Sadr::Protocol.uri(buffer.path),
504
+ {tabSize: values["tab_size"], insertSpaces: !values["use_tabs"]}), deadline)
505
+ raise Error, "document changed while awaiting save formatting" unless buffer.version == version
506
+ apply_save_format(buffer, version, edits)
507
+ ran = true
508
+ end
509
+ action_clients = route_language_clients(language.name, clients, "codeAction").select do |client|
510
+ provider = client.capabilities["codeActionProvider"]
511
+ provider == true || provider.is_a?(Hash)
512
+ end
513
+ unless action_clients.empty?
514
+ kinds.each { |kind| ran = run_code_action_on_save(action_clients, buffer, kind, deadline) || ran }
515
+ end
516
+ ran
517
+ ensure
518
+ buffer.end_undo_group
519
+ clients.each { |client| @save_action_errors&.delete(client) }
520
+ end
521
+ end
522
+
523
+ def save_action_await(future, deadline)
524
+ unless future.respond_to?(:done?) && future.respond_to?(:await)
525
+ raise Error, "language server request did not return a future"
526
+ end
527
+ loop do
528
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
529
+ if remaining <= 0
530
+ future.cancel if !future.done? && future.respond_to?(:cancel)
531
+ raise Sadr::Timeout, "save actions timed out"
532
+ end
533
+ break if future.done?
534
+ drain
535
+ sleep([remaining, 0.001].min) unless future.done?
536
+ end
537
+ drain
538
+ future.await(timeout: 0)
539
+ end
540
+
541
+ def apply_save_format(buffer, version, edits)
542
+ edits ||= []
543
+ raise Error, "invalid formatting response" unless edits.is_a?(Array)
544
+ edit = {"documentChanges" => [{"textDocument" => {
545
+ "uri" => Sadr::Protocol.uri(buffer.path), "version" => version
546
+ }, "edits" => edits}]}
547
+ result = apply_workspace_edit(edit)
548
+ raise Error, result["failureReason"].to_s unless result["applied"]
549
+ end
550
+
551
+ def run_code_action_on_save(clients, buffer, kind, deadline)
552
+ version = buffer.version
553
+ range = Sadr::Protocol.range(buffer.rope, 0...buffer.rope.bytesize)
554
+ pending = clients.map do |client|
555
+ [client, client.code_action(Sadr::Protocol.uri(buffer.path), range,
556
+ {diagnostics: diagnostics_for(buffer), only: [kind], triggerKind: 2}), nil]
557
+ rescue StandardError => error
558
+ [client, nil, error]
559
+ end
560
+ failures, successes = [], 0
561
+ actions = pending.flat_map do |client, future, failure|
562
+ if failure
563
+ failures << failure
564
+ next []
565
+ end
566
+ begin
567
+ result = save_action_await(future, deadline)
568
+ successes += 1
569
+ Array(result).map { |action| [action, client] }
570
+ rescue StandardError => error
571
+ failures << error
572
+ []
573
+ end
574
+ end
575
+ raise failures.first if successes.zero? && !failures.empty?
576
+ actions = deduplicate_client_items(actions)
577
+ raise Error, "document changed while awaiting save code actions" unless buffer.version == version
578
+ raise Error, "invalid code action response" unless actions.all? { |action, _client| action.is_a?(Hash) }
579
+ candidates = actions.reject { |action, _client| action["disabled"] }
580
+ selected = candidates.find { |action, _client| action["kind"] == kind } ||
581
+ candidates.find { |action, _client| action["kind"].is_a?(String) && action["kind"].start_with?("#{kind}.") } ||
582
+ candidates.find { |action, _client| !action.key?("kind") }
583
+ return false unless selected
584
+ action, client = selected
585
+
586
+ provider = client.capabilities["codeActionProvider"]
587
+ if provider.is_a?(Hash) && provider["resolveProvider"] && !action.key?("edit") && !action["command"].is_a?(String)
588
+ resolved = save_action_await(client.resolve_code_action(action), deadline)
589
+ raise Error, "invalid resolved code action" unless resolved.nil? || resolved.is_a?(Hash)
590
+ action = action.merge(resolved || {})
591
+ raise Error, "document changed while resolving save code action" unless buffer.version == version
592
+ end
593
+ if action["edit"]
594
+ result = apply_workspace_edit(action["edit"])
595
+ raise Error, result["failureReason"].to_s unless result["applied"]
596
+ end
597
+ command = action["command"]
598
+ command = action if command.is_a?(String)
599
+ if command
600
+ raise Error, "invalid code action command" unless command.is_a?(Hash)
601
+ @save_action_errors&.delete(client)
602
+ active = @save_action_clients ||= Hash.new(0)
603
+ active[client] += 1
604
+ begin
605
+ request = client.execute_command(command.fetch("command"), arguments: command.fetch("arguments", []))
606
+ save_action_await(request, deadline)
607
+ ensure
608
+ active[client] -= 1
609
+ active.delete(client) if active[client].zero?
610
+ end
611
+ raise Error, @save_action_errors.delete(client) if @save_action_errors&.key?(client)
612
+ end
613
+ true
614
+ ensure
615
+ cancel_language_requests(pending)
616
+ end
617
+ private :save_action_await, :apply_save_format, :run_code_action_on_save
618
+
619
+ def prepare_rename(current = editor)
620
+ self.palette = nil
621
+ invalidate_prepare_rename
622
+ unless current && current.buffer.path
623
+ self.palette = {kind: :rename, query: +"", index: 0, matches: []}
624
+ return true
625
+ end
626
+
627
+ snapshot = rename_snapshot(current)
628
+ language, client = snapshot.values_at(:language, :client)
629
+ unless client || language_server_options(language)
630
+ return open_rename_palette(snapshot, "")
631
+ end
632
+ return open_rename_palette(snapshot, "") if client && !snapshot[:prepare_supported]
633
+
634
+ requests = @prepare_rename_requests ||= {}
635
+ if requests.length >= PREPARE_RENAME_REQUEST_LIMIT
636
+ release_rename_snapshot(snapshot)
637
+ @message = "Too many rename requests"
638
+ return false
639
+ end
640
+ id = @prepare_rename_request_id = @prepare_rename_request_id.to_i + 1
641
+ requests[id] = snapshot
642
+ start_prepare_rename(id, snapshot)
643
+ @message = "Preparing rename…"
644
+ nil
645
+ rescue StandardError => error
646
+ @prepare_rename_requests&.delete_if { |_id, request| request.equal?(snapshot) }
647
+ release_rename_snapshot(snapshot) if snapshot
648
+ @message = error.message
649
+ false
650
+ end
651
+
652
+ def rename_prepared(snapshot, name)
653
+ unless valid_rename_value?(name) && name.bytesize <= RENAME_VALUE_LIMIT
654
+ release_rename_snapshot(snapshot)
655
+ @message = "Invalid rename value"
656
+ return false
657
+ end
658
+ unless rename_snapshot_valid?(snapshot)
659
+ release_rename_snapshot(snapshot)
660
+ @message = "Rename cancelled because the document changed"
661
+ return false
662
+ end
663
+
664
+ requests = @prepare_rename_requests ||= {}
665
+ if requests.length >= PREPARE_RENAME_REQUEST_LIMIT
666
+ release_rename_snapshot(snapshot)
667
+ @message = "Too many rename requests"
668
+ return false
669
+ end
670
+ id = @prepare_rename_request_id = @prepare_rename_request_id.to_i + 1
671
+ requests[id] = snapshot
672
+ start_prepared_rename(id, snapshot, name.dup.freeze)
673
+ @message = "rename…"
674
+ nil
675
+ rescue StandardError => error
676
+ @prepare_rename_requests&.delete_if { |_id, request| request.equal?(snapshot) }
677
+ release_rename_snapshot(snapshot)
678
+ @message = error.message
679
+ false
680
+ end
681
+
682
+ def invalidate_prepare_rename(buffer = nil, client: nil, editor: nil)
683
+ @prepare_rename_requests&.delete_if do |_id, snapshot|
684
+ matches = (!buffer || snapshot[:buffer].equal?(buffer)) && (!editor || snapshot[:editor].equal?(editor)) &&
685
+ (!client || snapshot[:client]&.equal?(client))
686
+ if matches
687
+ snapshot[:future]&.cancel
688
+ release_rename_snapshot(snapshot)
689
+ end
690
+ matches
691
+ end
692
+ snapshot = @palette&.dig(:kind) == :rename && @palette[:rename]
693
+ if snapshot && (!buffer || snapshot[:buffer].equal?(buffer)) && (!editor || snapshot[:editor].equal?(editor)) &&
694
+ (!client || snapshot[:client]&.equal?(client))
695
+ self.palette = nil
696
+ end
697
+ nil
698
+ end
699
+
700
+ def request_completions(current, buffer, offset)
701
+ cancel_completion_requests
702
+ version = buffer.version
703
+ generation = @completion_generation = (@completion_generation || 0) + 1
704
+ context = {editor: current, version: version, generation: generation,
705
+ query: completion_query(buffer, offset), metadata: {}, errors: []}
706
+ job = Thread.new do
707
+ completions = @providers.complete(buffer, offset, context)
708
+ post do
709
+ next unless generation == @completion_generation
710
+ next unless @panes.any? { |pane| pane.editors.include?(current) }
711
+ if buffer.version == version
712
+ display_completions(completions, current, context)
713
+ else
714
+ @message = "Document changed; request completion again"
715
+ end
716
+ end
717
+ rescue StandardError => error
718
+ post { @message = error.message }
719
+ end
720
+ (@completion_jobs ||= []) << job
721
+ (@language_jobs ||= []) << job
722
+ @completion_jobs.reject! { |thread| !thread.alive? }
723
+ @language_jobs.reject! { |thread| !thread.alive? }
724
+ @message = "completion…"
725
+ end
726
+
727
+ def cancel_completion_requests
728
+ @completion_generation = (@completion_generation || 0) + 1
729
+ jobs = @completion_jobs&.dup || []
730
+ jobs.each do |thread|
731
+ thread.kill if thread.alive?
732
+ begin
733
+ thread.join
734
+ rescue StandardError
735
+ nil
736
+ end
737
+ end
738
+ @completion_jobs&.clear
739
+ @language_jobs&.reject! { |thread| jobs.include?(thread) || !thread.alive? }
740
+ end
741
+
742
+ def lsp_completions(buffer, offset, context)
743
+ responses = if context.key?(:lsp_result)
744
+ [[context[:client], context[:lsp_result]]]
745
+ else
746
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10
747
+ clients = language_clients(buffer, feature: "completion",
748
+ timeout: deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC))
749
+ uri = Sadr::Protocol.uri(buffer.path)
750
+ position = Sadr::Protocol.position(buffer.rope, offset)
751
+ pending = clients.map do |client|
752
+ [client, client.completion(uri, position), nil]
753
+ rescue StandardError => error
754
+ [client, nil, error]
755
+ end
756
+ begin
757
+ pending.filter_map do |client, future, failure|
758
+ if failure
759
+ context[:errors] << [:lsp, failure.message.to_s.slice(0, 4_096).freeze] if context[:errors].length < 64
760
+ next
761
+ end
762
+ begin
763
+ [client, language_request_await(future, deadline)]
764
+ rescue StandardError => error
765
+ context[:errors] << [:lsp, error.message.to_s.slice(0, 4_096).freeze] if context[:errors].length < 64
766
+ nil
767
+ end
768
+ end
769
+ ensure
770
+ cancel_language_requests(pending)
771
+ end
772
+ end
773
+ context[:client] = responses.first&.first
774
+ responses.flat_map do |client, result|
775
+ lsp_completion_items(result).map do |item|
776
+ text_edit = item["textEdit"]
777
+ if text_edit
778
+ range = text_edit.is_a?(Hash) && (text_edit["range"] || text_edit["replace"] || text_edit["insert"])
779
+ unless range.is_a?(Hash) && text_edit["newText"].is_a?(String) && text_edit["newText"].valid_encoding?
780
+ raise Error, "invalid completion text edit"
781
+ end
782
+ Sadr::Protocol.offset(buffer.rope, range.fetch("start"))
783
+ Sadr::Protocol.offset(buffer.rope, range.fetch("end"))
784
+ end
785
+ insertion = text_edit && text_edit["newText"] || item["insertText"] || item.fetch("label")
786
+ documentation = item["documentation"]
787
+ documentation = documentation["value"] if documentation.is_a?(Hash)
788
+ additional = item.fetch("additionalTextEdits", []).map do |entry|
789
+ raise Error, "invalid completion additional edit" unless entry.is_a?(Hash)
790
+ range = entry.fetch("range")
791
+ [Sadr::Protocol.offset(buffer.rope, range.fetch("start"))...Sadr::Protocol.offset(buffer.rope, range.fetch("end")), entry.fetch("newText")]
792
+ end
793
+ completion = Provider::Completion.new(item.fetch("label"), insertion, item["kind"], item["detail"], documentation,
794
+ item["sortText"], item["filterText"], additional, :lsp)
795
+ context[:metadata][completion] ||= {item: item, client: client}
796
+ completion
797
+ end
798
+ end
799
+ end
800
+ private :request_completions, :cancel_completion_requests, :lsp_completions
88
801
  def diagnostics_for(buffer)
89
- return [] if !buffer.path || @clients.empty?
90
- uri = LSP::Protocol.uri(buffer.path)
91
- @clients.values.flat_map { |client| client.diagnostics.fetch(uri, []) }
802
+ return [] unless buffer.path
803
+ uri = Sadr::Protocol.uri(buffer.path)
804
+ local = @diagnostics.for_uri(uri).reject { |entry| entry.source == :lsp }.map(&:diagnostic)
805
+ clients = [*(@language_clients || {}).values.flatten, *(@lsp_diagnostics || {}).keys].uniq
806
+ lsp = clients.flat_map do |client|
807
+ next [] unless @opened_lsp_documents&.key?([client, buffer]) &&
808
+ @diagnostic_versions&.dig(client, uri) == buffer.version
809
+ @lsp_diagnostics.fetch(client).fetch(uri, [])
810
+ end.uniq
811
+ local + lsp
812
+ end
813
+
814
+ def document_highlight_decorations(buffer, rows, current)
815
+ return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)
816
+
817
+ client = active_language_client(current.language_document.definition.name, "documentHighlight")
818
+ key = document_highlight_key(client, current, buffer)
819
+ value = (@document_highlight_cache || {})[key]
820
+ value.is_a?(Array) ? value.select { |item| diagnostic_item_visible?(buffer, item, rows) } : []
821
+ end
822
+
823
+ def request_document_highlights(current)
824
+ requests = @document_highlight_requests ||= {}
825
+ requests.delete_if do |editor, entry|
826
+ hidden = @panes.none? { |pane| pane.active.equal?(editor) }
827
+ entry[:future]&.cancel if hidden
828
+ hidden
829
+ end
830
+ buffer = current.buffer
831
+ return false unless buffer.path && !buffer.read_only && @panes.any? { |pane| pane.active.equal?(current) }
832
+
833
+ language = current.language_document.definition.name
834
+ client = active_language_client(language, "documentHighlight")
835
+ key = document_highlight_key(client, current, buffer)
836
+ cache = @document_highlight_cache ||= {}
837
+ return false if cache.key?(key)
838
+
839
+ pending = requests[current]
840
+ if pending && pending[:buffer].equal?(buffer) && pending[:version] == buffer.version &&
841
+ pending[:head] == current.primary.head && (!client || !pending[:client] || pending[:client].equal?(client))
842
+ return false
843
+ end
844
+ invalidate_document_highlights(editor: current) if pending || cache.keys.any? { |entry| entry[1].equal?(current) }
845
+ unless client || language_server_options(language)
846
+ cache_document_highlights(key, false)
847
+ return false
848
+ end
849
+ return false if requests.length >= DOCUMENT_HIGHLIGHT_REQUEST_LIMIT
850
+
851
+ request = {client: client, editor: current, buffer: buffer, version: buffer.version,
852
+ head: current.primary.head, rope: buffer.rope, uri: Sadr::Protocol.uri(buffer.path)}
853
+ requests[current] = request
854
+ @decorations.invalidate(:document_highlight, buffer: buffer)
855
+ @window&.request_frame
856
+ job = Thread.new do
857
+ begin
858
+ owner = language_client(buffer, feature: "documentHighlight")
859
+ request[:client] = owner
860
+ supported = document_highlight_supported?(owner)
861
+ valid = document_highlight_request_valid?(request, owner)
862
+ future = owner.document_highlight(request[:uri], Sadr::Protocol.position(request[:rope], request[:head])) if supported && valid
863
+ request[:future] = future
864
+ result = future.await(timeout: 10) if future && document_highlight_request_valid?(request, owner)
865
+ future&.cancel unless document_highlight_request_valid?(request, owner)
866
+ highlights = supported && valid ? normalize_document_highlights(request[:rope], result) : false
867
+ post do
868
+ next unless @document_highlight_requests&.[](current).equal?(request)
869
+ @document_highlight_requests.delete(current)
870
+ next unless document_highlight_result_valid?(request, owner)
871
+
872
+ cache_document_highlights(document_highlight_key(owner, current, buffer,
873
+ request[:version], request[:head], supported), highlights)
874
+ end
875
+ rescue StandardError => error
876
+ post do
877
+ next unless @document_highlight_requests&.[](current).equal?(request)
878
+ @document_highlight_requests.delete(current)
879
+ next unless owner ? document_highlight_result_valid?(request, owner) : document_highlight_editor_valid?(request)
880
+
881
+ cache_document_highlights(document_highlight_key(owner, current, buffer,
882
+ request[:version], request[:head]), false)
883
+ @message = error.message unless owner && @retired_language_clients&.[](owner)
884
+ end
885
+ ensure
886
+ worker = Thread.current
887
+ post { @language_jobs&.delete(worker) }
888
+ end
889
+ end
890
+ (@language_jobs ||= []) << job
891
+ @language_jobs.reject! { |thread| !thread.alive? }
892
+ true
893
+ end
894
+
895
+ def invalidate_document_highlights(buffer = nil, client: nil, editor: nil)
896
+ buffer ||= editor&.buffer
897
+ @document_highlight_cache&.delete_if do |key, _value|
898
+ (!buffer || key[2].equal?(buffer)) && (!client || key[0]&.equal?(client)) && (!editor || key[1].equal?(editor))
899
+ end
900
+ @document_highlight_requests&.delete_if do |current, entry|
901
+ matches = (!buffer || entry[:buffer].equal?(buffer)) && (!client || entry[:client]&.equal?(client)) &&
902
+ (!editor || current.equal?(editor))
903
+ entry[:future]&.cancel if matches
904
+ matches
905
+ end
906
+ @decorations.invalidate(:document_highlight, buffer: buffer)
907
+ @window&.request_frame unless @closed
908
+ nil
909
+ end
910
+
911
+ def document_link_decorations(buffer, rows, current)
912
+ return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)
913
+
914
+ client = active_language_client(current.language_document.definition.name, "documentLink")
915
+ cache = (@document_link_cache || {})[document_link_key(client, current, buffer)]
916
+ return [] unless cache.is_a?(Hash)
917
+
918
+ cache[:entries].filter_map do |entry|
919
+ next unless diagnostic_item_visible?(buffer, entry[:decoration], rows)
920
+
921
+ entry[:decoration]
922
+ end
923
+ end
924
+
925
+ def request_document_links(current)
926
+ requests = @document_link_requests ||= {}
927
+ requests.delete_if do |editor, request|
928
+ hidden = @panes.none? { |pane| pane.active.equal?(editor) }
929
+ request[:future]&.cancel if hidden
930
+ hidden
931
+ end
932
+ buffer = current.buffer
933
+ return false unless buffer.path && !buffer.read_only && @panes.any? { |pane| pane.active.equal?(current) }
934
+
935
+ language = current.language_document.definition.name
936
+ client = active_language_client(language, "documentLink")
937
+ key = document_link_key(client, current, buffer)
938
+ return false if (@document_link_cache ||= {}).key?(key)
939
+
940
+ pending = requests[current]
941
+ if pending && pending[:buffer].equal?(buffer) && pending[:version] == buffer.version &&
942
+ pending[:document].equal?(current.language_document) &&
943
+ (!client || !pending[:client] || pending[:client].equal?(client)) &&
944
+ (!pending[:client] || pending[:supported] == document_link_supported?(client) &&
945
+ pending[:resolve] == document_link_resolve_supported?(client))
946
+ return false
947
+ end
948
+ invalidate_document_links(editor: current) if pending || @document_link_cache.keys.any? { |entry| entry[1].equal?(current) }
949
+ unless client || language_server_options(language)
950
+ cache_document_links(key, false)
951
+ return false
952
+ end
953
+ return false if requests.length >= DOCUMENT_LINK_REQUEST_LIMIT
954
+
955
+ request = {client: client, editor: current, buffer: buffer, version: buffer.version,
956
+ rope: buffer.rope, uri: Sadr::Protocol.uri(buffer.path), language: language,
957
+ document: current.language_document,
958
+ supported: client && document_link_supported?(client),
959
+ resolve: client && document_link_resolve_supported?(client)}
960
+ requests[current] = request
961
+ @decorations.invalidate(:document_link, buffer: buffer)
962
+ job = Thread.new do
963
+ begin
964
+ owner = language_client(buffer, feature: "documentLink")
965
+ if request[:client]
966
+ next unless request[:client].equal?(owner)
967
+ else
968
+ request[:client] = owner
969
+ request[:supported] = document_link_supported?(owner)
970
+ request[:resolve] = document_link_resolve_supported?(owner)
971
+ end
972
+ supported, resolve = request.values_at(:supported, :resolve)
973
+ valid = document_link_request_valid?(request, owner)
974
+ future = owner.document_link(request[:uri]) if supported && valid
975
+ request[:future] = future
976
+ result = future.await(timeout: 10) if future && document_link_request_valid?(request, owner)
977
+ future&.cancel unless document_link_request_valid?(request, owner)
978
+ links = supported && valid ? normalize_document_links(request[:rope], result, resolve: resolve) : false
979
+ post do
980
+ next unless @document_link_requests&.[](current).equal?(request)
981
+ @document_link_requests.delete(current)
982
+ next unless document_link_result_valid?(request, owner)
983
+
984
+ cache_document_links(document_link_key(owner, current, buffer, request[:version], supported, resolve), links)
985
+ end
986
+ rescue StandardError => error
987
+ post do
988
+ next unless @document_link_requests&.[](current).equal?(request)
989
+ @document_link_requests.delete(current)
990
+ next unless owner ? document_link_result_valid?(request, owner) : document_link_editor_valid?(request)
991
+
992
+ cache_document_links(document_link_key(owner, current, buffer, request[:version]), false)
993
+ @message = error.message unless owner && @retired_language_clients&.[](owner)
994
+ end
995
+ ensure
996
+ worker = Thread.current
997
+ post { @language_jobs&.delete(worker) }
998
+ end
999
+ end
1000
+ (@language_jobs ||= []) << job
1001
+ @language_jobs.reject! { |thread| !thread.alive? }
1002
+ true
1003
+ end
1004
+
1005
+ def invalidate_document_links(buffer = nil, client: nil, editor: nil)
1006
+ buffer ||= editor&.buffer
1007
+ @document_link_cache&.delete_if do |key, cache|
1008
+ matches = (!buffer || key[2].equal?(buffer)) && (!client || key[0]&.equal?(client)) &&
1009
+ (!editor || key[1].equal?(editor))
1010
+ cache[:entries].each { |entry| entry[:future]&.cancel } if matches && cache.is_a?(Hash)
1011
+ matches
1012
+ end
1013
+ @document_link_requests&.delete_if do |current, request|
1014
+ matches = (!buffer || request[:buffer].equal?(buffer)) && (!client || request[:client]&.equal?(client)) &&
1015
+ (!editor || current.equal?(editor))
1016
+ request[:future]&.cancel if matches
1017
+ matches
1018
+ end
1019
+ @decorations.invalidate(:document_link, buffer: buffer)
1020
+ @window&.request_frame unless @closed
1021
+ nil
1022
+ end
1023
+
1024
+ def linked_editing_range(current = editor)
1025
+ invalidate_linked_editing_ranges
1026
+ buffer = current.buffer
1027
+ unless buffer.path && !buffer.read_only
1028
+ @message = "Linked editing is not available here"
1029
+ return false
1030
+ end
1031
+
1032
+ snapshot = linked_editing_snapshot(current)
1033
+ language, client = snapshot.values_at(:language, :client)
1034
+ fallback = linked_editing_markup?(snapshot)
1035
+ unless client || language_server_options(language) || fallback
1036
+ release_linked_editing_snapshot(snapshot)
1037
+ @message = "Linked editing is not available here"
1038
+ return false
1039
+ end
1040
+ if client && !snapshot[:supported] && !fallback
1041
+ release_linked_editing_snapshot(snapshot)
1042
+ @message = "Language server does not support linked editing"
1043
+ return false
1044
+ end
1045
+ requests = @linked_editing_requests ||= {}
1046
+ if requests.length >= LINKED_EDITING_REQUEST_LIMIT
1047
+ release_linked_editing_snapshot(snapshot)
1048
+ @message = "Too many linked editing requests"
1049
+ return false
1050
+ end
1051
+
1052
+ id = @linked_editing_request_id = @linked_editing_request_id.to_i + 1
1053
+ requests[id] = snapshot
1054
+ start_linked_editing_request(id, snapshot)
1055
+ @message = "Loading linked ranges…"
1056
+ nil
1057
+ rescue StandardError => error
1058
+ @linked_editing_requests&.delete_if { |_id, request| request.equal?(snapshot) }
1059
+ release_linked_editing_snapshot(snapshot) if snapshot
1060
+ @message = error.message
1061
+ false
1062
+ end
1063
+
1064
+ def invalidate_linked_editing_ranges(buffer = nil, client: nil, editor: nil)
1065
+ @linked_editing_requests&.delete_if do |_id, snapshot|
1066
+ matches = (!buffer || snapshot[:buffer].equal?(buffer)) && (!client || snapshot[:client]&.equal?(client)) &&
1067
+ (!editor || snapshot[:editor].equal?(editor))
1068
+ if matches
1069
+ snapshot[:future]&.cancel
1070
+ release_linked_editing_snapshot(snapshot)
1071
+ end
1072
+ matches
1073
+ end
1074
+ nil
1075
+ end
1076
+
1077
+ def diagnostic_decorations(buffer, rows)
1078
+ state = [buffer.version, @diagnostic_generation || 0, @settings["diagnostics"]]
1079
+ cached = @diagnostic_decoration_cache&.[](buffer)
1080
+ unless cached && cached.first == state
1081
+ cached = [state, build_diagnostic_decorations(buffer).freeze]
1082
+ (@diagnostic_decoration_cache ||= {})[buffer] = cached
1083
+ end
1084
+ cached.last.select { |item| diagnostic_item_visible?(buffer, item, rows) }
1085
+ end
1086
+
1087
+ def invalidate_diagnostics(buffer = nil)
1088
+ @diagnostic_generation = (@diagnostic_generation || 0) + 1
1089
+ buffer ? @diagnostic_decoration_cache&.delete(buffer) : @diagnostic_decoration_cache&.clear
1090
+ @decorations.invalidate(:diagnostics, buffer: buffer)
92
1091
  end
93
1092
  def resource_workspace_edit?(edit)
94
1093
  edit.is_a?(Hash) && edit["documentChanges"].is_a?(Array) && edit["documentChanges"].any? { |change| change.is_a?(Hash) && change["kind"] }
@@ -102,7 +1101,7 @@ module Canopus
102
1101
  paths = change["kind"] == "rename" ? change.values_at("oldUri", "newUri") : [change["uri"]]
103
1102
  raise Error, "resource URI is too long" unless paths.all? { |uri| uri.is_a?(String) && uri.bytesize <= 16_384 }
104
1103
  suffix = change.dig("options", "overwrite") ? " (overwrite; backup retained)" : ""
105
- "#{change['kind']}: #{paths.map { |uri| LSP::Protocol.path(uri) }.join(' → ')}#{suffix}"
1104
+ "#{change['kind']}: #{paths.map { |uri| Sadr::Protocol.path(uri) }.join(' → ')}#{suffix}"
106
1105
  end
107
1106
  self.palette = {kind: :workspace_edit, query: label.to_s.slice(0, 1_024), index: details.empty? ? 0 : 1, matches: ["Apply changes", "Cancel"],
108
1107
  edit: edit, response: response, on_applied: on_applied, details: details}
@@ -133,7 +1132,7 @@ module Canopus
133
1132
  pending, snapshots, originals = {}, {}, {}
134
1133
  plans = documents.map do |document, edits|
135
1134
  raise Error, "invalid text document edit" unless document.is_a?(Hash) && edits.is_a?(Array)
136
- path = canonical_path(LSP::Protocol.path(document.fetch("uri")))
1135
+ path = canonical_path(Sadr::Protocol.path(document.fetch("uri")))
137
1136
  raise Error, "language server edit is outside project" unless path.start_with?(@root + File::SEPARATOR)
138
1137
  actual = File.exist?(path) ? File.realpath(path) : path
139
1138
  raise Error, "language server edit follows a link outside project" unless actual.start_with?(@root + File::SEPARATOR)
@@ -150,7 +1149,7 @@ module Canopus
150
1149
  raise Error, "invalid LSP text edit"
151
1150
  end
152
1151
  range = entry.fetch("range")
153
- [LSP::Protocol.offset(rope, range.fetch("start"))...LSP::Protocol.offset(rope, range.fetch("end")), entry.fetch("newText")]
1152
+ [Sadr::Protocol.offset(rope, range.fetch("start"))...Sadr::Protocol.offset(rope, range.fetch("end")), entry.fetch("newText")]
154
1153
  end
155
1154
  # Repeated TextDocumentEdits address the preceding staged snapshot.
156
1155
  snapshots[buffer] = [rope.apply_edits(changes), version + (changes.empty? ? 0 : 1)]
@@ -168,6 +1167,50 @@ module Canopus
168
1167
  grouped&.reverse_each(&:end_undo_group)
169
1168
  pending&.each { |path, buffer| buffer.close unless @buffers[path].equal?(buffer) }
170
1169
  end
1170
+
1171
+ def display_completions(completions, current, context)
1172
+ metadata = context.fetch(:metadata)
1173
+ language = current.language_document.definition.name
1174
+ active = route_language_clients(language, language_client_list(language), "completion")
1175
+ completions = completions.reject do |completion|
1176
+ client = metadata[completion]&.fetch(:client)
1177
+ client && !active.include?(client)
1178
+ end
1179
+ return if completions.empty? && context[:client] && !active.include?(context[:client])
1180
+ if completions.empty? && (failure = context.fetch(:errors).first)
1181
+ @message = failure.last
1182
+ self.palette = nil
1183
+ return
1184
+ end
1185
+ self.palette = {kind: :completion, query: +"", index: 0, matches: completions.map(&:label), items: completions,
1186
+ completion_metadata: metadata, completion_generation: context[:generation], editor: current, version: current.buffer.version}
1187
+ update_palette
1188
+ end
1189
+
1190
+ def accept_provider_completion(completion, current)
1191
+ raise Error, "invalid completion" unless completion.is_a?(Provider::Completion)
1192
+ value = completion.insert_text || completion.label
1193
+ snippet = completion.source == :snippet || completion.kind == :snippet
1194
+ variables = current.snippet_variables(workspace_root: @root, clipboard: @window.respond_to?(:clipboard) ? @window.clipboard : nil) if snippet
1195
+ expanded = snippet ? Snippet.new(value, variables: variables).text : value
1196
+ range = current.primary.range
1197
+ additional = completion.additional_edits.map do |entry|
1198
+ raise ArgumentError, "invalid completion additional edit" unless entry.is_a?(Array) && entry.length == 2
1199
+ entry
1200
+ end.sort_by { |edit| edit.first.begin }
1201
+ current.buffer.rope.apply_edits((additional + [[range, expanded]]).sort_by { |edit| edit.first.begin })
1202
+ current.buffer.begin_undo_group
1203
+ begin
1204
+ current.select(range.begin, range.end)
1205
+ current.buffer.edit(additional, kind: :completion) unless additional.empty?
1206
+ snippet ? current.insert_snippet(value, variables: variables) : current.insert_text(value, auto_indent: false, pair: false)
1207
+ ensure
1208
+ current.buffer.end_undo_group
1209
+ end
1210
+ show_snippet_choices(current) if snippet
1211
+ end
1212
+ private :display_completions, :accept_provider_completion
1213
+
171
1214
  def accept_language_result(palette, index)
172
1215
  item = palette[:items][index]
173
1216
  return unless item
@@ -176,7 +1219,10 @@ module Canopus
176
1219
  if current && palette[:version] && current.buffer.version != palette[:version]
177
1220
  raise Error, "Document changed; request #{palette[:kind]} again"
178
1221
  end
179
- client = palette[:client]
1222
+ completion = item if item.is_a?(Provider::Completion)
1223
+ metadata = palette[:completion_metadata]&.[](completion)
1224
+ item = metadata[:item] if metadata
1225
+ client = metadata&.fetch(:client) || palette[:item_clients]&.[](index) || palette[:client]
180
1226
  raise Error, "Language server settings changed; request again" if client && @retired_language_clients&.[](client)
181
1227
  provider, resolver = case palette[:kind]
182
1228
  when :completion then ["completionProvider", :resolve_completion]
@@ -185,11 +1231,12 @@ module Canopus
185
1231
  capability = client&.capabilities&.fetch(provider, nil) if provider && client.respond_to?(:capabilities)
186
1232
  if !palette[:resolved] && capability.is_a?(Hash) && capability["resolveProvider"]
187
1233
  (@language_jobs ||= []) << Thread.new do
188
- resolved = client.public_send(resolver, item).await
1234
+ resolved = client.public_send(resolver, item).await(timeout: 10)
189
1235
  post do
1236
+ next if palette[:completion_generation] && palette[:completion_generation] != @completion_generation
190
1237
  items = palette[:items].dup
191
1238
  items[index] = item.merge(resolved || {})
192
- accept_language_result(palette.merge(items: items, resolved: true), index)
1239
+ accept_language_result(palette.merge(items: items, resolved: true, client: client), index)
193
1240
  end
194
1241
  rescue StandardError => error
195
1242
  post { @message = error.message }
@@ -199,10 +1246,11 @@ module Canopus
199
1246
  case palette[:kind]
200
1247
  when :completion
201
1248
  editor = palette[:editor]
1249
+ return accept_provider_completion(completion, editor) if completion && !metadata
202
1250
  text_edit = item["textEdit"]
203
1251
  range = if text_edit
204
1252
  range = text_edit["range"] || text_edit["replace"] || text_edit.fetch("insert")
205
- LSP::Protocol.offset(editor.buffer.rope, range.fetch("start"))...LSP::Protocol.offset(editor.buffer.rope, range.fetch("end"))
1253
+ Sadr::Protocol.offset(editor.buffer.rope, range.fetch("start"))...Sadr::Protocol.offset(editor.buffer.rope, range.fetch("end"))
206
1254
  else
207
1255
  editor.primary.range
208
1256
  end
@@ -211,34 +1259,30 @@ module Canopus
211
1259
  variables = editor.snippet_variables(workspace_root: @root, clipboard: @window.respond_to?(:clipboard) ? @window.clipboard : nil) if snippet
212
1260
  expanded = snippet ? Snippet.new(value, variables: variables).text : value
213
1261
  additional = item.fetch("additionalTextEdits", []).map do |entry|
214
- [LSP::Protocol.offset(editor.buffer.rope, entry.fetch("range").fetch("start"))...LSP::Protocol.offset(editor.buffer.rope, entry.fetch("range").fetch("end")), entry.fetch("newText")]
1262
+ [Sadr::Protocol.offset(editor.buffer.rope, entry.fetch("range").fetch("start"))...Sadr::Protocol.offset(editor.buffer.rope, entry.fetch("range").fetch("end")), entry.fetch("newText")]
215
1263
  end.sort_by { |entry| entry.first.begin }
216
1264
  editor.buffer.rope.apply_edits((additional + [[range, expanded]]).sort_by { |entry| entry.first.begin })
217
1265
  editor.buffer.begin_undo_group
218
1266
  begin
219
1267
  editor.select(range.begin, range.end)
220
1268
  editor.buffer.edit(additional, kind: :completion) unless additional.empty?
221
- snippet ? editor.insert_snippet(value, variables: variables) : editor.insert_text(value, auto_indent: false)
1269
+ snippet ? editor.insert_snippet(value, variables: variables) : editor.insert_text(value, auto_indent: false, pair: false)
222
1270
  ensure
223
1271
  editor.buffer.end_undo_group
224
1272
  end
225
1273
  show_snippet_choices(editor) if snippet
226
1274
  command = item["command"]
227
1275
  client.execute_command(command.fetch("command"), arguments: command.fetch("arguments", [])) if command && client
228
- when :locations, :symbols
1276
+ when :locations, :symbols, :workspace_symbol_results
229
1277
  location = item["location"] || item
230
- path = LSP::Protocol.path(location["uri"] || location.fetch("targetUri"))
231
- opened = open(path)
232
- range = location["range"] || location.fetch("targetSelectionRange")
233
- opened.select(LSP::Protocol.offset(opened.buffer.rope, range.fetch("start")))
234
- opened.reveal_cursor
1278
+ jump_to_language_location(location)
235
1279
  when :code_actions
236
1280
  raise Error, item["disabled"]["reason"].to_s if item["disabled"]
237
1281
  run_command = lambda do
238
1282
  command = item["command"]
239
1283
  if command
240
1284
  command = item if command.is_a?(String)
241
- palette[:client].execute_command(command.fetch("command"), arguments: command.fetch("arguments", []))
1285
+ client.execute_command(command.fetch("command"), arguments: command.fetch("arguments", []))
242
1286
  end
243
1287
  end
244
1288
  return confirm_workspace_edit(item["edit"], label: item.fetch("title", "Apply code action?"), on_applied: run_command) if resource_workspace_edit?(item["edit"])
@@ -247,16 +1291,12 @@ module Canopus
247
1291
  end
248
1292
  end
249
1293
  def show_diagnostics
250
- items, labels = [], []
251
- @buffers.each_value.uniq.each do |buffer|
252
- next unless buffer.path
253
- diagnostics_for(buffer).each do |diagnostic|
254
- row = diagnostic.dig("range", "start", "line")
255
- next unless row.is_a?(Integer)
256
- labels << "#{File.basename(buffer.path)}:#{row + 1} #{diagnostic['message']}"
257
- items << {"uri" => LSP::Protocol.uri(buffer.path), "range" => diagnostic.fetch("range")}
258
- end
1294
+ entries = @diagnostics.all
1295
+ labels = entries.map do |entry|
1296
+ row = entry.diagnostic.dig("range", "start", "line")
1297
+ "#{File.basename(Sadr::Protocol.path(entry.uri))}:#{row + 1} #{entry.diagnostic['message']}"
259
1298
  end
1299
+ items = entries.map { |entry| {"uri" => entry.uri, "range" => entry.diagnostic.fetch("range")} }
260
1300
  self.palette = {kind: :locations, query: +"", index: 0, matches: labels, items: items}
261
1301
  update_palette
262
1302
  end
@@ -271,75 +1311,2634 @@ module Canopus
271
1311
  language_request(:documentSymbol)
272
1312
  end
273
1313
  end
274
- def fold_current
275
- current, document = editor, editor.language_document
276
- ranges = document.fold_ranges
277
- if document.syntax_ready?
278
- range = ranges.reverse.find { |item| item.cover?(current.primary.head) }
279
- current.display_map.fold(range) if range
280
- else
281
- @pending_fold = [current, document, current.buffer.version, current.primary.head]
282
- @message = "Analyzing fold ranges…"
1314
+ def expand_selection(current = editor)
1315
+ state = @selection_range_states&.[](current)
1316
+ if state && selection_range_state_valid?(current, state)
1317
+ return apply_selection_expansion(current, state)
283
1318
  end
1319
+ invalidate_selection_ranges(editor: current) if state || @pending_selection_ranges&.key?(current)
1320
+
1321
+ selections = current.selections.dup.freeze
1322
+ unless selections.length.between?(1, SELECTION_RANGE_POSITION_LIMIT)
1323
+ @message = "Too many selections to expand"
1324
+ return false
1325
+ end
1326
+ document, buffer, version = current.language_document, current.buffer, current.buffer.version
1327
+ heads = selections.map(&:head).freeze
1328
+ language = document.definition.name
1329
+ client = active_language_client(language, "selectionRange")
1330
+ pending = {document: document, buffer: buffer, version: version, selections: selections,
1331
+ heads: heads, source: :lsp, client: client, supported: selection_range_supported?(client)}
1332
+ unless buffer.path && !buffer.read_only && (client || language_server_options(language))
1333
+ return selection_with_antares(current, pending)
1334
+ end
1335
+ unless selection_range_supported?(client) || !client
1336
+ return selection_with_antares(current, pending)
1337
+ end
1338
+
1339
+ (@pending_selection_ranges ||= {})[current] = pending
1340
+ unless request_selection_ranges(buffer, version, client, heads)
1341
+ @pending_selection_ranges.delete(current)
1342
+ return selection_with_antares(current, pending)
1343
+ end
1344
+ @message = "Loading selection ranges…"
1345
+ nil
1346
+ end
1347
+
1348
+ def shrink_selection(current = editor)
1349
+ state = @selection_range_states&.[](current)
1350
+ unless state && selection_range_state_valid?(current, state) && !state[:history].empty?
1351
+ invalidate_selection_ranges(editor: current)
1352
+ @message = "No smaller selection"
1353
+ return false
1354
+ end
1355
+
1356
+ previous, chains = state[:history].pop
1357
+ current.set_selections(previous, merge: false)
1358
+ state[:current] = previous
1359
+ state[:chains] = chains
1360
+ current.reveal_cursor
1361
+ @message = "Selection shrunk"
1362
+ @window&.request_frame
1363
+ true
1364
+ end
1365
+
1366
+ def fold_current
1367
+ current, document = editor, editor.language_document
1368
+ buffer, version, cursor = current.buffer, current.buffer.version, current.primary.head
1369
+ language = document.definition.name
1370
+ client = active_language_client(language, "foldingRange")
1371
+ key = folding_range_key(client, buffer, version)
1372
+ cache = @folding_range_cache ||= {}
1373
+ return cache[key] == false ? fold_with_antares(current, document, version, cursor) :
1374
+ apply_fold(current, cache[key], cursor) if cache.key?(key)
1375
+
1376
+ unless buffer.path && !buffer.read_only && (client || language_server_options(language))
1377
+ return fold_with_antares(current, document, version, cursor)
1378
+ end
1379
+ unless folding_range_supported?(client) || !client
1380
+ cache_folding_ranges(key, false)
1381
+ return fold_with_antares(current, document, version, cursor)
1382
+ end
1383
+
1384
+ (@pending_folds ||= {})[current] = {document: document, buffer: buffer, version: version,
1385
+ cursor: cursor, source: :lsp, client: client}
1386
+ unless request_folding_ranges(buffer, version, client)
1387
+ @pending_folds.delete(current)
1388
+ return fold_with_antares(current, document, version, cursor)
1389
+ end
1390
+ @message = "Loading fold ranges…"
1391
+ end
1392
+ def language_ready(current, document)
1393
+ if @palette&.dig(:kind) == :outline && @palette[:editor].equal?(current) && @palette[:document].equal?(document)
1394
+ if @palette[:version] != current.buffer.version || (!@palette[:loading] && !document.syntax_ready?)
1395
+ @palette[:version], @palette[:loading] = current.buffer.version, true
1396
+ @palette[:items], @palette[:matches] = [], []
1397
+ @palette.delete(:all_matches)
1398
+ @palette.delete(:search)
1399
+ @palette.delete(:indices)
1400
+ document.request(syntax: true)
1401
+ elsif document.syntax_ready? && (@palette[:loading] || !@palette[:items].equal?(document.outline))
1402
+ symbols = document.outline
1403
+ @palette[:items] = symbols
1404
+ @palette[:matches] = symbols.map { |symbol| "#{' ' * symbol.depth}#{symbol.name}" }
1405
+ @palette.delete(:all_matches)
1406
+ @palette.delete(:search)
1407
+ @palette[:loading], @palette[:partial] = false, !document.syntax_complete?
1408
+ update_palette
1409
+ @message = @palette[:partial] ? "Outline (visible region only)" : "Outline"
1410
+ @window&.request_frame
1411
+ elsif @palette[:loading] && !document.pending?
1412
+ @palette[:loading] = false
1413
+ @message = "Outline analysis unavailable"
1414
+ end
1415
+ end
1416
+ pending = @pending_folds&.[](current)
1417
+ if pending && pending[:source] == :antares
1418
+ unless pending_fold_valid?(current, pending, document)
1419
+ @pending_folds.delete(current)
1420
+ else
1421
+ return if !document.syntax_ready? && document.pending?
1422
+ @pending_folds.delete(current)
1423
+ if document.syntax_ready?
1424
+ apply_fold(current, document.fold_ranges, pending[:cursor])
1425
+ else
1426
+ @message = "Fold analysis unavailable"
1427
+ end
1428
+ end
1429
+ end
1430
+
1431
+ pending = @pending_selection_ranges&.[](current)
1432
+ return unless pending && pending[:source] == :antares
1433
+ unless pending_selection_range_valid?(current, pending, document)
1434
+ @pending_selection_ranges.delete(current)
1435
+ return
1436
+ end
1437
+ chains = document.selection_ranges(pending[:heads])
1438
+ return if !chains && document.pending?
1439
+ @pending_selection_ranges.delete(current)
1440
+ if chains
1441
+ apply_selection_ranges(current, pending,
1442
+ normalize_antares_selection_ranges(pending[:buffer].rope, pending[:heads], chains))
1443
+ else
1444
+ @message = "Selection analysis unavailable"
1445
+ end
1446
+ end
1447
+
1448
+ def invalidate_folding_ranges(buffer = nil, client: nil, editor: nil)
1449
+ affected = []
1450
+ unless editor && !buffer && !client
1451
+ @folding_range_cache&.delete_if do |key, _value|
1452
+ matches = (!buffer || key[1].equal?(buffer)) && (!client || key[0]&.equal?(client))
1453
+ affected << key[1] if matches
1454
+ matches
1455
+ end
1456
+ @folding_range_requests&.delete_if do |_id, request|
1457
+ matches = (!buffer || request[:buffer].equal?(buffer)) && (!client || request[:client]&.equal?(client))
1458
+ if matches
1459
+ affected << request[:buffer]
1460
+ request[:future]&.cancel
1461
+ end
1462
+ matches
1463
+ end
1464
+ end
1465
+ @pending_folds&.delete_if do |current, pending|
1466
+ (!buffer || pending[:buffer].equal?(buffer)) && (!editor || current.equal?(editor)) &&
1467
+ (!client || pending[:client]&.equal?(client) || affected.any? { |item| item.equal?(pending[:buffer]) })
1468
+ end
1469
+ nil
1470
+ end
1471
+
1472
+ def invalidate_selection_ranges(buffer = nil, client: nil, editor: nil)
1473
+ affected = []
1474
+ unless editor && !buffer && !client
1475
+ @selection_range_requests&.delete_if do |_id, request|
1476
+ matches = (!buffer || request[:buffer].equal?(buffer)) && (!client || request[:client]&.equal?(client))
1477
+ if matches
1478
+ affected << request[:buffer]
1479
+ request[:future]&.cancel
1480
+ end
1481
+ matches
1482
+ end
1483
+ end
1484
+ @pending_selection_ranges&.delete_if do |current, pending|
1485
+ (!buffer || pending[:buffer].equal?(buffer)) && (!editor || current.equal?(editor)) &&
1486
+ (!client || pending[:client]&.equal?(client) || affected.any? { |item| item.equal?(pending[:buffer]) })
1487
+ end
1488
+ @selection_range_states&.delete_if do |current, state|
1489
+ (!buffer || state[:buffer].equal?(buffer)) && (!editor || current.equal?(editor)) &&
1490
+ (!client || state[:client]&.equal?(client) || affected.any? { |item| item.equal?(state[:buffer]) })
1491
+ end
1492
+ cancel_unused_selection_range_requests
1493
+ nil
1494
+ end
1495
+
1496
+ def invalidate_hidden_selection_ranges
1497
+ visible = @panes.filter_map(&:active)
1498
+ @pending_selection_ranges&.delete_if do |current, _pending|
1499
+ !visible.include?(current)
1500
+ end
1501
+ cancel_unused_selection_range_requests
1502
+ @prepare_rename_requests&.values&.select { |snapshot| !visible.include?(snapshot[:editor]) }&.each do |snapshot|
1503
+ invalidate_prepare_rename(editor: snapshot[:editor])
1504
+ end
1505
+ snapshot = @palette&.dig(:kind) == :rename && @palette[:rename]
1506
+ invalidate_prepare_rename(editor: snapshot[:editor]) if snapshot && !visible.include?(snapshot[:editor])
1507
+ @document_link_requests&.keys&.reject { |current| visible.include?(current) }&.each do |current|
1508
+ invalidate_document_links(editor: current)
1509
+ end
1510
+ @document_link_cache&.keys&.map { |key| key[1] }&.uniq&.reject { |current| visible.include?(current) }&.each do |current|
1511
+ invalidate_document_links(editor: current)
1512
+ end
1513
+ @linked_editing_requests&.values&.select { |request| !visible.include?(request[:editor]) }&.each do |request|
1514
+ invalidate_linked_editing_ranges(editor: request[:editor])
1515
+ end
1516
+ @hierarchy_prepare_requests&.values&.select { |request| !visible.include?(request[:editor]) }&.each do |request|
1517
+ invalidate_hierarchy(editor: request[:editor])
1518
+ end
1519
+ nil
1520
+ end
1521
+ private :invalidate_hidden_selection_ranges
1522
+
1523
+ private
1524
+
1525
+ def rename_snapshot(current)
1526
+ buffer, offset = current.buffer, current.primary.head
1527
+ language = current.language_document.definition.name
1528
+ client = active_language_client(language, "rename")
1529
+ snapshot = {editor: current, buffer: buffer, version: buffer.version, rope: buffer.rope,
1530
+ selections: current.selections, offset: offset, uri: Sadr::Protocol.uri(buffer.path),
1531
+ position: Sadr::Protocol.position(buffer.rope, offset), language: language,
1532
+ client: client, prepare_supported: prepare_rename_supported?(client)}
1533
+ unless Sadr::Protocol.offset(snapshot[:rope], snapshot[:position]) == offset
1534
+ raise Error, "invalid rename position"
1535
+ end
1536
+ snapshot[:selection_subscription] = current.on_selection do
1537
+ invalidate_prepare_rename(editor: current) unless current.selections == snapshot[:selections]
1538
+ end
1539
+ snapshot[:edit_subscription] = buffer.on_edit { invalidate_prepare_rename(buffer) }
1540
+ snapshot
1541
+ end
1542
+
1543
+ def start_prepare_rename(id, snapshot)
1544
+ job = Thread.new do
1545
+ begin
1546
+ owner = language_client(snapshot[:buffer], feature: "rename")
1547
+ unless @prepare_rename_requests&.[](id).equal?(snapshot) && rename_editor_valid?(snapshot)
1548
+ next
1549
+ end
1550
+ snapshot[:client] = owner
1551
+ snapshot[:prepare_supported] = prepare_rename_supported?(owner)
1552
+ unless snapshot[:prepare_supported]
1553
+ post do
1554
+ next unless take_rename_request(id, snapshot) && rename_snapshot_valid?(snapshot)
1555
+ open_rename_palette(snapshot, "")
1556
+ end
1557
+ next
1558
+ end
1559
+
1560
+ future = owner.prepare_rename(snapshot[:uri], snapshot[:position])
1561
+ snapshot[:future] = future
1562
+ result = future.await(timeout: 10) if prepare_rename_request_valid?(id, snapshot, owner)
1563
+ unless prepare_rename_request_valid?(id, snapshot, owner)
1564
+ future.cancel
1565
+ next
1566
+ end
1567
+ placeholder = normalize_prepare_rename(snapshot[:rope], snapshot[:offset], result)
1568
+ post do
1569
+ next unless take_rename_request(id, snapshot)
1570
+ unless rename_snapshot_valid?(snapshot)
1571
+ release_rename_snapshot(snapshot)
1572
+ next
1573
+ end
1574
+ if placeholder.nil?
1575
+ release_rename_snapshot(snapshot)
1576
+ @message = "Rename is not available here"
1577
+ else
1578
+ open_rename_palette(snapshot, placeholder)
1579
+ end
1580
+ end
1581
+ rescue StandardError => error
1582
+ post do
1583
+ next unless take_rename_request(id, snapshot)
1584
+ valid = rename_editor_valid?(snapshot) && !@retired_language_clients&.[](owner)
1585
+ release_rename_snapshot(snapshot)
1586
+ @message = error.message if valid
1587
+ end
1588
+ ensure
1589
+ worker = Thread.current
1590
+ post do
1591
+ release_rename_snapshot(snapshot) if take_rename_request(id, snapshot)
1592
+ @language_jobs&.delete(worker)
1593
+ end
1594
+ end
1595
+ end
1596
+ (@language_jobs ||= []) << job
1597
+ @language_jobs.reject! { |thread| !thread.alive? }
1598
+ true
1599
+ end
1600
+
1601
+ def start_prepared_rename(id, snapshot, name)
1602
+ job = Thread.new do
1603
+ begin
1604
+ owner = language_client(snapshot[:buffer], feature: "rename")
1605
+ unless snapshot[:client]&.equal?(owner) && prepare_rename_request_valid?(id, snapshot, owner)
1606
+ raise Error, "Rename cancelled because the language server changed"
1607
+ end
1608
+ future = owner.rename(snapshot[:uri], snapshot[:position], name)
1609
+ snapshot[:future] = future
1610
+ result = future.await(timeout: 10) if prepare_rename_request_valid?(id, snapshot, owner)
1611
+ unless prepare_rename_request_valid?(id, snapshot, owner)
1612
+ future.cancel
1613
+ next
1614
+ end
1615
+ post do
1616
+ next unless take_rename_request(id, snapshot)
1617
+ valid = rename_snapshot_valid?(snapshot)
1618
+ release_rename_snapshot(snapshot)
1619
+ display_language_result(:rename, result, owner, snapshot[:editor]) if valid
1620
+ end
1621
+ rescue StandardError => error
1622
+ post do
1623
+ next unless take_rename_request(id, snapshot)
1624
+ valid = rename_editor_valid?(snapshot) && !@retired_language_clients&.[](owner)
1625
+ release_rename_snapshot(snapshot)
1626
+ @message = error.message if valid
1627
+ end
1628
+ ensure
1629
+ worker = Thread.current
1630
+ post { @language_jobs&.delete(worker) }
1631
+ end
1632
+ end
1633
+ (@language_jobs ||= []) << job
1634
+ @language_jobs.reject! { |thread| !thread.alive? }
1635
+ true
1636
+ end
1637
+
1638
+ def prepare_rename_request_valid?(id, snapshot, owner)
1639
+ @prepare_rename_requests&.[](id).equal?(snapshot) && rename_snapshot_valid?(snapshot) &&
1640
+ snapshot[:client].equal?(owner) && @opened_lsp_documents&.key?([owner, snapshot[:buffer]])
1641
+ end
1642
+
1643
+ def rename_snapshot_valid?(snapshot)
1644
+ client = active_language_client(snapshot[:language], "rename")
1645
+ rename_editor_valid?(snapshot) && client.equal?(snapshot[:client]) &&
1646
+ prepare_rename_supported?(client) == snapshot[:prepare_supported]
1647
+ end
1648
+
1649
+ def rename_editor_valid?(snapshot)
1650
+ current, buffer = snapshot.values_at(:editor, :buffer)
1651
+ !@closed && current.buffer.equal?(buffer) && buffer.version == snapshot[:version] &&
1652
+ current.selections == snapshot[:selections] && current.primary.head == snapshot[:offset] &&
1653
+ buffer.path && Sadr::Protocol.uri(buffer.path) == snapshot[:uri] &&
1654
+ @panes.any? { |pane| pane.active.equal?(current) }
1655
+ end
1656
+
1657
+ def take_rename_request(id, snapshot)
1658
+ @prepare_rename_requests&.delete(id).equal?(snapshot)
1659
+ end
1660
+
1661
+ def open_rename_palette(snapshot, placeholder)
1662
+ unless rename_snapshot_valid?(snapshot)
1663
+ release_rename_snapshot(snapshot)
1664
+ return false
1665
+ end
1666
+ self.palette = {kind: :rename, query: +placeholder, index: 0, matches: [], rename: snapshot}
1667
+ true
1668
+ end
1669
+
1670
+ def release_rename_snapshot(snapshot)
1671
+ snapshot&.delete(:selection_subscription)&.detach
1672
+ snapshot&.delete(:edit_subscription)&.detach
1673
+ snapshot&.delete(:future)
1674
+ nil
1675
+ end
1676
+
1677
+ def prepare_rename_supported?(client)
1678
+ provider = client.capabilities["renameProvider"] if client&.respond_to?(:capabilities)
1679
+ provider.is_a?(Hash) && (provider["prepareProvider"] == true || provider[:prepareProvider] == true)
1680
+ end
1681
+
1682
+ def valid_rename_value?(value)
1683
+ value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? && !value.include?("\0")
1684
+ end
1685
+
1686
+ def cancel_unused_selection_range_requests
1687
+ @selection_range_requests&.delete_if do |_id, request|
1688
+ used = (@pending_selection_ranges || {}).values.any? do |pending|
1689
+ pending[:source] == :lsp && pending[:buffer].equal?(request[:buffer]) &&
1690
+ pending[:version] == request[:version] && pending[:heads] == request[:heads] &&
1691
+ (!pending[:client] || !request[:client] || pending[:client].equal?(request[:client]))
1692
+ end
1693
+ request[:future]&.cancel unless used
1694
+ !used
1695
+ end
1696
+ nil
1697
+ end
1698
+
1699
+ def request_selection_ranges(buffer, version, client, heads)
1700
+ requests = @selection_range_requests ||= {}
1701
+ pending = requests.values.any? do |request|
1702
+ request[:buffer].equal?(buffer) && request[:version] == version && request[:heads] == heads &&
1703
+ (!client || !request[:client] || request[:client].equal?(client))
1704
+ end
1705
+ return true if pending
1706
+ return false if requests.length >= SELECTION_RANGE_REQUEST_LIMIT
1707
+
1708
+ @selection_range_request_id = @selection_range_request_id.to_i + 1
1709
+ id = @selection_range_request_id
1710
+ request = {client: client, buffer: buffer, version: version, rope: buffer.rope,
1711
+ heads: heads, uri: Sadr::Protocol.uri(buffer.path)}
1712
+ requests[id] = request
1713
+ job = Thread.new do
1714
+ begin
1715
+ owner = language_client(buffer, feature: "selectionRange")
1716
+ request[:client] = owner
1717
+ supported = selection_range_supported?(owner)
1718
+ valid = selection_range_request_valid?(id, request, owner)
1719
+ positions = request[:heads].map { |offset| Sadr::Protocol.position(request[:rope], offset) }
1720
+ future = owner.selection_range(request[:uri], positions) if supported && valid
1721
+ request[:future] = future
1722
+ result = future.await(timeout: 10) if future && selection_range_request_valid?(id, request, owner)
1723
+ future&.cancel unless selection_range_request_valid?(id, request, owner)
1724
+ chains = supported && valid ? normalize_selection_ranges(request[:rope], request[:heads], result) : false
1725
+ chains = false if chains.nil?
1726
+ post do
1727
+ next unless @selection_range_requests&.delete(id).equal?(request)
1728
+ accepted = selection_range_result_valid?(request, owner) &&
1729
+ selection_range_supported?(owner) == supported
1730
+ finish_pending_selection_ranges(buffer, version, heads, accepted ? chains : false, owner)
1731
+ end
1732
+ rescue StandardError
1733
+ post do
1734
+ next unless @selection_range_requests&.delete(id).equal?(request)
1735
+ finish_pending_selection_ranges(buffer, version, heads, false, owner)
1736
+ end
1737
+ ensure
1738
+ worker = Thread.current
1739
+ post { @language_jobs&.delete(worker) }
1740
+ end
1741
+ end
1742
+ (@language_jobs ||= []) << job
1743
+ @language_jobs.reject! { |thread| !thread.alive? }
1744
+ true
1745
+ end
1746
+
1747
+ def finish_pending_selection_ranges(buffer, version, heads, chains, owner)
1748
+ (@pending_selection_ranges || {}).dup.each do |current, pending|
1749
+ next unless pending[:buffer].equal?(buffer) && pending[:version] == version &&
1750
+ pending[:heads] == heads && pending[:source] == :lsp
1751
+ @pending_selection_ranges.delete(current)
1752
+ next unless pending_selection_range_valid?(current, pending)
1753
+
1754
+ pending[:client] = owner
1755
+ pending[:supported] = selection_range_supported?(owner)
1756
+ if chains == false
1757
+ selection_with_antares(current, pending)
1758
+ else
1759
+ apply_selection_ranges(current, pending, chains)
1760
+ end
1761
+ end
1762
+ end
1763
+
1764
+ def selection_with_antares(current, pending)
1765
+ document = pending[:document]
1766
+ chains = document.selection_ranges(pending[:heads])
1767
+ if chains
1768
+ return apply_selection_ranges(current, pending,
1769
+ normalize_antares_selection_ranges(pending[:buffer].rope, pending[:heads], chains))
1770
+ end
1771
+
1772
+ client = active_language_client(document.definition.name, "selectionRange")
1773
+ pending = pending.merge(source: :antares, client: client,
1774
+ supported: selection_range_supported?(client))
1775
+ (@pending_selection_ranges ||= {})[current] = pending
1776
+ @message = "Analyzing selection ranges…"
1777
+ nil
1778
+ rescue StandardError
1779
+ @pending_selection_ranges&.delete(current)
1780
+ @message = "Selection analysis unavailable"
1781
+ false
1782
+ end
1783
+
1784
+ def apply_selection_ranges(current, pending, chains)
1785
+ return false unless pending_selection_range_valid?(current, pending)
1786
+
1787
+ state = {document: pending[:document], buffer: pending[:buffer], version: pending[:version],
1788
+ client: pending[:client], supported: pending[:supported], chains: chains,
1789
+ current: pending[:selections], history: []}
1790
+ (@selection_range_states ||= {})[current] = state
1791
+ apply_selection_expansion(current, state)
1792
+ end
1793
+
1794
+ def apply_selection_expansion(current, state)
1795
+ pairs = state[:current].zip(state[:chains]).map do |selection, chain|
1796
+ range = chain.find do |candidate|
1797
+ candidate.begin <= selection.start && selection.end <= candidate.end &&
1798
+ (candidate.begin < selection.start || selection.end < candidate.end)
1799
+ end
1800
+ expanded = if !range
1801
+ selection
1802
+ elsif selection.reversed?
1803
+ Selection.new(selection.id, range.end, range.begin, nil)
1804
+ else
1805
+ Selection.new(selection.id, range.begin, range.end, nil)
1806
+ end
1807
+ [expanded, chain]
1808
+ end.each_with_index.sort_by { |(selection, _chain), index| [selection.start, index] }.map!(&:first)
1809
+ expanded = pairs.map(&:first).freeze
1810
+ if expanded == state[:current]
1811
+ @message = "No larger selection"
1812
+ return false
1813
+ end
1814
+
1815
+ state[:history] << [state[:current], state[:chains]].freeze
1816
+ state[:current] = expanded
1817
+ state[:chains] = pairs.map(&:last).freeze
1818
+ current.set_selections(expanded, merge: false)
1819
+ current.reveal_cursor
1820
+ @message = "Selection expanded"
1821
+ @window&.request_frame
1822
+ true
1823
+ end
1824
+
1825
+ def pending_selection_range_valid?(current, pending, document = pending[:document])
1826
+ !@closed && current.language_document.equal?(document) && current.buffer.equal?(pending[:buffer]) &&
1827
+ current.buffer.version == pending[:version] && current.selections == pending[:selections] &&
1828
+ @panes.any? { |pane| pane.active.equal?(current) }
1829
+ end
1830
+
1831
+ def selection_range_state_valid?(current, state)
1832
+ client = active_language_client(current.language_document.definition.name, "selectionRange")
1833
+ !@closed && client.equal?(state[:client]) && selection_range_supported?(client) == state[:supported] &&
1834
+ current.language_document.equal?(state[:document]) && current.buffer.equal?(state[:buffer]) &&
1835
+ current.buffer.version == state[:version] && current.selections == state[:current] &&
1836
+ @panes.any? { |pane| pane.active.equal?(current) }
1837
+ end
1838
+
1839
+ def selection_range_supported?(client)
1840
+ provider = client.capabilities["selectionRangeProvider"] if client&.respond_to?(:capabilities)
1841
+ provider == true || provider.is_a?(Hash)
1842
+ end
1843
+
1844
+ def selection_range_result_valid?(request, owner)
1845
+ selection_range_context_valid?(request) && language_client_active?(owner) &&
1846
+ @opened_lsp_documents&.key?([owner, request[:buffer]])
1847
+ end
1848
+
1849
+ def selection_range_request_valid?(id, request, owner)
1850
+ @selection_range_requests&.[](id).equal?(request) && selection_range_result_valid?(request, owner)
1851
+ end
1852
+
1853
+ def selection_range_context_valid?(request)
1854
+ buffer = request[:buffer]
1855
+ !@closed && buffer.version == request[:version] && buffer.path &&
1856
+ Sadr::Protocol.uri(buffer.path) == request[:uri] &&
1857
+ @panes.any? { |pane| pane.editors.any? { |current| current.buffer.equal?(buffer) } }
1858
+ end
1859
+
1860
+ def request_folding_ranges(buffer, version, client)
1861
+ requests = @folding_range_requests ||= {}
1862
+ pending = requests.values.any? do |request|
1863
+ request[:buffer].equal?(buffer) && request[:version] == version &&
1864
+ (!client || !request[:client] || request[:client].equal?(client))
1865
+ end
1866
+ return true if pending
1867
+ return false if requests.length >= FOLDING_RANGE_REQUEST_LIMIT
1868
+
1869
+ @folding_range_request_id = @folding_range_request_id.to_i + 1
1870
+ id = @folding_range_request_id
1871
+ request = {client: client, buffer: buffer, version: version, rope: buffer.rope,
1872
+ uri: Sadr::Protocol.uri(buffer.path)}
1873
+ requests[id] = request
1874
+ job = Thread.new do
1875
+ begin
1876
+ owner = language_client(buffer, feature: "foldingRange")
1877
+ request[:client] = owner
1878
+ supported = folding_range_supported?(owner)
1879
+ valid = folding_range_request_valid?(id, request, owner)
1880
+ future = owner.folding_range(request[:uri]) if supported && valid
1881
+ request[:future] = future
1882
+ result = future.await(timeout: 10) if future && folding_range_request_valid?(id, request, owner)
1883
+ future&.cancel unless folding_range_request_valid?(id, request, owner)
1884
+ ranges = supported && valid ? normalize_folding_ranges(request[:rope], result) : false
1885
+ ranges = false if ranges.nil?
1886
+ post do
1887
+ next unless @folding_range_requests&.delete(id).equal?(request)
1888
+ accepted = folding_range_result_valid?(request, owner) &&
1889
+ folding_range_supported?(owner) == supported
1890
+ cache_folding_ranges(folding_range_key(owner, buffer, version, supported), ranges) if accepted
1891
+ ranges = false unless accepted
1892
+ finish_pending_folds(buffer, version, ranges)
1893
+ end
1894
+ rescue StandardError
1895
+ post do
1896
+ next unless @folding_range_requests&.delete(id).equal?(request)
1897
+ accepted = owner ? folding_range_result_valid?(request, owner) : fold_context_valid?(request)
1898
+ cache_folding_ranges(folding_range_key(owner, buffer, version), false) if accepted
1899
+ finish_pending_folds(buffer, version, false)
1900
+ end
1901
+ ensure
1902
+ worker = Thread.current
1903
+ post { @language_jobs&.delete(worker) }
1904
+ end
1905
+ end
1906
+ (@language_jobs ||= []) << job
1907
+ @language_jobs.reject! { |thread| !thread.alive? }
1908
+ true
1909
+ end
1910
+
1911
+ def finish_pending_folds(buffer, version, ranges)
1912
+ (@pending_folds || {}).dup.each do |current, pending|
1913
+ next unless pending[:buffer].equal?(buffer) && pending[:version] == version && pending[:source] == :lsp
1914
+ @pending_folds.delete(current)
1915
+ next unless pending_fold_valid?(current, pending)
1916
+
1917
+ if ranges == false
1918
+ fold_with_antares(current, pending[:document], version, pending[:cursor])
1919
+ else
1920
+ apply_fold(current, ranges, pending[:cursor])
1921
+ end
1922
+ end
1923
+ end
1924
+
1925
+ def fold_with_antares(current, document, version, cursor)
1926
+ ranges = document.fold_ranges
1927
+ return apply_fold(current, ranges, cursor) if document.syntax_ready?
1928
+
1929
+ (@pending_folds ||= {})[current] = {document: document, buffer: current.buffer, version: version,
1930
+ cursor: cursor, source: :antares,
1931
+ client: active_language_client(document.definition.name, "foldingRange")}
1932
+ @message = "Analyzing fold ranges…"
1933
+ nil
1934
+ end
1935
+
1936
+ def apply_fold(current, ranges, cursor)
1937
+ row = current.buffer.rope.point_at(cursor).row
1938
+ range = ranges.select do |item|
1939
+ first = current.buffer.rope.point_at(item.begin).row
1940
+ last = current.buffer.rope.point_at(item.end).row
1941
+ row.between?(first, last)
1942
+ end.min_by { |item| [item.end - item.begin, -item.begin] }
1943
+ current.display_map.fold(range) if range
1944
+ @message = range ? "Folded" : "No fold at cursor"
1945
+ @window&.request_frame
1946
+ range
1947
+ end
1948
+
1949
+ def pending_fold_valid?(current, pending, document = pending[:document])
1950
+ !@closed && current.language_document.equal?(document) && current.buffer.equal?(pending[:buffer]) &&
1951
+ current.buffer.version == pending[:version] && current.primary.head == pending[:cursor] &&
1952
+ @panes.any? { |pane| pane.active.equal?(current) }
1953
+ end
1954
+
1955
+ def folding_range_supported?(client)
1956
+ provider = client.capabilities["foldingRangeProvider"] if client&.respond_to?(:capabilities)
1957
+ provider == true || provider.is_a?(Hash)
1958
+ end
1959
+
1960
+ def folding_range_key(client, buffer, version = buffer.version,
1961
+ supported = folding_range_supported?(client))
1962
+ [client, buffer, version, supported]
1963
+ end
1964
+
1965
+ def folding_range_result_valid?(request, owner)
1966
+ fold_context_valid?(request) && language_client_active?(owner) &&
1967
+ @opened_lsp_documents&.key?([owner, request[:buffer]])
1968
+ end
1969
+
1970
+ def folding_range_request_valid?(id, request, owner)
1971
+ @folding_range_requests&.[](id).equal?(request) && folding_range_result_valid?(request, owner)
1972
+ end
1973
+
1974
+ def fold_context_valid?(request)
1975
+ buffer = request[:buffer]
1976
+ !@closed && buffer.version == request[:version] && buffer.path &&
1977
+ Sadr::Protocol.uri(buffer.path) == request[:uri] &&
1978
+ @panes.any? { |pane| pane.editors.any? { |current| current.buffer.equal?(buffer) } }
1979
+ end
1980
+
1981
+ def cache_folding_ranges(key, ranges)
1982
+ cache = @folding_range_cache ||= {}
1983
+ cache.delete_if { |entry, _value| entry[0].equal?(key[0]) && entry[1].equal?(key[1]) }
1984
+ cache.shift while cache.length >= FOLDING_RANGE_REQUEST_LIMIT
1985
+ cache[key] = ranges
1986
+ end
1987
+
1988
+ def cache_inlay_hints(client, buffer, version, rows, result, id, settings)
1989
+ raise Error, "invalid inlay hints" unless result.nil? || result.is_a?(Array)
1990
+ hints = result || []
1991
+ raise Error, "too many inlay hints" if hints.length > 10_000
1992
+
1993
+ items = build_inlay_hint_decorations(buffer, hints, settings).freeze
1994
+ raise Error, "inlay hint is outside its requested range" unless items.all? { |item| rows.cover?(item.row) }
1995
+ cache = @inlay_hint_cache ||= {}
1996
+ cache.delete_if do |key, _|
1997
+ key[0].equal?(client) && key[1].equal?(buffer) && key[2] == version && key[3] == rows.begin && key[4] == rows.end
1998
+ end
1999
+ cache.shift while cache.length >= 64
2000
+ cache[[client, buffer, version, rows.begin, rows.end]] = [id, items].freeze
2001
+ @decorations.invalidate(:inlay_hint, buffer: buffer)
2002
+ @window&.request_frame
2003
+ end
2004
+
2005
+ def build_inlay_hint_decorations(buffer, hints, settings)
2006
+ sequence = 0
2007
+ hints.flat_map do |hint|
2008
+ unless hint.is_a?(Hash) && hint["position"].is_a?(Hash) && [nil, 1, 2].include?(hint["kind"]) &&
2009
+ [nil, true, false].include?(hint["paddingLeft"]) && [nil, true, false].include?(hint["paddingRight"])
2010
+ raise Error, "invalid inlay hint"
2011
+ end
2012
+ next [] if hint["kind"] == 1 && !settings["types"] || hint["kind"] == 2 && !settings["parameter_names"]
2013
+
2014
+ position = Sadr::Protocol.position_value(hint.fetch("position"))
2015
+ offset = Sadr::Protocol.offset(buffer.rope, position)
2016
+ raise Error, "invalid inlay hint position" unless Sadr::Protocol.position(buffer.rope, offset) == position
2017
+ row = buffer.rope.point_at(offset).row
2018
+ parts = truncate_inlay_label(hint.fetch("label"), settings["max_length"]).reject { |label, _location| label.empty? }
2019
+ parts.each_with_index.map do |(label, location), index|
2020
+ validate_inlay_location(location) if location
2021
+ click = location && ->(_editor, _offset) { jump_to_language_location(location) }
2022
+ left = index.zero? && hint["paddingLeft"]
2023
+ right = index == parts.length - 1 && hint["paddingRight"]
2024
+ style = {color: :muted, padding_left: left ? 4 : 0, padding_right: right ? 4 : 0,
2025
+ cells: Zaniah::Unicode.width(label) + (left ? 1 : 0) + (right ? 1 : 0)}
2026
+ sequence += 1
2027
+ Decoration::Item.new(:inline, offset...offset, row, label, style, 20 + sequence, :inlay_hint, click)
2028
+ end
2029
+ rescue KeyError, RangeError, TypeError, Sadr::Error => error
2030
+ raise Error, "invalid inlay hint: #{error.message}"
2031
+ end
2032
+ end
2033
+
2034
+ def truncate_inlay_label(label, maximum)
2035
+ parts = label.is_a?(String) ? [{"value" => label}] : label
2036
+ unless parts.is_a?(Array) && parts.length <= 10_000 &&
2037
+ parts.all? { |part| part.is_a?(Hash) && part["value"].is_a?(String) && part["value"].valid_encoding? }
2038
+ raise Error, "invalid inlay hint label"
2039
+ end
2040
+
2041
+ clusters = []
2042
+ parts.each_with_index do |part, index|
2043
+ part["value"].each_grapheme_cluster do |cluster|
2044
+ clusters << [cluster, part["location"], index]
2045
+ break if clusters.length > maximum
2046
+ end
2047
+ break if clusters.length > maximum
2048
+ end
2049
+ if clusters.length > maximum
2050
+ omitted = clusters[maximum - 1]
2051
+ clusters = clusters.first(maximum - 1)
2052
+ clusters << ["…", omitted[1], omitted[2]]
2053
+ end
2054
+ clusters.chunk_while { |left, right| left[2] == right[2] }
2055
+ .map { |group| [group.map(&:first).join, group.first[1]] }
2056
+ end
2057
+
2058
+ def validate_inlay_location(location)
2059
+ raise Error, "invalid inlay hint location" unless location.is_a?(Hash)
2060
+ Sadr::Protocol.path(location.fetch("uri"))
2061
+ Sadr::Protocol.range_value(location.fetch("range"))
2062
+ rescue KeyError, Sadr::Error
2063
+ raise Error, "invalid inlay hint location"
2064
+ end
2065
+
2066
+ def jump_to_language_location(location)
2067
+ path = Sadr::Protocol.path(location["uri"] || location.fetch("targetUri"))
2068
+ opened = open(path)
2069
+ range = location["range"] || location.fetch("targetSelectionRange")
2070
+ opened.select(Sadr::Protocol.offset(opened.buffer.rope, range.fetch("start")))
2071
+ opened.reveal_cursor
2072
+ end
2073
+
2074
+ def build_diagnostic_decorations(buffer)
2075
+ settings = @settings["diagnostics"]
2076
+ maximum = DIAGNOSTIC_SEVERITIES.fetch(settings["severity"])
2077
+ diagnostics = diagnostics_for(buffer).filter_map do |diagnostic|
2078
+ severity = diagnostic.fetch("severity", 1)
2079
+ next if severity > maximum
2080
+
2081
+ range = diagnostic.fetch("range")
2082
+ first = Sadr::Protocol.offset(buffer.rope, range.fetch("start"))
2083
+ last = Sadr::Protocol.offset(buffer.rope, range.fetch("end"))
2084
+ next if last < first
2085
+
2086
+ row = buffer.rope.point_at(first).row
2087
+ [diagnostic, first...last, row, severity]
2088
+ rescue KeyError, RangeError, TypeError
2089
+ nil
2090
+ end
2091
+ highlights = diagnostics.map do |_diagnostic, range, _row, severity|
2092
+ Decoration::Item.new(:highlight, range, nil, nil,
2093
+ {color: DIAGNOSTIC_COLORS.fetch(severity), underline: :wave}, 5 - severity, :diagnostics, nil)
2094
+ end
2095
+ return highlights unless settings["inline"]
2096
+
2097
+ inlines = diagnostics.group_by { |entry| entry[2] }.map do |row, entries|
2098
+ diagnostic, _range, _row, severity = entries.min_by { |entry| entry[3] }
2099
+ message = diagnostic.fetch("message").encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
2100
+ .gsub(/\s+/, " ").strip
2101
+ message = "#{message} (+#{entries.length - 1})" if entries.length > 1
2102
+ message = truncate_diagnostic_message(message, settings["inline_max_length"])
2103
+ offset = buffer.rope.line_start(row) + buffer.rope.line(row).bytesize
2104
+ Decoration::Item.new(:inline, offset...offset, row, message,
2105
+ {color: DIAGNOSTIC_COLORS.fetch(severity), padding_left: 8}, 10 + severity, :diagnostics, nil)
2106
+ end
2107
+ highlights + inlines
2108
+ end
2109
+
2110
+ def truncate_diagnostic_message(message, maximum)
2111
+ clusters = message.scan(/\X/)
2112
+ clusters.length > maximum ? clusters.first(maximum - 1).join + "…" : message
2113
+ end
2114
+
2115
+ def diagnostic_item_visible?(buffer, item, rows)
2116
+ first = item.row || buffer.rope.point_at(item.range.begin).row
2117
+ last = item.kind == :highlight ? buffer.rope.point_at(item.range.end).row : first
2118
+ last >= rows.begin && first < rows.end
2119
+ rescue RangeError
2120
+ false
2121
+ end
2122
+
2123
+ def accept_diagnostic_notification(client, params)
2124
+ uri = params.fetch("uri")
2125
+ entry = @opened_lsp_documents&.keys&.find do |owner, buffer|
2126
+ owner.equal?(client) && buffer.path && Sadr::Protocol.uri(buffer.path) == uri
2127
+ end
2128
+ return unless entry
2129
+
2130
+ buffer = entry.last
2131
+ version = params["version"]
2132
+ return if version && version != buffer.version
2133
+ language = definition_for(buffer.path).name
2134
+ return unless route_language_clients(language, language_client_list(language), "diagnostics").include?(client)
2135
+
2136
+ diagnostics = snapshot_lsp_diagnostics(params.fetch("diagnostics"))
2137
+ documents = ((@lsp_diagnostics ||= {})[client] ||= {})
2138
+ previous = documents[uri]
2139
+ documents[uri] = diagnostics
2140
+ publish_lsp_diagnostics(uri)
2141
+ (@diagnostic_versions ||= {}).tap { |versions| (versions[client] ||= {})[uri] = buffer.version }
2142
+ invalidate_diagnostics(buffer)
2143
+ @window&.request_frame
2144
+ rescue ArgumentError, KeyError, Sadr::Error
2145
+ if defined?(documents) && documents
2146
+ previous ? documents[uri] = previous : documents.delete(uri)
2147
+ @lsp_diagnostics.delete(client) if documents.empty?
2148
+ end
2149
+ nil
2150
+ end
2151
+
2152
+ def snapshot_lsp_diagnostics(diagnostics)
2153
+ Sadr::Protocol.diagnostics(diagnostics)
2154
+ values = JSON.parse(JSON.generate(diagnostics))
2155
+ freeze_value = lambda do |value|
2156
+ value.each { |key, child| key.freeze; freeze_value.call(child) } if value.is_a?(Hash)
2157
+ value.each { |child| freeze_value.call(child) } if value.is_a?(Array)
2158
+ value.freeze
2159
+ end
2160
+ freeze_value.call(values)
2161
+ rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError
2162
+ raise ArgumentError, "invalid diagnostics"
2163
+ end
2164
+ private :snapshot_lsp_diagnostics
2165
+
2166
+ def cache_code_lenses(client, buffer, version, result, generation)
2167
+ raise Error, "invalid code lenses" unless result.nil? || result.is_a?(Array)
2168
+ lenses = result || []
2169
+ raise Error, "too many code lenses" if lenses.length > 10_000
2170
+
2171
+ entries = lenses.each_with_index.map { |lens, index| validate_code_lens_entry(buffer, lens, index) }.freeze
2172
+ cache = @code_lens_cache ||= {}
2173
+ cache.delete_if { |key, _| key[0].equal?(client) && key[1].equal?(buffer) && key[2] == version }
2174
+ cache.shift while cache.length >= 64
2175
+ cache[[client, buffer, version]] = {generation: generation, entries: entries}
2176
+ @decorations.invalidate(:code_lens, buffer: buffer)
2177
+ @window&.request_frame
2178
+ end
2179
+
2180
+ def validate_code_lens_entry(buffer, value, index)
2181
+ raise Error, "invalid code lens" unless value.is_a?(Hash)
2182
+ encoded = JSON.generate(value)
2183
+ raise Error, "code lens exceeds 1 MiB" if encoded.bytesize > 1 << 20
2184
+ lens = JSON.parse(encoded)
2185
+ range = Sadr::Protocol.range_value(lens.fetch("range"))
2186
+ first = Sadr::Protocol.offset(buffer.rope, range.start)
2187
+ last = Sadr::Protocol.offset(buffer.rope, range.end)
2188
+ unless last >= first && Sadr::Protocol.position(buffer.rope, first) == range.start &&
2189
+ Sadr::Protocol.position(buffer.rope, last) == range.end
2190
+ raise Error, "invalid code lens range"
2191
+ end
2192
+ command = validate_code_lens_command(lens["command"])
2193
+ disabled = lens["disabled"] || command&.[]("disabled")
2194
+ unless disabled.nil? || disabled.is_a?(Hash) && bounded_code_lens_string(disabled["reason"], "disabled reason", empty: true)
2195
+ raise Error, "invalid code lens disabled reason"
2196
+ end
2197
+ {lens: lens.freeze, range: range, row: range.start.line, index: index, command: command,
2198
+ disabled: !!disabled, state: command ? :ready : :unresolved}
2199
+ rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError, KeyError, RangeError, TypeError, Sadr::Error => error
2200
+ raise Error, "invalid code lens: #{error.message}"
2201
+ end
2202
+
2203
+ def validate_code_lens_command(command)
2204
+ return unless command
2205
+ raise Error, "invalid code lens command" unless command.is_a?(Hash)
2206
+ title = bounded_code_lens_string(command["title"], "command title")
2207
+ title = title.gsub(/\s+/, " ").strip
2208
+ raise Error, "invalid code lens command title" if title.empty?
2209
+ title = truncate_diagnostic_message(title, 120).freeze
2210
+ name = bounded_code_lens_string(command["command"], "command name")
2211
+ arguments = command.fetch("arguments", [])
2212
+ raise Error, "too many code lens arguments" unless arguments.is_a?(Array) && arguments.length <= 1_000
2213
+ raise Error, "code lens arguments exceed 1 MiB" if JSON.generate(arguments).bytesize > 1 << 20
2214
+ command.merge("title" => title, "command" => name, "arguments" => arguments.freeze).freeze
2215
+ rescue JSON::GeneratorError, JSON::NestingError => error
2216
+ raise Error, "invalid code lens arguments: #{error.message}"
2217
+ end
2218
+
2219
+ def bounded_code_lens_string(value, name, empty: false)
2220
+ valid = value.is_a?(String) && value.valid_encoding? && value.bytesize <= 4_096 && !value.include?("\0")
2221
+ valid &&= !value.empty? unless empty
2222
+ raise Error, "invalid code lens #{name}" unless valid
2223
+ value.freeze
2224
+ end
2225
+
2226
+ def resolve_code_lens_entry(key, cache, entry)
2227
+ client, buffer, version = key
2228
+ provider = client.capabilities["codeLensProvider"]
2229
+ unless provider.is_a?(Hash) && provider["resolveProvider"]
2230
+ entry[:state] = :unavailable
2231
+ return
2232
+ end
2233
+ entry[:state] = :resolving
2234
+ generation = cache[:generation]
2235
+ job = Thread.new do
2236
+ begin
2237
+ result = client.resolve_code_lens(entry[:lens]).await(timeout: 10)
2238
+ post do
2239
+ current = @code_lens_cache&.[](key)
2240
+ next unless current.equal?(cache) && current[:generation] == generation &&
2241
+ buffer.version == version && language_client_active?(client)
2242
+
2243
+ begin
2244
+ resolved = validate_code_lens_entry(buffer, entry[:lens].merge(result || {}), entry[:index])
2245
+ raise Error, "resolved code lens moved" unless resolved[:range] == entry[:range]
2246
+ entry.replace(resolved)
2247
+ entry[:state] = :unavailable unless entry[:command]
2248
+ @decorations.invalidate(:code_lens, buffer: buffer)
2249
+ @window&.request_frame
2250
+ rescue StandardError => error
2251
+ entry[:state] = :failed
2252
+ @message = error.message
2253
+ @decorations.invalidate(:code_lens, buffer: buffer)
2254
+ @window&.request_frame
2255
+ end
2256
+ end
2257
+ rescue StandardError => error
2258
+ post do
2259
+ current = @code_lens_cache&.[](key)
2260
+ if current.equal?(cache) && current[:generation] == generation
2261
+ entry[:state] = :failed
2262
+ @message = error.message unless @retired_language_clients&.[](client)
2263
+ @decorations.invalidate(:code_lens, buffer: buffer)
2264
+ @window&.request_frame
2265
+ end
2266
+ end
2267
+ ensure
2268
+ worker = Thread.current
2269
+ post { @language_jobs&.delete(worker) }
2270
+ end
2271
+ end
2272
+ (@language_jobs ||= []) << job
2273
+ @language_jobs.reject! { |thread| !thread.alive? }
2274
+ end
2275
+
2276
+ def execute_code_lens(key, generation, command)
2277
+ client, buffer, version = key
2278
+ cache = @code_lens_cache&.[](key)
2279
+ return false unless cache && cache[:generation] == generation &&
2280
+ buffer.version == version && language_client_active?(client) && settings_for_path(buffer.path,
2281
+ language: definition_for(buffer.path).name)["code_lens"]["enabled"]
2282
+
2283
+ request = begin
2284
+ client.execute_command(command.fetch("command"), arguments: command.fetch("arguments", []))
2285
+ rescue StandardError => error
2286
+ post { @message = error.message unless @retired_language_clients&.[](client) }
2287
+ return true
2288
+ end
2289
+ job = Thread.new do
2290
+ request.await(timeout: 10)
2291
+ rescue StandardError => error
2292
+ post { @message = error.message unless @retired_language_clients&.[](client) }
2293
+ ensure
2294
+ worker = Thread.current
2295
+ post { @language_jobs&.delete(worker) }
2296
+ end
2297
+ (@language_jobs ||= []) << job
2298
+ @language_jobs.reject! { |thread| !thread.alive? }
2299
+ true
2300
+ end
2301
+
2302
+ public
2303
+
2304
+ def inlay_hint_decorations(buffer, rows)
2305
+ active = (@language_clients || {}).values.flatten
2306
+ active = @clients.values if active.empty?
2307
+ cache = (@inlay_hint_cache || {}).select do |key, _entry|
2308
+ key[1].equal?(buffer) && key[2] == buffer.version && active.include?(key[0])
2309
+ end
2310
+ covered = []
2311
+ cache.sort_by { |_key, entry| -entry.first }.flat_map do |key, entry|
2312
+ items = entry.last.select { |item| rows.cover?(item.row) && covered.none? { |range| range.cover?(item.row) } }
2313
+ covered << (key[3]...key[4])
2314
+ items
2315
+ end.sort_by { |item| [item.range.begin, item.priority] }.freeze
2316
+ end
2317
+
2318
+ def request_inlay_hints(current, visible_rows, start: false)
2319
+ @language_jobs&.reject! { |thread| !thread.alive? }
2320
+ buffer = current.buffer
2321
+ return false unless buffer.path && !buffer.read_only
2322
+ unless visible_rows.is_a?(Range) && visible_rows.begin.is_a?(Integer) && visible_rows.end.is_a?(Integer)
2323
+ raise ArgumentError, "inlay hint rows must be an integer range"
2324
+ end
2325
+ first = visible_rows.begin.clamp(0, buffer.line_count)
2326
+ last = (visible_rows.exclude_end? ? visible_rows.end : visible_rows.end + 1).clamp(first, buffer.line_count)
2327
+ return false if first == last
2328
+
2329
+ language = current.language_document.definition.name
2330
+ client = active_language_client(language, "inlayHint")
2331
+ return false unless client || start
2332
+ return false if client && !client.capabilities["inlayHintProvider"]
2333
+ settings = settings_for_path(buffer.path, language: language)["inlay_hints"]
2334
+ return false unless settings["enabled"]
2335
+ unless client
2336
+ options = language_server_options(language)
2337
+ attempts = @inlay_hint_start_attempts ||= {}
2338
+ return false if attempts.key?(language) && attempts[language] == options
2339
+ attempts[language] = options
2340
+ return false unless options
2341
+ end
2342
+
2343
+ requested = [first - 50, 0].max...[last + 50, buffer.line_count].min
2344
+ cached = (@inlay_hint_cache || {}).keys.any? do |key|
2345
+ (!client || key[0].equal?(client)) && key[1].equal?(buffer) && key[2] == buffer.version &&
2346
+ key[3] <= first && key[4] >= last
2347
+ end
2348
+ pending = (@inlay_hint_requests || {}).values.any? do |entry|
2349
+ entry[:buffer].equal?(buffer) && entry[:version] == buffer.version && entry[:generation] == @inlay_hint_generation.to_i &&
2350
+ (!client || !entry[:client] || entry[:client].equal?(client)) && entry[:range].begin <= first && entry[:range].end >= last
2351
+ end
2352
+ return false if cached || pending
2353
+
2354
+ @inlay_hint_request_id = @inlay_hint_request_id.to_i + 1
2355
+ id, version, generation = @inlay_hint_request_id, buffer.version, @inlay_hint_generation.to_i
2356
+ rope = buffer.rope
2357
+ byte_range = rope.line_start(requested.begin)...(requested.end == buffer.line_count ? rope.bytesize : rope.line_start(requested.end))
2358
+ protocol_range = Sadr::Protocol.range(rope, byte_range)
2359
+ (@inlay_hint_requests ||= {})[id] = {client: client, buffer: buffer, version: version, generation: generation, range: requested}
2360
+ job = Thread.new do
2361
+ begin
2362
+ owner = language_client(buffer, feature: "inlayHint")
2363
+ supported = owner.capabilities["inlayHintProvider"]
2364
+ valid = buffer.version == version && @inlay_hint_generation.to_i == generation && language_client_active?(owner)
2365
+ result = owner.inlay_hint(Sadr::Protocol.uri(buffer.path), protocol_range).await(timeout: 10) if supported && valid
2366
+ post do
2367
+ @inlay_hint_requests&.delete(id)
2368
+ @language_jobs&.reject! { |thread| !thread.alive? }
2369
+ next unless supported && valid && @inlay_hint_generation.to_i == generation && language_client_active?(owner)
2370
+ next unless buffer.version == version && @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }
2371
+
2372
+ cache_inlay_hints(owner, buffer, version, requested, result, id, settings)
2373
+ end
2374
+ rescue StandardError => error
2375
+ post do
2376
+ @inlay_hint_requests&.delete(id)
2377
+ @language_jobs&.reject! { |thread| !thread.alive? }
2378
+ @message = error.message unless owner && @retired_language_clients&.[](owner)
2379
+ end
2380
+ end
2381
+ end
2382
+ (@language_jobs ||= []) << job
2383
+ @language_jobs.reject! { |thread| !thread.alive? }
2384
+ true
2385
+ end
2386
+
2387
+ def request_visible_inlay_hints(current = editor)
2388
+ @inlay_hint_start_attempts&.delete(current.language_document.definition.name)
2389
+ map = current.display_map
2390
+ first = current.scroll_y.floor.clamp(0, map.row_count - 1)
2391
+ last = [first + current.viewport_rows, map.row_count - 1].min
2392
+ visible_inlay_hint_ranges(current, first...(last + 1)).map do |rows|
2393
+ request_inlay_hints(current, rows, start: true)
2394
+ end.any?
2395
+ end
2396
+
2397
+ def visible_inlay_hint_ranges(current, display_rows)
2398
+ visible_language_ranges(current, display_rows, gap: 101)
2399
+ end
2400
+
2401
+ def invalidate_inlay_hints(buffer = nil, client: nil)
2402
+ @inlay_hint_generation = @inlay_hint_generation.to_i + 1
2403
+ @inlay_hint_cache&.delete_if do |key, _|
2404
+ (!buffer || key[1].equal?(buffer)) && (!client || key[0].equal?(client))
2405
+ end
2406
+ @decorations.invalidate(:inlay_hint, buffer: buffer)
2407
+ @window&.request_frame unless @closed
2408
+ nil
2409
+ end
2410
+
2411
+ def code_lens_decorations(buffer, rows)
2412
+ active = (@language_clients || {}).values.flatten
2413
+ active = @clients.values if active.empty?
2414
+ caches = (@code_lens_cache || {}).select do |key, _cache|
2415
+ key[1].equal?(buffer) && key[2] == buffer.version && active.include?(key[0])
2416
+ end
2417
+ caches.flat_map do |key, cache|
2418
+ cache[:entries].filter_map do |entry|
2419
+ next unless rows.cover?(entry[:row])
2420
+ command = entry[:command]
2421
+ next unless command
2422
+
2423
+ click = unless entry[:disabled]
2424
+ ->(_editor, _offset) { execute_code_lens(key, cache[:generation], command) }
2425
+ end
2426
+ Decoration::Item.new(:block, nil, entry[:row], command.fetch("title"),
2427
+ {position: :above, color: entry[:disabled] ? :muted : :accent},
2428
+ 30 + entry[:index], :code_lens, click)
2429
+ end
2430
+ end.sort_by { |item| [item.row, item.priority] }.freeze
2431
+ end
2432
+
2433
+ private def resolve_visible_code_lenses(key, cache, rows)
2434
+ return unless cache[:entries].any? { |entry| rows.cover?(entry[:row]) && entry[:state] == :unresolved }
2435
+
2436
+ client = key.first
2437
+ resolving = (@code_lens_cache || {}).sum do |candidate, value|
2438
+ candidate.first.equal?(client) ? value[:entries].count { |entry| entry[:state] == :resolving } : 0
2439
+ end
2440
+ cache[:entries].each do |entry|
2441
+ next unless rows.cover?(entry[:row]) && entry[:state] == :unresolved
2442
+ break if resolving >= CODE_LENS_RESOLVE_LIMIT
2443
+
2444
+ resolve_code_lens_entry(key, cache, entry)
2445
+ resolving += 1
2446
+ end
2447
+ end
2448
+
2449
+ def request_code_lenses(current, visible_rows, start: false)
2450
+ @language_jobs&.reject! { |thread| !thread.alive? }
2451
+ buffer = current.buffer
2452
+ return false unless buffer.path && !buffer.read_only
2453
+ unless visible_rows.is_a?(Range) && visible_rows.begin.is_a?(Integer) && visible_rows.end.is_a?(Integer)
2454
+ raise ArgumentError, "code lens rows must be an integer range"
2455
+ end
2456
+ first = visible_rows.begin.clamp(0, buffer.line_count)
2457
+ last = (visible_rows.exclude_end? ? visible_rows.end : visible_rows.end + 1).clamp(first, buffer.line_count)
2458
+ return false if first == last
2459
+
2460
+ language = current.language_document.definition.name
2461
+ client = active_language_client(language, "codeLens")
2462
+ return false unless client || start
2463
+ provider = client&.capabilities&.[]("codeLensProvider")
2464
+ return false if client && provider != true && !provider.is_a?(Hash)
2465
+ return false unless settings_for_path(buffer.path, language: language)["code_lens"]["enabled"]
2466
+ cached = (@code_lens_cache || {}).find do |key, _cache|
2467
+ client && key[0].equal?(client) && key[1].equal?(buffer) && key[2] == buffer.version
2468
+ end
2469
+ generation = @code_lens_generation.to_i
2470
+ pending = (@code_lens_requests || {}).values.any? do |entry|
2471
+ entry[:buffer].equal?(buffer) && entry[:version] == buffer.version &&
2472
+ (!client || !entry[:client] || entry[:client].equal?(client))
2473
+ end
2474
+ if cached
2475
+ resolve_visible_code_lenses(*cached, first...last)
2476
+ return false
2477
+ end
2478
+ return false if pending || (@code_lens_requests || {}).length >= CODE_LENS_REQUEST_LIMIT
2479
+ unless client
2480
+ options = language_server_options(language)
2481
+ attempts = @code_lens_start_attempts ||= {}
2482
+ return false if attempts.key?(language) && attempts[language] == options
2483
+ attempts[language] = options
2484
+ return false unless options
2485
+ end
2486
+
2487
+ @code_lens_request_id = @code_lens_request_id.to_i + 1
2488
+ id, version = @code_lens_request_id, buffer.version
2489
+ request = {client: client, buffer: buffer, version: version, generation: generation}
2490
+ (@code_lens_requests ||= {})[id] = request
2491
+ job = Thread.new do
2492
+ begin
2493
+ owner = language_client(buffer, feature: "codeLens")
2494
+ request[:client] = owner
2495
+ capability = owner.capabilities["codeLensProvider"]
2496
+ supported = capability == true || capability.is_a?(Hash)
2497
+ valid = buffer.version == version && language_client_active?(owner) && @code_lens_requests&.[](id).equal?(request)
2498
+ result = owner.code_lens(Sadr::Protocol.uri(buffer.path)).await(timeout: 10) if supported && valid
2499
+ post do
2500
+ pending = @code_lens_requests&.delete(id)
2501
+ @language_jobs&.reject! { |thread| !thread.alive? }
2502
+ next unless pending.equal?(request) && supported && valid && language_client_active?(owner)
2503
+ next unless buffer.version == version && @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }
2504
+
2505
+ begin
2506
+ cache_code_lenses(owner, buffer, version, result, generation)
2507
+ rescue StandardError => error
2508
+ cache_code_lenses(owner, buffer, version, [], generation)
2509
+ @message = error.message
2510
+ end
2511
+ end
2512
+ rescue StandardError => error
2513
+ post do
2514
+ pending = @code_lens_requests&.delete(id)
2515
+ @language_jobs&.reject! { |thread| !thread.alive? }
2516
+ @code_lens_start_attempts&.delete(language) unless client
2517
+ if pending.equal?(request) && owner && language_client_active?(owner) && buffer.version == version &&
2518
+ @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }
2519
+ cache_code_lenses(owner, buffer, version, [], generation)
2520
+ end
2521
+ @message = error.message if pending.equal?(request) && !(owner && @retired_language_clients&.[](owner))
2522
+ end
2523
+ ensure
2524
+ worker = Thread.current
2525
+ post { @language_jobs&.delete(worker) }
2526
+ end
2527
+ end
2528
+ (@language_jobs ||= []) << job
2529
+ @language_jobs.reject! { |thread| !thread.alive? }
2530
+ true
2531
+ end
2532
+
2533
+ def request_visible_code_lenses(current = editor)
2534
+ @code_lens_start_attempts&.delete(current.language_document.definition.name)
2535
+ map = current.display_map
2536
+ first = current.scroll_y.floor.clamp(0, map.row_count - 1)
2537
+ last = [first + current.viewport_rows, map.row_count - 1].min
2538
+ visible_code_lens_ranges(current, first...(last + 1)).map do |rows|
2539
+ request_code_lenses(current, rows, start: true)
2540
+ end.any?
2541
+ end
2542
+
2543
+ def visible_code_lens_ranges(current, display_rows)
2544
+ visible_language_ranges(current, display_rows, gap: 1)
2545
+ end
2546
+
2547
+ def invalidate_code_lenses(buffer = nil, client: nil)
2548
+ @code_lens_generation = @code_lens_generation.to_i + 1
2549
+ @code_lens_cache&.delete_if do |key, _|
2550
+ (!buffer || key[1].equal?(buffer)) && (!client || key[0].equal?(client))
2551
+ end
2552
+ @code_lens_requests&.delete_if do |_id, entry|
2553
+ (!buffer || entry[:buffer].equal?(buffer)) && (!client || entry[:client]&.equal?(client))
2554
+ end
2555
+ @decorations.invalidate(:code_lens, buffer: buffer)
2556
+ @window&.request_frame unless @closed
2557
+ nil
2558
+ end
2559
+
2560
+ def bracket_decorations(buffer, rows, current)
2561
+ return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)
2562
+ return [] if buffer.rope.respond_to?(:lazy?) && buffer.rope.lazy?
2563
+
2564
+ values = settings_for_editor(current)
2565
+ colorize = values["bracket_colorization"]
2566
+ guides = values["indent_guides"]
2567
+ return [] unless colorize || guides["enabled"]
2568
+
2569
+ items = colorize ? current.language_document.brackets(rows).flat_map do |pair|
2570
+ style = {color: :"bracket.#{pair.depth % 6 + 1}", foreground: true}.freeze
2571
+ [pair.open_row, pair.close_row].zip([pair.open_range, pair.close_range]).filter_map do |row, range|
2572
+ Decoration::Item.new(:highlight, range, row, nil, style, 15 + pair.depth, :bracket, nil) if rows.cover?(row)
2573
+ end
2574
+ end : []
2575
+ return items.freeze unless guides["enabled"]
2576
+
2577
+ active = active_structure_guide(current, guides["active"])
2578
+ first = rows.begin.clamp(0, buffer.line_count)
2579
+ last = (rows.exclude_end? ? rows.end : rows.end + 1).clamp(first, buffer.line_count)
2580
+ guide_count = 0
2581
+ (first...last).each do |row|
2582
+ start = buffer.rope.line_start(row)
2583
+ indentation_offsets(buffer.line(row), current.tab_size, limit: INDENT_GUIDE_LIMIT - guide_count).each do |column, offset|
2584
+ highlighted = active && row > active[:start_line] && row <= active[:end_line] && column == active[:column]
2585
+ style = {guide: true, active: !!highlighted,
2586
+ color: highlighted ? :"indent.guide.active" : :"indent.guide"}.freeze
2587
+ items << Decoration::Item.new(:highlight, (start + offset...start + offset), row, nil,
2588
+ style, highlighted ? 13 : 2, :bracket, nil)
2589
+ guide_count += 1
2590
+ end
2591
+ # ponytail: one viewport never needs more than this; paginate if deeply indented generated files become common.
2592
+ break if guide_count >= INDENT_GUIDE_LIMIT
2593
+ end
2594
+ items.freeze
2595
+ end
2596
+
2597
+ def invalidate_brackets(buffer = nil)
2598
+ @decorations.invalidate(:bracket, buffer: buffer)
2599
+ @window&.request_frame unless @closed
2600
+ nil
2601
+ end
2602
+
2603
+ def sticky_scroll_enabled?(current)
2604
+ language_setting(current, "sticky_scroll", "enabled")
2605
+ end
2606
+
2607
+ def breadcrumbs_enabled?(current)
2608
+ language_setting(current, "breadcrumbs", "enabled")
2609
+ end
2610
+
2611
+ def sticky_context(current, display_row)
2612
+ return [] unless sticky_scroll_enabled?(current)
2613
+
2614
+ map = current.display_map
2615
+ first = display_row.clamp(0, map.row_count - 1)
2616
+ entry = document_symbol_entry(current)
2617
+ return [] unless entry
2618
+ top = map.to_buffer(DisplayPoint.new(first, 0))
2619
+ maximum = language_setting(current, "sticky_scroll", "max_lines")
2620
+ context_key = [entry[:generation], map.tree.object_id, first, top, maximum]
2621
+ contexts = @sticky_context_cache ||= {}
2622
+ return contexts[context_key] if contexts.key?(context_key)
2623
+
2624
+ result = document_symbol_chain(current, top)
2625
+ .select { |symbol| map.to_display(symbol.selection.begin).row < first }
2626
+ .last(maximum).freeze
2627
+ contexts.shift while contexts.length >= 256
2628
+ contexts[context_key] = result
2629
+ end
2630
+
2631
+ # Returns cached symbols containing the byte offset, outermost first. This
2632
+ # never asks a language server or parser to do work and is safe during paint.
2633
+ def document_symbol_chain(current, offset)
2634
+ entry = document_symbol_entry(current)
2635
+ return [] unless entry && offset.is_a?(Integer) && offset.between?(0, current.buffer.rope.bytesize)
2636
+
2637
+ result = []
2638
+ parent_id = nil
2639
+ while (symbol = containing_document_symbol(entry, parent_id, offset))
2640
+ result << symbol
2641
+ parent_id = symbol.id
2642
+ end
2643
+ result.freeze
2644
+ end
2645
+
2646
+ # Breadcrumb navigation consumes the already-indexed sibling list.
2647
+ # Generation invalidates navigation as soon as its cache is replaced.
2648
+ def document_symbol_siblings(current, symbol, generation:)
2649
+ entry = document_symbol_entry(current)
2650
+ return [] unless entry && entry[:generation] == generation && entry[:by_id][symbol.id].equal?(symbol)
2651
+
2652
+ entry[:children][symbol.parent_id] || []
2653
+ end
2654
+
2655
+ def document_symbol_generation(current)
2656
+ document_symbol_entry(current)&.dig(:generation)
2657
+ end
2658
+
2659
+ def breadcrumb_context(current)
2660
+ return unless breadcrumbs_enabled?(current)
2661
+ path = current.buffer.path
2662
+ return unless path
2663
+
2664
+ entry = document_symbol_entry(current)
2665
+ chain = entry ? document_symbol_chain(current, current.primary.head) : []
2666
+ symbols = if chain.any? { |symbol| symbol.kind == :structure }
2667
+ chain.last(2)
2668
+ else
2669
+ container = chain.reverse.find { |symbol| BREADCRUMB_CONTAINER_KINDS.include?(symbol.kind) }
2670
+ callable = chain.reverse.find { |symbol| BREADCRUMB_CALLABLE_KINDS.include?(symbol.kind) }
2671
+ [container, callable].compact.uniq.sort_by(&:depth)
2672
+ end
2673
+ prefix = @root + File::SEPARATOR
2674
+ relative = path.start_with?(prefix) ? path.delete_prefix(prefix) : File.basename(path)
2675
+ project_path = relative if path.start_with?(prefix)
2676
+ items = [{kind: :path, label: relative.freeze, value: project_path&.freeze}.freeze]
2677
+ items.concat(symbols.map { |symbol| {kind: :symbol, label: symbol.name, value: symbol}.freeze })
2678
+ {buffer: current.buffer, document: current.language_document, path: path, version: current.buffer.version,
2679
+ client: active_language_client(current.language_document.definition.name, "documentSymbol"),
2680
+ generation: entry&.dig(:generation), items: items.freeze}.freeze
2681
+ end
2682
+
2683
+ def minimap_settings(current)
2684
+ @settings["minimap"].merge(
2685
+ @settings["languages"].fetch(current.language_document.definition.name, {}).fetch("minimap", {})
2686
+ )
2687
+ end
2688
+
2689
+ def show_breadcrumb_menu(pane, current, item, context)
2690
+ return false unless breadcrumb_context_valid?(pane, current, context) && context[:items].any? { |entry| entry.equal?(item) }
2691
+
2692
+ activate_tab(pane, current)
2693
+ if item[:kind] == :path
2694
+ source = item[:value]
2695
+ return false unless source
2696
+ directory = File.dirname(source)
2697
+ values = files.select { |path| File.dirname(path) == directory }
2698
+ labels = values
2699
+ else
2700
+ source = item[:value]
2701
+ values = document_symbol_siblings(current, source, generation: context[:generation])
2702
+ .select { |symbol| breadcrumb_symbol_category(symbol) == breadcrumb_symbol_category(source) }
2703
+ labels = breadcrumb_symbol_labels(current.buffer, values)
2704
+ end
2705
+ return false if values.empty?
2706
+
2707
+ self.palette = {kind: :breadcrumbs, query: +"", index: 0, matches: labels, items: values,
2708
+ breadcrumb_kind: item[:kind], breadcrumb_source: item, breadcrumb_context: context,
2709
+ breadcrumb_editor: current, breadcrumb_pane: pane}
2710
+ update_palette
2711
+ true
2712
+ end
2713
+
2714
+ def refresh_sticky_fallback(current, document = current.language_document)
2715
+ return [] unless (sticky_scroll_enabled?(current) || breadcrumbs_enabled?(current)) &&
2716
+ document.equal?(current.language_document) && document.syntax_ready?
2717
+
2718
+ buffer = current.buffer
2719
+ regions = document.structure_regions
2720
+ cached = @sticky_fallback_cache&.[](current)
2721
+ return cached.last[:symbols] if cached && cached[0] == buffer.version && cached[1].equal?(document) && cached[2].equal?(regions)
2722
+
2723
+ rope = buffer.rope
2724
+ drafts = regions.first(STICKY_SYMBOL_LIMIT).filter_map do |region|
2725
+ first, last, kind = region.values_at(:start_line, :end_line, :kind)
2726
+ next unless %i[block region].include?(kind) && first.is_a?(Integer) && last.is_a?(Integer) &&
2727
+ first >= 0 && last > first && last < buffer.line_count
2728
+
2729
+ start = rope.line_start(first)
2730
+ finish = last + 1 < buffer.line_count ? rope.line_start(last + 1) : rope.bytesize
2731
+ label = sticky_source_label(rope, first)
2732
+ label = "Line #{first + 1}" if label.empty?
2733
+ [truncate_diagnostic_message(label, 256).freeze, (start...finish).freeze, (start...start).freeze]
2734
+ end.uniq { |_name, range, _selection| [range.begin, range.end] }
2735
+ .sort_by { |_name, range, _selection| [range.begin, -range.end] }
2736
+ parents = []
2737
+ values = drafts.each_with_index.map do |(name, range, selection), id|
2738
+ parents.pop until parents.empty? || strictly_contains?(parents.last.range, range)
2739
+ parent = parents.last
2740
+ symbol = Language::DocumentSymbol.new(id, name, :structure, range, selection,
2741
+ parent ? parent.depth + 1 : 0, parent&.id)
2742
+ parents << symbol
2743
+ symbol
2744
+ end.freeze
2745
+ entry = sticky_cache_entry(buffer, buffer.version, nil, values)
2746
+ @sticky_fallback_cache ||= {}
2747
+ @sticky_fallback_cache[current] = [buffer.version, document, regions, entry]
2748
+ @sticky_context_cache&.clear
2749
+ values
2750
+ end
2751
+
2752
+ def request_sticky_symbols(current)
2753
+ return false unless sticky_scroll_enabled?(current) || breadcrumbs_enabled?(current)
2754
+
2755
+ buffer = current.buffer
2756
+ return false unless buffer.path && !buffer.read_only
2757
+ language = current.language_document.definition.name
2758
+ client = active_language_client(language, "documentSymbol")
2759
+ cache = @sticky_symbol_cache || {}
2760
+ return false if client && cache.key?([client, buffer, buffer.version])
2761
+ return false if client && !client.capabilities["documentSymbolProvider"]
2762
+
2763
+ requests = @sticky_symbol_requests ||= {}
2764
+ return false if requests.values.any? { |entry| entry[:buffer].equal?(buffer) && entry[:version] == buffer.version }
2765
+ return false if requests.length >= STICKY_REQUEST_LIMIT
2766
+ unless client
2767
+ options = language_server_options(language)
2768
+ attempts = @sticky_symbol_start_attempts ||= {}
2769
+ return false if attempts.key?(language) && attempts[language] == options
2770
+ attempts[language] = options
2771
+ return false unless options
2772
+ end
2773
+
2774
+ id = @sticky_symbol_request_id = @sticky_symbol_request_id.to_i + 1
2775
+ request = {client: client, buffer: buffer, version: buffer.version,
2776
+ rope: buffer.rope, uri: Sadr::Protocol.uri(buffer.path)}
2777
+ requests[id] = request
2778
+ job = Thread.new do
2779
+ begin
2780
+ owner = language_client(buffer, feature: "documentSymbol")
2781
+ request[:client] = owner
2782
+ supported = !!owner.capabilities["documentSymbolProvider"]
2783
+ active = @sticky_symbol_requests&.[](id).equal?(request) && buffer.version == request[:version] && language_client_active?(owner)
2784
+ future = owner.document_symbol(request[:uri]) if supported && active
2785
+ request[:future] = future
2786
+ if future && @sticky_symbol_requests&.[](id).equal?(request)
2787
+ symbols = normalize_document_symbols(request[:rope], future.await(timeout: 10), request[:uri])
2788
+ else
2789
+ future&.cancel
2790
+ symbols = false
2791
+ end
2792
+ post do
2793
+ pending = @sticky_symbol_requests&.delete(id)
2794
+ next unless pending.equal?(request) && buffer.version == request[:version] && language_client_active?(owner) &&
2795
+ buffer.path && Sadr::Protocol.uri(buffer.path) == request[:uri] &&
2796
+ @opened_lsp_documents&.key?([owner, buffer]) &&
2797
+ @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }
2798
+
2799
+ cache_sticky_symbols(owner, buffer, request[:version], symbols)
2800
+ end
2801
+ rescue StandardError => error
2802
+ post do
2803
+ pending = @sticky_symbol_requests&.delete(id)
2804
+ if pending.equal?(request) && owner && buffer.version == request[:version] && language_client_active?(owner) &&
2805
+ buffer.path && Sadr::Protocol.uri(buffer.path) == request[:uri] && @opened_lsp_documents&.key?([owner, buffer])
2806
+ cache_sticky_symbols(owner, buffer, request[:version], false)
2807
+ @message = error.message unless @retired_language_clients&.[](owner)
2808
+ end
2809
+ end
2810
+ ensure
2811
+ worker = Thread.current
2812
+ post { @language_jobs&.delete(worker) }
2813
+ end
2814
+ end
2815
+ (@language_jobs ||= []) << job
2816
+ @language_jobs.reject! { |thread| !thread.alive? }
2817
+ true
2818
+ end
2819
+
2820
+ def invalidate_sticky_symbols(buffer = nil, client: nil)
2821
+ @sticky_symbol_cache&.delete_if do |key, _|
2822
+ (!buffer || key[1].equal?(buffer)) && (!client || key[0].equal?(client))
2823
+ end
2824
+ @sticky_symbol_requests&.delete_if do |_id, entry|
2825
+ matches = (!buffer || entry[:buffer].equal?(buffer)) && (!client || entry[:client]&.equal?(client))
2826
+ entry[:future]&.cancel if matches
2827
+ matches
2828
+ end
2829
+ @sticky_fallback_cache&.delete_if { |editor, _| editor.buffer.equal?(buffer) } if buffer
2830
+ @sticky_fallback_cache&.clear unless buffer || client
2831
+ @sticky_context_cache&.clear
2832
+ @sticky_symbol_start_attempts&.clear unless buffer || client
2833
+ @window&.request_frame unless @closed
2834
+ nil
2835
+ end
2836
+
2837
+ private
2838
+
2839
+ def containing_document_symbol(entry, parent_id, offset)
2840
+ siblings = entry[:children][parent_id] || []
2841
+ index = siblings.bsearch_index { |candidate| candidate.range.begin > offset } || siblings.length
2842
+ candidate = siblings[index - 1] if index.positive?
2843
+ candidate if candidate && offset < candidate.range.end
2844
+ end
2845
+
2846
+ def breadcrumb_context_valid?(pane, current, context)
2847
+ return false if @closed || !context.is_a?(Hash) || !@panes.include?(pane) || !pane.active.equal?(current)
2848
+ return false unless context[:buffer].equal?(current.buffer) && context[:document].equal?(current.language_document) &&
2849
+ context[:path] == current.buffer.path && context[:version] == current.buffer.version
2850
+
2851
+ entry = document_symbol_entry(current)
2852
+ active_client = active_language_client(current.language_document.definition.name, "documentSymbol")
2853
+ return false unless context[:client].equal?(active_client) && context[:generation] == entry&.dig(:generation)
2854
+ client = context[:client]
2855
+ !client || language_client_active?(client) && client.running? && @opened_lsp_documents&.key?([client, current.buffer])
2856
+ end
2857
+
2858
+ def breadcrumb_symbol_category(symbol)
2859
+ return :container if BREADCRUMB_CONTAINER_KINDS.include?(symbol.kind)
2860
+ return :callable if BREADCRUMB_CALLABLE_KINDS.include?(symbol.kind)
2861
+
2862
+ symbol.kind
2863
+ end
2864
+
2865
+ def breadcrumb_symbol_labels(buffer, symbols)
2866
+ labels = symbols.map do |symbol|
2867
+ point = buffer.rope.point_at(symbol.selection.begin)
2868
+ "#{symbol.name} — #{point.row + 1}:#{point.column + 1}"
2869
+ end
2870
+ duplicates = labels.tally
2871
+ labels.each_with_index.map { |label, index| duplicates[label] > 1 ? "#{label} ##{symbols[index].id}" : label }.freeze
2872
+ end
2873
+
2874
+ def accept_breadcrumb_palette(state, index)
2875
+ item = index && state[:items][index]
2876
+ context = state[:breadcrumb_context]
2877
+ current = state[:breadcrumb_editor]
2878
+ pane = state[:breadcrumb_pane]
2879
+ return false unless item && breadcrumb_context_valid?(pane, current, context)
2880
+
2881
+ if state[:breadcrumb_kind] == :path
2882
+ source = state.dig(:breadcrumb_source, :value)
2883
+ return false unless item.is_a?(String) && source && File.dirname(item) == File.dirname(source)
2884
+ absolute = @project.path(item)
2885
+ return false unless File.file?(absolute)
2886
+ activate_tab(pane, current)
2887
+ open(item)
2888
+ else
2889
+ source = state.dig(:breadcrumb_source, :value)
2890
+ siblings = document_symbol_siblings(current, source, generation: context[:generation])
2891
+ return false unless item.is_a?(Language::DocumentSymbol) && siblings.any? { |symbol| symbol.equal?(item) } &&
2892
+ breadcrumb_symbol_category(item) == breadcrumb_symbol_category(source)
2893
+ activate_tab(pane, current)
2894
+ current.select(item.selection.begin)
2895
+ current.reveal_cursor
2896
+ end
2897
+ true
2898
+ end
2899
+
2900
+ def document_symbol_entry(current)
2901
+ buffer = current.buffer
2902
+ client = active_language_client(current.language_document.definition.name, "documentSymbol")
2903
+ key = [client, buffer, buffer.version]
2904
+ cache = @sticky_symbol_cache || {}
2905
+ return cache[key] if client && cache.key?(key) && cache[key]
2906
+
2907
+ fallback = @sticky_fallback_cache&.[](current)
2908
+ fallback.last if fallback && fallback[0] == buffer.version && fallback[1].equal?(current.language_document)
2909
+ end
2910
+
2911
+ def language_setting(current, group, key)
2912
+ values = settings_for_editor(current)
2913
+ defaults = values[group]
2914
+ override = values["languages"].fetch(current.language_document.definition.name, {}).fetch(group, {})
2915
+ override.fetch(key, defaults.fetch(key))
2916
+ end
2917
+
2918
+ def sticky_source_label(rope, row)
2919
+ first = rope.line_start(row)
2920
+ last = row + 1 < rope.line_count ? rope.line_start(row + 1) : rope.bytesize
2921
+ ending = [last, first + 4_096].min
2922
+ begin
2923
+ rope.point_at(ending)
2924
+ rescue RangeError
2925
+ ending -= 1
2926
+ retry
2927
+ end
2928
+ rope.byteslice(first, ending - first).to_s
2929
+ .sub(/(?:\r\n|[\r\n\u2028\u2029])\z/, "").strip
2930
+ end
2931
+
2932
+ def normalize_document_symbols(rope, result, uri)
2933
+ raise Error, "invalid document symbols" unless result.nil? || result.is_a?(Array)
2934
+ raise Error, "too many document symbols" if result && result.length > STICKY_SYMBOL_LIMIT
2935
+ items = Array(result)
2936
+ raise Error, "invalid document symbol" if items.any? { |item| !item.is_a?(Hash) }
2937
+ flat = items.first&.key?("location")
2938
+ if items.any? { |item| item.key?("location") != flat }
2939
+ raise Error, "invalid mixed document symbols"
2940
+ end
2941
+ return normalize_flat_document_symbols(rope, items, uri) if flat
2942
+
2943
+ stack = items.reverse.map { |item| [item, 0, nil] }
2944
+ symbols, seen = [], {}.compare_by_identity
2945
+ until stack.empty?
2946
+ item, depth, parent_id = stack.pop
2947
+ raise Error, "too many document symbols" if symbols.length >= STICKY_SYMBOL_LIMIT
2948
+ raise Error, "document symbol nesting exceeds #{STICKY_DEPTH_LIMIT}" if depth > STICKY_DEPTH_LIMIT
2949
+ raise Error, "invalid document symbol" unless item.is_a?(Hash)
2950
+ raise Error, "cyclic document symbols" if seen.key?(item)
2951
+ seen[item] = true
2952
+
2953
+ name = bounded_sticky_string(item["name"], "name")
2954
+ kind = sticky_symbol_kind(item["kind"])
2955
+ bounded_sticky_string(item["detail"], "detail", optional: true)
2956
+ range = strict_symbol_range(rope, item["range"])
2957
+ selection = strict_symbol_range(rope, item["selectionRange"])
2958
+ unless range.begin <= selection.begin && selection.end <= range.end
2959
+ raise Error, "document symbol selection is outside its range"
2960
+ end
2961
+ parent = symbols[parent_id] if parent_id
2962
+ unless !parent || parent.range.begin <= range.begin && range.end <= parent.range.end
2963
+ raise Error, "document symbol is outside its parent"
2964
+ end
2965
+ id = symbols.length
2966
+ symbols << Language::DocumentSymbol.new(id, name, kind, range, selection, depth, parent_id)
2967
+ children = item.fetch("children", [])
2968
+ raise Error, "invalid document symbol children" unless children.is_a?(Array)
2969
+ raise Error, "too many document symbols" if symbols.length + stack.length + children.length > STICKY_SYMBOL_LIMIT
2970
+ children.reverse_each { |child| stack << [child, depth + 1, id] }
2971
+ end
2972
+ validate_document_symbol_siblings!(symbols)
2973
+ rescue KeyError, RangeError, TypeError, ArgumentError, Sadr::Error => error
2974
+ raise Error, "invalid document symbols: #{error.message}"
2975
+ end
2976
+
2977
+ def normalize_flat_document_symbols(rope, items, uri)
2978
+ drafts = items.each_with_index.map do |item, index|
2979
+ lookup_name = bounded_sticky_string(item["name"], "name", truncate: false)
2980
+ name = truncate_diagnostic_message(lookup_name, 256).freeze
2981
+ kind = sticky_symbol_kind(item["kind"])
2982
+ container = bounded_sticky_string(item["containerName"], "container name", optional: true, truncate: false)
2983
+ location = item["location"]
2984
+ raise Error, "invalid document symbol location" unless location.is_a?(Hash)
2985
+ bounded_sticky_string(location["uri"], "URI", maximum: 16_384)
2986
+ raise Error, "document symbol belongs to another document" unless location["uri"] == uri
2987
+ range = strict_symbol_range(rope, location["range"])
2988
+ {name: name, lookup_name: lookup_name, kind: kind, selection: range, container: container, index: index}
2989
+ end.sort_by { |draft| [draft[:selection].begin, -draft[:selection].end, draft[:index]] }
2990
+
2991
+ parents, seen = [], Hash.new { |hash, name| hash[name] = [] }
2992
+ drafts.each_with_index do |draft, id|
2993
+ parents.pop until parents.empty? || strictly_contains?(drafts[parents.last][:selection], draft[:selection])
2994
+ parent_id = draft[:container] && seen[draft[:container]].last
2995
+ parent_id ||= parents.last
2996
+ draft[:id], draft[:parent_id] = id, parent_id
2997
+ draft[:depth] = parent_id ? drafts[parent_id][:depth] + 1 : 0
2998
+ raise Error, "document symbol nesting exceeds #{STICKY_DEPTH_LIMIT}" if draft[:depth] > STICKY_DEPTH_LIMIT
2999
+ parents << id
3000
+ seen[draft[:lookup_name]] << id
3001
+ end
3002
+
3003
+ following = []
3004
+ scopes = Array.new(drafts.length)
3005
+ (drafts.length - 1).downto(0) do |id|
3006
+ draft = drafts[id]
3007
+ following.pop while following.any? && drafts[following.last][:depth] > draft[:depth]
3008
+ finish = following.empty? ? rope.bytesize : drafts[following.last][:selection].begin
3009
+ finish = draft[:selection].end unless finish > draft[:selection].begin
3010
+ scopes[id] = (draft[:selection].begin...finish).freeze
3011
+ following << id
3012
+ end
3013
+ symbols = drafts.map do |draft|
3014
+ Language::DocumentSymbol.new(draft[:id], draft[:name], draft[:kind], scopes[draft[:id]], draft[:selection],
3015
+ draft[:depth], draft[:parent_id])
3016
+ end
3017
+ validate_document_symbol_siblings!(symbols)
3018
+ end
3019
+
3020
+ def sticky_symbol_kind(value)
3021
+ raise Error, "invalid document symbol kind" unless value.is_a?(Integer) && value.between?(1, 26)
3022
+
3023
+ value
3024
+ end
3025
+
3026
+ def strictly_contains?(outer, inner)
3027
+ outer.begin <= inner.begin && inner.end <= outer.end && outer != inner
3028
+ end
3029
+
3030
+ def validate_document_symbol_siblings!(symbols)
3031
+ by_id = symbols.to_h { |symbol| [symbol.id, symbol] }
3032
+ symbols.each do |symbol|
3033
+ parent = by_id[symbol.parent_id]
3034
+ next unless parent
3035
+ unless parent.range.begin <= symbol.range.begin && symbol.range.end <= parent.range.end
3036
+ raise Error, "document symbol is outside its parent"
3037
+ end
3038
+ end
3039
+ symbols.group_by(&:parent_id).each_value do |siblings|
3040
+ siblings.sort_by { |symbol| [symbol.range.begin, symbol.range.end, symbol.id] }.each_cons(2) do |left, right|
3041
+ raise Error, "overlapping document symbol siblings" if left.range.end > right.range.begin
3042
+ end
3043
+ end
3044
+ symbols.freeze
284
3045
  end
285
- def language_ready(current, document)
286
- if @palette&.dig(:kind) == :outline && @palette[:editor].equal?(current) && @palette[:document].equal?(document)
287
- if @palette[:version] != current.buffer.version || (!@palette[:loading] && !document.syntax_ready?)
288
- @palette[:version], @palette[:loading] = current.buffer.version, true
289
- @palette[:items], @palette[:matches] = [], []
290
- @palette.delete(:all_matches)
291
- @palette.delete(:search)
292
- @palette.delete(:indices)
293
- document.request(syntax: true)
294
- elsif document.syntax_ready? && (@palette[:loading] || !@palette[:items].equal?(document.outline))
295
- symbols = document.outline
296
- @palette[:items] = symbols
297
- @palette[:matches] = symbols.map { |symbol| "#{' ' * symbol.depth}#{symbol.name}" }
298
- @palette.delete(:all_matches)
299
- @palette.delete(:search)
300
- @palette[:loading], @palette[:partial] = false, !document.syntax_complete?
301
- update_palette
302
- @message = @palette[:partial] ? "Outline (visible region only)" : "Outline"
303
- @window&.request_frame
304
- elsif @palette[:loading] && !document.pending?
305
- @palette[:loading] = false
306
- @message = "Outline analysis unavailable"
3046
+
3047
+ def bounded_sticky_string(value, name, optional: false, maximum: 4_096, truncate: true)
3048
+ return if optional && value.nil?
3049
+ unless value.is_a?(String) && value.valid_encoding? && value.bytesize.between?(1, maximum) && !value.include?("\0")
3050
+ raise Error, "invalid document symbol #{name}"
3051
+ end
3052
+ value = value.dup.freeze
3053
+ truncate ? truncate_diagnostic_message(value, 256).freeze : value
3054
+ end
3055
+
3056
+ def strict_symbol_range(rope, value)
3057
+ strict_language_range(rope, value, "document symbol")
3058
+ end
3059
+
3060
+ def strict_language_range(rope, value, label, allow_empty: true)
3061
+ range = Sadr::Protocol.range_value(value)
3062
+ first = Sadr::Protocol.offset(rope, range.start)
3063
+ last = Sadr::Protocol.offset(rope, range.end)
3064
+ unless last >= first && (allow_empty || last > first) && Sadr::Protocol.position(rope, first) == range.start &&
3065
+ Sadr::Protocol.position(rope, last) == range.end
3066
+ raise Error, "invalid #{label} range"
3067
+ end
3068
+ (first...last).freeze
3069
+ end
3070
+
3071
+ def normalize_prepare_rename(rope, offset, result)
3072
+ return if result.nil?
3073
+
3074
+ keys = result.keys.map(&:to_s) if result.is_a?(Hash) && result.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
3075
+ raise Error, "invalid prepare rename response" if keys && keys.uniq.length != keys.length
3076
+ value = ->(key) { result.key?(key) ? result[key] : result[key.to_sym] }
3077
+ placeholder = if keys == ["defaultBehavior"]
3078
+ behavior = value.call("defaultBehavior")
3079
+ raise Error, "invalid prepare rename default behavior" unless behavior == true || behavior == false
3080
+ point = rope.point_at(offset)
3081
+ line_start = rope.line_start(point.row)
3082
+ line_end = point.row + 1 < rope.line_count ? rope.line_start(point.row + 1) : rope.bytesize
3083
+ before = rope.byteslice(line_start...offset).to_s[/[[:alnum:]_]*\z/].to_s
3084
+ after = rope.byteslice(offset...line_end).to_s[/\A[[:alnum:]_]*/].to_s
3085
+ before + after
3086
+ elsif keys&.sort == %w[placeholder range]
3087
+ range = strict_language_range(rope, value.call("range"), "prepare rename", allow_empty: false)
3088
+ raise Error, "invalid prepare rename position" unless range.cover?(offset)
3089
+ value.call("placeholder")
3090
+ elsif result.is_a?(Sadr::Range_) || keys&.sort == %w[end start]
3091
+ range = strict_language_range(rope, result, "prepare rename", allow_empty: false)
3092
+ raise Error, "invalid prepare rename position" unless range.cover?(offset)
3093
+ rope.byteslice(range).to_s
3094
+ else
3095
+ raise Error, "invalid prepare rename response"
3096
+ end
3097
+ unless valid_rename_value?(placeholder) && placeholder.bytesize <= RENAME_VALUE_LIMIT
3098
+ raise Error, "invalid prepare rename placeholder"
3099
+ end
3100
+ placeholder.dup.freeze
3101
+ rescue KeyError, RangeError, TypeError, ArgumentError, Sadr::Error => error
3102
+ raise Error, "invalid prepare rename response: #{error.message}"
3103
+ end
3104
+
3105
+ def normalize_document_highlights(rope, result)
3106
+ raise Error, "invalid document highlights" unless result.nil? || result.is_a?(Array)
3107
+ raise Error, "too many document highlights" if result && result.length > DOCUMENT_HIGHLIGHT_LIMIT
3108
+
3109
+ Array(result).map do |item|
3110
+ raise Error, "invalid document highlight" unless item.is_a?(Hash)
3111
+ kind = item.fetch("kind", 1)
3112
+ raise Error, "invalid document highlight kind" unless DOCUMENT_HIGHLIGHT_STYLES.key?(kind)
3113
+ range = strict_language_range(rope, item.fetch("range"), "document highlight", allow_empty: false)
3114
+ Decoration::Item.new(:highlight, range, nil, nil, DOCUMENT_HIGHLIGHT_STYLES.fetch(kind), 20, :document_highlight, nil)
3115
+ end.freeze
3116
+ rescue KeyError, RangeError, TypeError, ArgumentError, Sadr::Error => error
3117
+ raise Error, "invalid document highlights: #{error.message}"
3118
+ end
3119
+
3120
+ def normalize_document_links(rope, result, resolve:)
3121
+ raise Error, "invalid document links" unless result.nil? || result.is_a?(Array)
3122
+ raise Error, "too many document links" if result && result.length > DOCUMENT_LINK_LIMIT
3123
+
3124
+ links = Array(result).each_with_index.map do |value, index|
3125
+ validate_document_link_entry(rope, value, index, resolve: resolve)
3126
+ end.sort_by { |entry| [entry[:range].begin, entry[:range].end] }
3127
+ links.each_cons(2) do |left, right|
3128
+ raise Error, "overlapping document links" if left[:range].end > right[:range].begin
3129
+ end
3130
+ links.freeze
3131
+ rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError, KeyError, RangeError, TypeError,
3132
+ ArgumentError, Sadr::Error => error
3133
+ raise Error, "invalid document links: #{error.message}"
3134
+ end
3135
+
3136
+ def validate_document_link_entry(rope, value, index, resolve:)
3137
+ raise Error, "invalid document link" unless value.is_a?(Hash)
3138
+ encoded = JSON.generate(value)
3139
+ raise Error, "document link exceeds 1 MiB" if encoded.bytesize > 1 << 20
3140
+ link = JSON.parse(encoded)
3141
+ range = strict_language_range(rope, link.fetch("range"), "document link", allow_empty: false)
3142
+ target = document_link_target(link["target"]) if link.key?("target")
3143
+ raise Error, "document link has no target" unless target || resolve
3144
+ tooltip = link["tooltip"]
3145
+ unless tooltip.nil? || bounded_document_link_text?(tooltip)
3146
+ raise Error, "invalid document link tooltip"
3147
+ end
3148
+ {link: link.freeze, range: range, target: target, tooltip: tooltip&.dup&.freeze,
3149
+ index: index, state: target ? :ready : :unresolved}
3150
+ end
3151
+
3152
+ def normalize_linked_editing_ranges(rope, offset, selection, result)
3153
+ return if result.nil?
3154
+ unless result.is_a?(Hash) && result.keys.all? { |key| key.is_a?(String) } &&
3155
+ (result.keys - %w[ranges wordPattern]).empty? && result["ranges"].is_a?(Array)
3156
+ raise Error, "invalid linked editing response"
3157
+ end
3158
+ values = result.fetch("ranges")
3159
+ raise Error, "too many linked editing ranges" if values.length > LINKED_EDITING_RANGE_LIMIT
3160
+ ranges = values.map do |value|
3161
+ strict_language_range(rope, value, "linked editing", allow_empty: false)
3162
+ end.sort_by { |range| [range.begin, range.end] }
3163
+ ranges.each_cons(2) do |left, right|
3164
+ raise Error, "overlapping linked editing ranges" if left.end > right.begin
3165
+ end
3166
+ return {ranges: [].freeze, source: nil, pattern: nil}.freeze if ranges.empty?
3167
+ source = ranges.find do |range|
3168
+ selection.empty? ? range.cover?(offset) : range.begin <= selection.start && selection.end <= range.end
3169
+ end
3170
+ unless source && source.begin <= selection.start && selection.end <= source.end
3171
+ raise Error, "linked editing ranges do not contain the selection"
3172
+ end
3173
+ pattern = result["wordPattern"]
3174
+ if pattern
3175
+ unless pattern.is_a?(String) && pattern.encoding == Encoding::UTF_8 && pattern.valid_encoding? &&
3176
+ pattern.bytesize.between?(1, LINKED_EDITING_PATTERN_LIMIT) && !pattern.include?("\0")
3177
+ raise Error, "invalid linked editing word pattern"
307
3178
  end
3179
+ Regexp.new(pattern)
308
3180
  end
309
- return unless @pending_fold && @pending_fold[0].equal?(current)
310
- _, requested_document, version, cursor = @pending_fold
311
- unless current.equal?(editor) && requested_document.equal?(document) && version == current.buffer.version && cursor == current.primary.head
312
- @pending_fold = nil
313
- return
3181
+ {ranges: ranges.freeze, source: source, pattern: pattern&.dup&.freeze}.freeze
3182
+ rescue RegexpError, KeyError, RangeError, TypeError, ArgumentError, Sadr::Error => error
3183
+ raise Error, "invalid linked editing response: #{error.message}"
3184
+ end
3185
+
3186
+ def normalize_folding_ranges(rope, result)
3187
+ return nil if result.nil?
3188
+ raise Error, "invalid folding ranges" unless result.is_a?(Array)
3189
+ raise Error, "too many folding ranges" if result.length > FOLDING_RANGE_LIMIT
3190
+
3191
+ result.map do |item|
3192
+ raise Error, "invalid folding range" unless item.is_a?(Hash)
3193
+ first_row, last_row = item.fetch("startLine"), item.fetch("endLine")
3194
+ unless first_row.is_a?(Integer) && last_row.is_a?(Integer) &&
3195
+ first_row.between?(0, 0x7fffffff) && last_row.between?(first_row, 0x7fffffff) &&
3196
+ last_row < rope.line_count
3197
+ raise Error, "invalid folding range lines"
3198
+ end
3199
+ first = folding_position(rope, first_row, item["startCharacter"], item.key?("startCharacter"))
3200
+ last = folding_position(rope, last_row, item["endCharacter"], item.key?("endCharacter"))
3201
+ raise Error, "invalid folding range bounds" unless first < last
3202
+ (first...last).freeze
3203
+ end.uniq.sort_by { |range| [range.begin, -range.end] }.freeze
3204
+ rescue KeyError, RangeError, TypeError, ArgumentError, Sadr::Error => error
3205
+ raise Error, "invalid folding ranges: #{error.message}"
3206
+ end
3207
+
3208
+ def normalize_selection_ranges(rope, positions, result)
3209
+ return nil if result.nil?
3210
+ unless result.is_a?(Array) && result.length == positions.length
3211
+ raise Error, "invalid selection ranges"
3212
+ end
3213
+
3214
+ total = 0
3215
+ result.zip(positions).map do |root, position|
3216
+ node, child, depth, ranges = root, nil, 0, []
3217
+ loop do
3218
+ unless node.is_a?(Hash) && depth < SELECTION_RANGE_DEPTH_LIMIT
3219
+ raise Error, "invalid selection range chain"
3220
+ end
3221
+ range = strict_language_range(rope, node.fetch("range"), "selection")
3222
+ unless range.begin <= position && position <= range.end &&
3223
+ (!child || range.begin <= child.begin && child.end <= range.end)
3224
+ raise Error, "invalid selection range nesting"
3225
+ end
3226
+ ranges << range unless ranges.last == range
3227
+ total += 1
3228
+ raise Error, "too many selection ranges" if total > SELECTION_RANGE_LIMIT
3229
+ break unless node.key?("parent")
3230
+ node, child, depth = node["parent"], range, depth + 1
3231
+ end
3232
+ ranges.freeze
3233
+ end.freeze
3234
+ rescue KeyError, RangeError, TypeError, ArgumentError, Sadr::Error => error
3235
+ raise Error, "invalid selection ranges: #{error.message}"
3236
+ end
3237
+
3238
+ def normalize_antares_selection_ranges(rope, positions, result)
3239
+ unless result.is_a?(Array) && result.length == positions.length
3240
+ raise Error, "invalid Antares selection ranges"
314
3241
  end
315
- return if !document.syntax_ready? && document.pending?
316
- @pending_fold = nil
317
- if document.syntax_ready?
318
- range = document.fold_ranges.reverse.find { |item| item.cover?(cursor) }
319
- current.display_map.fold(range) if range
320
- @message = range ? "Folded" : "No fold at cursor"
321
- @window&.request_frame
3242
+ total = 0
3243
+ result.zip(positions).map do |ranges, position|
3244
+ child = nil
3245
+ unless ranges.is_a?(Array) && ranges.length <= SELECTION_RANGE_DEPTH_LIMIT
3246
+ raise Error, "invalid Antares selection range chain"
3247
+ end
3248
+ ranges.map do |range|
3249
+ unless range.is_a?(Range) && range.exclude_end? && range.begin.is_a?(Integer) && range.end.is_a?(Integer) &&
3250
+ range.begin >= 0 && range.end >= range.begin
3251
+ raise Error, "invalid Antares selection range"
3252
+ end
3253
+ rope.point_at(range.begin)
3254
+ rope.point_at(range.end)
3255
+ unless range.begin <= position && position <= range.end &&
3256
+ (!child || range.begin <= child.begin && child.end <= range.end)
3257
+ raise Error, "invalid Antares selection range nesting"
3258
+ end
3259
+ child = range
3260
+ total += 1
3261
+ raise Error, "too many Antares selection ranges" if total > SELECTION_RANGE_LIMIT
3262
+ range.freeze
3263
+ end.uniq.freeze
3264
+ end.freeze
3265
+ rescue RangeError, TypeError, ArgumentError => error
3266
+ raise Error, "invalid Antares selection ranges: #{error.message}"
3267
+ end
3268
+
3269
+ def folding_position(rope, row, character, present)
3270
+ point = if !present
3271
+ Sadr::Protocol.position(rope, rope.line_start(row) + rope.line(row).bytesize)
322
3272
  else
323
- @message = "Fold analysis unavailable"
3273
+ raise Error, "invalid folding range character" unless character.is_a?(Integer) && character.between?(0, 0x7fffffff)
3274
+ Sadr::Position.new(line: row, character: character)
324
3275
  end
3276
+ offset = Sadr::Protocol.offset(rope, point)
3277
+ raise Error, "invalid folding range character" unless Sadr::Protocol.position(rope, offset) == point
3278
+ offset
325
3279
  end
326
3280
 
327
- private
328
- def display_language_result(kind, result, client, current)
329
- case kind
330
- when :completion
331
- items = result.is_a?(Hash) ? result.fetch("items", []) : Array(result)
332
- defaults = result.is_a?(Hash) ? result.fetch("itemDefaults", {}) : {}
333
- items = items.map do |item|
334
- merged = defaults.reject { |key, _| key == "editRange" }.merge(item)
335
- if defaults["editRange"] && !merged["textEdit"]
336
- range = defaults["editRange"]
337
- merged["textEdit"] = (range.key?("start") ? {"range" => range} : range).merge("newText" => item["textEditText"] || item["insertText"] || item.fetch("label"))
3281
+ def document_link_target(value)
3282
+ unless bounded_document_link_text?(value, maximum: DOCUMENT_LINK_URI_LIMIT)
3283
+ raise Error, "invalid document link target"
3284
+ end
3285
+ uri = URI::DEFAULT_PARSER.parse(value)
3286
+ case uri.scheme&.downcase
3287
+ when "http", "https"
3288
+ raise Error, "invalid document link target" unless uri.host && !uri.host.empty?
3289
+ when "file"
3290
+ Sadr::Protocol.path(value)
3291
+ else
3292
+ raise Error, "unsafe document link target"
3293
+ end
3294
+ value.dup.freeze
3295
+ rescue URI::InvalidURIError => error
3296
+ raise Error, "invalid document link target: #{error.message}"
3297
+ end
3298
+
3299
+ def bounded_document_link_text?(value, maximum: DOCUMENT_LINK_TEXT_LIMIT)
3300
+ value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? &&
3301
+ value.bytesize.between?(1, maximum) && !value.include?("\0")
3302
+ end
3303
+
3304
+ def document_link_supported?(client)
3305
+ provider = client.capabilities["documentLinkProvider"] if client&.respond_to?(:capabilities)
3306
+ provider == true || provider.is_a?(Hash)
3307
+ end
3308
+
3309
+ def document_link_resolve_supported?(client)
3310
+ provider = client.capabilities["documentLinkProvider"] if client&.respond_to?(:capabilities)
3311
+ provider.is_a?(Hash) && (provider["resolveProvider"] == true || provider[:resolveProvider] == true)
3312
+ end
3313
+
3314
+ def document_link_key(client, current, buffer, version = buffer.version,
3315
+ supported = document_link_supported?(client), resolve = document_link_resolve_supported?(client),
3316
+ document = current.language_document)
3317
+ [client, current, buffer, version, supported, resolve, document]
3318
+ end
3319
+
3320
+ def document_link_result_valid?(request, owner)
3321
+ document_link_editor_valid?(request) && request[:client].equal?(owner) &&
3322
+ active_language_client(request[:language], "documentLink").equal?(owner) &&
3323
+ request[:supported] == document_link_supported?(owner) &&
3324
+ request[:resolve] == document_link_resolve_supported?(owner) &&
3325
+ @opened_lsp_documents&.key?([owner, request[:buffer]])
3326
+ end
3327
+
3328
+ def document_link_editor_valid?(request)
3329
+ current, buffer = request.values_at(:editor, :buffer)
3330
+ !@closed && current.buffer.equal?(buffer) && buffer.version == request[:version] &&
3331
+ current.language_document.equal?(request[:document]) &&
3332
+ buffer.path && Sadr::Protocol.uri(buffer.path) == request[:uri] &&
3333
+ @panes.any? { |pane| pane.active.equal?(current) }
3334
+ end
3335
+
3336
+ def document_link_request_valid?(request, owner)
3337
+ @document_link_requests&.[](request[:editor]).equal?(request) && document_link_result_valid?(request, owner)
3338
+ end
3339
+
3340
+ def cache_document_links(key, links)
3341
+ cache = @document_link_cache ||= {}
3342
+ cache.delete_if { |entry, value| entry[1].equal?(key[1]) && (cancel_document_link_cache(value); true) }
3343
+ cache.shift while cache.length >= DOCUMENT_LINK_REQUEST_LIMIT
3344
+ if links == false
3345
+ cache[key] = false
3346
+ else
3347
+ holder = {entries: links}
3348
+ links.each do |entry|
3349
+ label = entry[:tooltip] || entry[:target] || "Open document link"
3350
+ entry[:decoration] = Decoration::Item.new(:highlight, entry[:range], nil, label,
3351
+ DOCUMENT_LINK_STYLE, 18, :document_link,
3352
+ ->(current, offset) { activate_document_link(key, holder, entry, current, offset) })
3353
+ end
3354
+ cache[key] = holder
3355
+ end
3356
+ @decorations.invalidate(:document_link, buffer: key[2])
3357
+ @window&.request_frame unless @closed
3358
+ links
3359
+ end
3360
+
3361
+ def cancel_document_link_cache(cache)
3362
+ cache[:entries].each { |entry| entry[:future]&.cancel } if cache.is_a?(Hash)
3363
+ nil
3364
+ end
3365
+
3366
+ def document_link_cache_valid?(key, cache)
3367
+ client, current, buffer, version, supported, resolve, document = key
3368
+ @document_link_cache&.[](key).equal?(cache) && !@closed && buffer.version == version &&
3369
+ current.buffer.equal?(buffer) && current.language_document.equal?(document) && language_client_active?(client) &&
3370
+ document_link_supported?(client) == supported && document_link_resolve_supported?(client) == resolve &&
3371
+ @panes.any? { |pane| pane.active.equal?(current) }
3372
+ end
3373
+
3374
+ def activate_document_link(key, cache, entry, current, offset)
3375
+ return false unless current.equal?(key[1]) && entry[:range].cover?(offset) && document_link_cache_valid?(key, cache)
3376
+ return open_document_link(entry[:target]) if entry[:target]
3377
+ return false unless key[5] && entry[:state] == :unresolved
3378
+ resolving = (@document_link_cache || {}).values.sum do |value|
3379
+ value.is_a?(Hash) ? value[:entries].count { |candidate| candidate[:state] == :resolving } : 0
3380
+ end
3381
+ if resolving >= DOCUMENT_LINK_RESOLVE_LIMIT
3382
+ @message = "Too many document links are resolving"
3383
+ return false
3384
+ end
3385
+
3386
+ entry[:state] = :resolving
3387
+ client, buffer = key.values_at(0, 2)
3388
+ job = Thread.new do
3389
+ begin
3390
+ future = client.resolve_document_link(entry[:link])
3391
+ entry[:future] = future
3392
+ result = future.await(timeout: 10) if document_link_cache_valid?(key, cache)
3393
+ unless document_link_cache_valid?(key, cache)
3394
+ future.cancel
3395
+ next
3396
+ end
3397
+ resolved = validate_document_link_entry(buffer.rope, result, entry[:index], resolve: false)
3398
+ raise Error, "resolved document link moved" unless resolved[:range] == entry[:range]
3399
+ post do
3400
+ next unless document_link_cache_valid?(key, cache) && entry[:state] == :resolving
3401
+ entry.update(resolved)
3402
+ entry.delete(:future)
3403
+ open_document_link(entry[:target])
338
3404
  end
339
- merged
3405
+ rescue StandardError => error
3406
+ post do
3407
+ next unless document_link_cache_valid?(key, cache) && entry[:state] == :resolving
3408
+ entry.delete(:future)
3409
+ entry[:state] = :failed
3410
+ @message = error.message unless @retired_language_clients&.[](client)
3411
+ end
3412
+ ensure
3413
+ worker = Thread.current
3414
+ post { @language_jobs&.delete(worker) }
340
3415
  end
341
- self.palette = {kind: :completion, query: +"", index: 0, matches: items.map { |item| item.fetch("label") }, items: items, editor: current, client: client, version: current.buffer.version}
342
- update_palette
3416
+ end
3417
+ (@language_jobs ||= []) << job
3418
+ @language_jobs.reject! { |thread| !thread.alive? }
3419
+ true
3420
+ rescue StandardError => error
3421
+ @message = error.message
3422
+ false
3423
+ end
3424
+
3425
+ def open_document_link(target)
3426
+ uri = URI::DEFAULT_PARSER.parse(document_link_target(target))
3427
+ if %w[http https].include?(uri.scheme.downcase)
3428
+ raise Error, "Opening URLs is unavailable" unless @window&.respond_to?(:open_url)
3429
+ @window.open_url(target)
3430
+ else
3431
+ path = canonical_path(Sadr::Protocol.path(target))
3432
+ raise Error, "Document link target is not a file" unless File.file?(path)
3433
+ open(path).tap { |current| current.select(0); current.reveal_cursor }
3434
+ end
3435
+ true
3436
+ rescue URI::InvalidURIError, SystemCallError, Sadr::Error => error
3437
+ raise Error, "Cannot open document link: #{error.message}"
3438
+ end
3439
+
3440
+ def linked_editing_snapshot(current)
3441
+ buffer, offset = current.buffer, current.primary.head
3442
+ language = current.language_document.definition.name
3443
+ client = active_language_client(language, "linkedEditingRange")
3444
+ snapshot = {editor: current, buffer: buffer, version: buffer.version, rope: buffer.rope,
3445
+ selections: current.selections, selection: current.primary, offset: offset,
3446
+ uri: Sadr::Protocol.uri(buffer.path), position: Sadr::Protocol.position(buffer.rope, offset),
3447
+ language: language, document: current.language_document,
3448
+ client: client, supported: linked_editing_supported?(client)}
3449
+ unless Sadr::Protocol.offset(snapshot[:rope], snapshot[:position]) == offset
3450
+ raise Error, "invalid linked editing position"
3451
+ end
3452
+ snapshot[:selection_subscription] = current.on_selection do
3453
+ invalidate_linked_editing_ranges(editor: current) unless current.selections == snapshot[:selections]
3454
+ end
3455
+ snapshot[:edit_subscription] = buffer.on_edit { invalidate_linked_editing_ranges(buffer) }
3456
+ snapshot
3457
+ end
3458
+
3459
+ def start_linked_editing_request(id, snapshot)
3460
+ job = Thread.new do
3461
+ owner = nil
3462
+ begin
3463
+ linked = nil
3464
+ fallback = false
3465
+ future = nil
3466
+ if snapshot[:client] || language_server_options(snapshot[:language])
3467
+ begin
3468
+ owner = language_client(snapshot[:buffer], feature: "linkedEditingRange")
3469
+ snapshot[:client] = owner
3470
+ snapshot[:supported] = linked_editing_supported?(owner)
3471
+ next unless @linked_editing_requests&.[](id).equal?(snapshot) &&
3472
+ linked_editing_editor_valid?(snapshot)
3473
+
3474
+ if snapshot[:supported]
3475
+ future = owner.linked_editing_range(snapshot[:uri], snapshot[:position])
3476
+ snapshot[:future] = future
3477
+ result = future.await(timeout: 10) if linked_editing_request_valid?(id, snapshot, owner)
3478
+ unless linked_editing_request_valid?(id, snapshot, owner)
3479
+ future.cancel
3480
+ next
3481
+ end
3482
+ linked = normalize_linked_editing_ranges(snapshot[:rope], snapshot[:offset], snapshot[:selection], result)
3483
+ fallback = result.nil?
3484
+ elsif linked_editing_markup?(snapshot)
3485
+ fallback = true
3486
+ else
3487
+ post do
3488
+ next unless take_linked_editing_request(id, snapshot)
3489
+ release_linked_editing_snapshot(snapshot)
3490
+ @message = "Language server does not support linked editing" if linked_editing_editor_valid?(snapshot)
3491
+ end
3492
+ next
3493
+ end
3494
+ rescue StandardError
3495
+ raise unless linked_editing_markup?(snapshot)
3496
+ future&.cancel
3497
+ fallback = true
3498
+ end
3499
+ else
3500
+ fallback = true
3501
+ end
3502
+ linked = markup_linked_editing_ranges(snapshot) if fallback && linked_editing_markup?(snapshot)
3503
+ post do
3504
+ next unless take_linked_editing_request(id, snapshot)
3505
+ valid = fallback ? linked_editing_editor_valid?(snapshot) : linked_editing_snapshot_valid?(snapshot)
3506
+ release_linked_editing_snapshot(snapshot)
3507
+ if valid && linked && !linked[:ranges].empty?
3508
+ apply_linked_editing_ranges(snapshot, linked)
3509
+ elsif valid
3510
+ @message = "Linked editing is not available here"
3511
+ end
3512
+ end
3513
+ rescue StandardError => error
3514
+ post do
3515
+ next unless take_linked_editing_request(id, snapshot)
3516
+ valid = linked_editing_editor_valid?(snapshot) && !@retired_language_clients&.[](owner)
3517
+ release_linked_editing_snapshot(snapshot)
3518
+ @message = error.message if valid
3519
+ end
3520
+ ensure
3521
+ worker = Thread.current
3522
+ post do
3523
+ release_linked_editing_snapshot(snapshot) if take_linked_editing_request(id, snapshot)
3524
+ @language_jobs&.delete(worker)
3525
+ end
3526
+ end
3527
+ end
3528
+ (@language_jobs ||= []) << job
3529
+ @language_jobs.reject! { |thread| !thread.alive? }
3530
+ true
3531
+ end
3532
+
3533
+ def linked_editing_markup?(snapshot)
3534
+ %w[html xml].include?(snapshot[:document].definition.lexer)
3535
+ end
3536
+
3537
+ def markup_linked_editing_ranges(snapshot)
3538
+ return if snapshot[:rope].bytesize > LINKED_EDITING_MARKUP_BYTE_LIMIT
3539
+ source = snapshot[:rope].to_s
3540
+ return unless source.valid_encoding?
3541
+
3542
+ html = snapshot[:document].definition.lexer == "html"
3543
+ scanner = StringScanner.new(source)
3544
+ stack, pairs, count = [], [], 0
3545
+ while scanner.scan_until(/</)
3546
+ token = scan_linked_editing_markup_tag(scanner, source, html)
3547
+ return if token == false
3548
+ next unless token
3549
+ return if (count += 1) > LINKED_EDITING_MARKUP_TAG_LIMIT
3550
+
3551
+ closing, name, range, self_closing = token
3552
+ key = html ? name.tr("A-Z", "a-z") : name
3553
+ if closing
3554
+ opening = stack.pop
3555
+ return unless opening && opening.first == key
3556
+ pairs << [opening.last, range]
3557
+ elsif !self_closing && !(html && LINKED_EDITING_HTML_VOID_TAGS.include?(key))
3558
+ return if stack.length >= LINKED_EDITING_MARKUP_DEPTH_LIMIT
3559
+ stack << [key, range]
3560
+ end
3561
+ end
3562
+ return unless stack.empty?
3563
+
3564
+ selection, offset = snapshot.values_at(:selection, :offset)
3565
+ pair = pairs.find do |ranges|
3566
+ ranges.any? { |range| selection.empty? ? range.cover?(offset) : range.begin <= selection.start && selection.end <= range.end }
3567
+ end
3568
+ return unless pair
3569
+ source_range = pair.find do |range|
3570
+ selection.empty? ? range.cover?(offset) : range.begin <= selection.start && selection.end <= range.end
3571
+ end
3572
+ {ranges: pair.sort_by(&:begin).freeze, source: source_range, pattern: nil}.freeze
3573
+ end
3574
+
3575
+ def scan_linked_editing_markup_tag(scanner, source, html)
3576
+ if scanner.peek(1) == "!" || scanner.peek(1) == "?"
3577
+ return false unless scan_linked_editing_markup_special(scanner)
3578
+ return nil
3579
+ end
3580
+ return false if scanner.peek(1) == "%"
3581
+
3582
+ closing = !!scanner.scan(/\//)
3583
+ name_start = scanner.pos
3584
+ name = scanner.scan(html ? /[A-Za-z][A-Za-z0-9:_.-]*/ : /[\p{L}_:][\p{L}\p{N}\p{M}_:.\-]*/u)
3585
+ return false unless name && name.bytesize <= LINKED_EDITING_MARKUP_NAME_LIMIT
3586
+ range = name_start...scanner.pos
3587
+ tail = scan_linked_editing_markup_tail(scanner, source)
3588
+ return false unless tail
3589
+ return false if closing && !tail.first.strip.empty?
3590
+ [closing, name, range, tail.last]
3591
+ end
3592
+
3593
+ def scan_linked_editing_markup_special(scanner)
3594
+ if scanner.scan(/!--/)
3595
+ !!scanner.scan_until(/-->/)
3596
+ elsif scanner.scan(/!\[CDATA\[/)
3597
+ !!scanner.scan_until(/\]\]>/)
3598
+ elsif scanner.scan(/\?/)
3599
+ !!scanner.scan_until(/\?>/)
3600
+ elsif scanner.scan(/!/)
3601
+ scan_linked_editing_markup_declaration(scanner)
3602
+ else
3603
+ false
3604
+ end
3605
+ end
3606
+
3607
+ def scan_linked_editing_markup_declaration(scanner)
3608
+ quote, brackets = nil, 0
3609
+ while (character = scanner.getch)
3610
+ if quote
3611
+ quote = nil if character == quote
3612
+ elsif character == "\"" || character == "'"
3613
+ quote = character
3614
+ elsif character == "["
3615
+ brackets += 1
3616
+ elsif character == "]"
3617
+ return false if brackets.zero?
3618
+ brackets -= 1
3619
+ elsif character == ">" && brackets.zero?
3620
+ return true
3621
+ end
3622
+ end
3623
+ false
3624
+ end
3625
+
3626
+ def scan_linked_editing_markup_tail(scanner, source)
3627
+ start, quote = scanner.pos, nil
3628
+ while (character = scanner.getch)
3629
+ if quote
3630
+ quote = nil if character == quote
3631
+ elsif character == "\"" || character == "'"
3632
+ quote = character
3633
+ elsif character == "<"
3634
+ return false
3635
+ elsif character == ">"
3636
+ tail = source.byteslice(start...(scanner.pos - 1)).to_s
3637
+ return [tail, tail.rstrip.end_with?("/")]
3638
+ end
3639
+ end
3640
+ false
3641
+ end
3642
+
3643
+ def apply_linked_editing_ranges(snapshot, linked)
3644
+ source, selection = linked[:source], snapshot[:selection]
3645
+ anchor, head = selection.anchor - source.begin, selection.head - source.begin
3646
+ selections = linked[:ranges].each_with_index.map do |range, index|
3647
+ unless anchor.between?(0, range.size) && head.between?(0, range.size)
3648
+ raise Error, "linked editing ranges do not match the selection"
3649
+ end
3650
+ first, last = range.begin + anchor, range.begin + head
3651
+ [first, last].each do |offset|
3652
+ position = Sadr::Protocol.position(snapshot[:rope], offset)
3653
+ raise Error, "invalid linked editing selection boundary" unless Sadr::Protocol.offset(snapshot[:rope], position) == offset
3654
+ end
3655
+ Selection.new(index, first, last, nil)
3656
+ end
3657
+ snapshot[:editor].set_selections(selections, merge: false)
3658
+ snapshot[:editor].reveal_cursor
3659
+ @message = "Linked ranges selected"
3660
+ @window&.request_frame
3661
+ true
3662
+ end
3663
+
3664
+ def linked_editing_supported?(client)
3665
+ provider = client.capabilities["linkedEditingRangeProvider"] if client&.respond_to?(:capabilities)
3666
+ provider == true || provider.is_a?(Hash)
3667
+ end
3668
+
3669
+ def linked_editing_editor_valid?(snapshot)
3670
+ current, buffer = snapshot.values_at(:editor, :buffer)
3671
+ !@closed && current.buffer.equal?(buffer) && buffer.version == snapshot[:version] &&
3672
+ current.language_document.equal?(snapshot[:document]) &&
3673
+ current.selections == snapshot[:selections] && current.primary.head == snapshot[:offset] &&
3674
+ buffer.path && Sadr::Protocol.uri(buffer.path) == snapshot[:uri] &&
3675
+ @panes.any? { |pane| pane.active.equal?(current) }
3676
+ end
3677
+
3678
+ def linked_editing_snapshot_valid?(snapshot)
3679
+ client = active_language_client(snapshot[:language], "linkedEditingRange")
3680
+ linked_editing_editor_valid?(snapshot) && client.equal?(snapshot[:client]) &&
3681
+ linked_editing_supported?(client) == snapshot[:supported]
3682
+ end
3683
+
3684
+ def linked_editing_request_valid?(id, snapshot, owner)
3685
+ @linked_editing_requests&.[](id).equal?(snapshot) && linked_editing_snapshot_valid?(snapshot) &&
3686
+ snapshot[:client].equal?(owner) && @opened_lsp_documents&.key?([owner, snapshot[:buffer]])
3687
+ end
3688
+
3689
+ def take_linked_editing_request(id, snapshot)
3690
+ @linked_editing_requests&.delete(id).equal?(snapshot)
3691
+ end
3692
+
3693
+ def release_linked_editing_snapshot(snapshot)
3694
+ snapshot&.delete(:selection_subscription)&.detach
3695
+ snapshot&.delete(:edit_subscription)&.detach
3696
+ snapshot&.delete(:future)
3697
+ nil
3698
+ end
3699
+
3700
+ def document_highlight_supported?(client)
3701
+ provider = client.capabilities["documentHighlightProvider"] if client&.respond_to?(:capabilities)
3702
+ provider == true || provider.is_a?(Hash)
3703
+ end
3704
+
3705
+ def document_highlight_key(client, current, buffer, version = buffer.version,
3706
+ head = current.primary.head, supported = document_highlight_supported?(client))
3707
+ [client, current, buffer, version, head, supported]
3708
+ end
3709
+
3710
+ def document_highlight_result_valid?(request, owner)
3711
+ buffer = request[:buffer]
3712
+ document_highlight_editor_valid?(request) && language_client_active?(owner) &&
3713
+ @opened_lsp_documents&.key?([owner, buffer])
3714
+ end
3715
+
3716
+ def document_highlight_editor_valid?(request)
3717
+ current, buffer = request.values_at(:editor, :buffer)
3718
+ !@closed && current.buffer.equal?(buffer) && buffer.version == request[:version] &&
3719
+ current.primary.head == request[:head] && buffer.path && Sadr::Protocol.uri(buffer.path) == request[:uri] &&
3720
+ @panes.any? { |pane| pane.active.equal?(current) }
3721
+ end
3722
+
3723
+ def document_highlight_request_valid?(request, owner)
3724
+ @document_highlight_requests&.[](request[:editor]).equal?(request) && document_highlight_result_valid?(request, owner)
3725
+ end
3726
+
3727
+ def cache_document_highlights(key, highlights)
3728
+ cache = @document_highlight_cache ||= {}
3729
+ cache.delete_if { |entry, _value| entry[1].equal?(key[1]) }
3730
+ cache.shift while cache.length >= DOCUMENT_HIGHLIGHT_REQUEST_LIMIT
3731
+ cache[key] = highlights
3732
+ @decorations.invalidate(:document_highlight, buffer: key[2])
3733
+ @window&.request_frame unless @closed
3734
+ highlights
3735
+ end
3736
+
3737
+ def cache_sticky_symbols(client, buffer, version, symbols)
3738
+ cache = @sticky_symbol_cache ||= {}
3739
+ cache.delete_if { |key, _| key[0].equal?(client) && key[1].equal?(buffer) }
3740
+ cache.shift while cache.length >= 64
3741
+ cache[[client, buffer, version]] = symbols == false ? false : sticky_cache_entry(buffer, version, client, symbols)
3742
+ @sticky_context_cache&.clear
3743
+ @window&.request_frame unless @closed
3744
+ symbols
3745
+ end
3746
+
3747
+ def sticky_cache_entry(buffer, version, client, symbols)
3748
+ generation = @sticky_symbol_generation = @sticky_symbol_generation.to_i + 1
3749
+ by_id = symbols.to_h { |symbol| [symbol.id, symbol] }.freeze
3750
+ children = symbols.group_by(&:parent_id).transform_values do |values|
3751
+ values.sort_by { |symbol| [symbol.range.begin, symbol.range.end, symbol.id] }.freeze
3752
+ end.freeze
3753
+ {buffer: buffer, version: version, client: client, generation: generation,
3754
+ symbols: symbols.freeze, by_id: by_id, children: children}.freeze
3755
+ end
3756
+
3757
+ def active_structure_guide(current, enabled)
3758
+ return unless enabled
3759
+ row = current.buffer.rope.point_at(current.primary.head).row
3760
+ region = current.language_document.structure_regions.lazy
3761
+ .select { |item| item[:kind] == :block && item[:start_line] <= row && row <= item[:end_line] }
3762
+ .min_by { |item| [item[:end_line] - item[:start_line], -item[:start_line]] }
3763
+ return unless region
3764
+
3765
+ indentation = indentation_offsets(current.buffer.line(region[:start_line]), current.tab_size).last&.first || 0
3766
+ region.merge(column: indentation + current.tab_size)
3767
+ end
3768
+
3769
+ def indentation_offsets(line, tab_size, limit: INDENT_GUIDE_LIMIT)
3770
+ return [] unless limit.positive?
3771
+ offsets, column, byte, target = [], 0, 0, tab_size
3772
+ line.each_byte do |character|
3773
+ break unless character == 32 || character == 9
3774
+ column += character == 9 ? tab_size - column % tab_size : 1
3775
+ byte += 1
3776
+ while target <= column
3777
+ offsets << [target, byte]
3778
+ return offsets if offsets.length >= limit
3779
+ target += tab_size
3780
+ end
3781
+ end
3782
+ offsets
3783
+ end
3784
+
3785
+ def visible_language_ranges(current, display_rows, gap:)
3786
+ map = current.display_map
3787
+ rows = display_rows.flat_map do |index|
3788
+ row = map.row(index)
3789
+ ending = map.to_buffer(DisplayPoint.new(index, row.text.length))
3790
+ [map.source_row(index), current.buffer.rope.point_at(ending).row]
3791
+ end
3792
+ rows.sort.uniq.slice_when { |left, right| right - left > gap }
3793
+ .map { |group| group.first...(group.last + 1) }
3794
+ end
3795
+
3796
+ def open_language_document(client, buffer, language_id)
3797
+ uri = Sadr::Protocol.uri(buffer.path)
3798
+ key = [client, buffer]
3799
+ @language_document_subscriptions&.delete(key)&.detach
3800
+ (@language_document_subscriptions ||= {})[key] = buffer.on_edit do |patch|
3801
+ invalidate_document_highlights(buffer)
3802
+ invalidate_document_links(buffer)
3803
+ invalidate_folding_ranges(buffer)
3804
+ invalidate_selection_ranges(buffer)
3805
+ invalidate_prepare_rename(buffer)
3806
+ invalidate_linked_editing_ranges(buffer)
3807
+ invalidate_inlay_hints(buffer)
3808
+ invalidate_code_lenses(buffer)
3809
+ invalidate_sticky_symbols(buffer)
3810
+ sync_language_document(client, uri, buffer, patch)
3811
+ end
3812
+ (@opened_lsp_documents ||= {})[key] = true
3813
+ client.open(Sadr::Document.new(uri: uri, language_id: language_id, version: buffer.version, text: buffer.text))
3814
+ uri
3815
+ rescue StandardError
3816
+ @opened_lsp_documents&.delete(key) if defined?(key)
3817
+ @language_document_subscriptions&.delete(key)&.detach if defined?(key)
3818
+ raise
3819
+ end
3820
+
3821
+ def close_language_document(client, buffer, uri: Sadr::Protocol.uri(buffer.path))
3822
+ forget_language_document(client, buffer, uri: uri)
3823
+ client.close(uri)
3824
+ end
3825
+
3826
+ def forget_language_document(client, buffer, uri: buffer.path && Sadr::Protocol.uri(buffer.path))
3827
+ key = [client, buffer]
3828
+ @opened_lsp_documents&.delete(key)
3829
+ @language_document_subscriptions&.delete(key)&.detach
3830
+ clear_client_diagnostics(client, uri) if uri
3831
+ invalidate_diagnostics(buffer)
3832
+ invalidate_document_highlights(buffer, client: client)
3833
+ invalidate_document_links(buffer, client: client)
3834
+ invalidate_folding_ranges(buffer, client: client)
3835
+ invalidate_selection_ranges(buffer, client: client)
3836
+ invalidate_prepare_rename(buffer, client: client)
3837
+ invalidate_linked_editing_ranges(buffer, client: client)
3838
+ invalidate_inlay_hints(buffer, client: client)
3839
+ invalidate_code_lenses(buffer, client: client)
3840
+ invalidate_sticky_symbols(buffer, client: client)
3841
+ end
3842
+
3843
+ def sync_language_document(client, uri, buffer, patch)
3844
+ clear_client_diagnostics(client, uri)
3845
+ changes = if patch.is_a?(Patch)
3846
+ patch.edits.reverse.map do |edit|
3847
+ Sadr::ContentChange.new(range: Sadr::Protocol.range(patch.before, edit.old_range), text: edit.new_text)
3848
+ end
3849
+ else
3850
+ [Sadr::ContentChange.new(range: nil, text: buffer.text)]
3851
+ end
3852
+ client.change(uri, buffer.version, changes)
3853
+ rescue Sadr::Error => error
3854
+ self.message = "Language server change failed: #{error.message}"
3855
+ resync_language_document(client, uri, buffer)
3856
+ end
3857
+
3858
+ def publish_lsp_diagnostics(uri)
3859
+ clients = [*(@language_clients || {}).values.flatten, *(@lsp_diagnostics || {}).keys].uniq
3860
+ values = clients.flat_map { |client| @lsp_diagnostics&.dig(client, uri) || [] }.uniq
3861
+ @diagnostics.publish(:lsp, uri, values)
3862
+ end
3863
+
3864
+ def clear_language_client_diagnostics(client)
3865
+ (@lsp_diagnostics&.dig(client)&.keys || []).dup.each { |uri| clear_client_diagnostics(client, uri) }
3866
+ end
3867
+
3868
+ def clear_client_diagnostics(client, uri)
3869
+ if (documents = @lsp_diagnostics&.[](client))
3870
+ documents.delete(uri)
3871
+ @lsp_diagnostics.delete(client) if documents.empty?
3872
+ end
3873
+ if (versions = @diagnostic_versions&.[](client))
3874
+ versions.delete(uri)
3875
+ @diagnostic_versions.delete(client) if versions.empty?
3876
+ end
3877
+ publish_lsp_diagnostics(uri)
3878
+ end
3879
+ private :publish_lsp_diagnostics, :clear_client_diagnostics, :clear_language_client_diagnostics
3880
+
3881
+ def resync_language_document(client, uri, buffer)
3882
+ key = [client, buffer]
3883
+ jobs = @language_document_resyncs ||= {}
3884
+ return if jobs[key]&.alive?
3885
+
3886
+ # ponytail: poll until Sadr exposes a restart-complete callback.
3887
+ jobs[key] = Thread.new do
3888
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 35
3889
+ until @closed || @retired_language_clients&.[](client) || !@opened_lsp_documents&.[](key)
3890
+ if client.running?
3891
+ version = buffer.version
3892
+ text = buffer.text
3893
+ next unless version == buffer.version
3894
+
3895
+ client.change(uri, version, [Sadr::ContentChange.new(range: nil, text: text)])
3896
+ break
3897
+ end
3898
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
3899
+ sleep 0.01
3900
+ end
3901
+ rescue Sadr::Error => error
3902
+ self.message = "Language server resync failed: #{error.message}" unless error.message == "document version must increase"
3903
+ ensure
3904
+ jobs.delete(key) if jobs[key].equal?(Thread.current)
3905
+ end
3906
+ end
3907
+
3908
+ def completion_query(buffer, offset)
3909
+ line = buffer.rope.byteslice(buffer.rope.line_start(buffer.rope.point_at(offset).row)...offset).to_s
3910
+ line[/[[:alnum:]_]*\z/].to_s.each_grapheme_cluster.to_a.last(64).join
3911
+ end
3912
+
3913
+ def lsp_completion_items(result)
3914
+ items = result.is_a?(Hash) ? result.fetch("items", []) : result.nil? ? [] : result
3915
+ defaults = result.is_a?(Hash) ? result.fetch("itemDefaults", {}) : {}
3916
+ raise Error, "invalid completion result" unless items.is_a?(Array) && defaults.is_a?(Hash)
3917
+ raise Error, "too many completion items" if items.length > Provider::Registry::MAX_ITEMS
3918
+
3919
+ items.map do |item|
3920
+ raise Error, "invalid completion item" unless item.is_a?(Hash)
3921
+ merged = defaults.reject { |key, _| key == "editRange" }.merge(item)
3922
+ if defaults["editRange"] && !merged["textEdit"]
3923
+ range = defaults["editRange"]
3924
+ raise Error, "invalid completion edit range" unless range.is_a?(Hash)
3925
+ text = item["textEditText"] || item["insertText"] || item.fetch("label")
3926
+ merged["textEdit"] = (range.key?("start") ? {"range" => range} : range).merge("newText" => text)
3927
+ end
3928
+ merged.freeze
3929
+ end
3930
+ rescue KeyError => error
3931
+ raise Error, "invalid completion result: #{error.message}"
3932
+ end
3933
+
3934
+ def display_language_result(kind, result, client, current, item_clients: nil)
3935
+ case kind
3936
+ when :completion
3937
+ generation = @completion_generation = (@completion_generation || 0) + 1
3938
+ context = {editor: current, version: current.buffer.version, generation: generation,
3939
+ query: completion_query(current.buffer, current.primary.head),
3940
+ lsp_result: result, client: client, metadata: {}, errors: []}
3941
+ display_completions(@providers.complete(current.buffer, current.primary.head, context), current, context)
343
3942
  when :hover, :signatureHelp
344
3943
  contents = result && (result["contents"] || result["signatures"]&.map { |signature| signature["label"] })
345
3944
  @hover_markup = kind == :hover && !(contents.is_a?(Hash) && contents["kind"] == "plaintext")
@@ -356,7 +3955,7 @@ module Canopus
356
3955
  location = item["location"] || item
357
3956
  uri = location["uri"] || location["targetUri"]
358
3957
  range = location["range"] || location["targetSelectionRange"]
359
- "#{item['name']} #{File.basename(LSP::Protocol.path(uri))}:#{range.fetch('start').fetch('line') + 1}"
3958
+ "#{item['name']} #{File.basename(Sadr::Protocol.path(uri))}:#{range.fetch('start').fetch('line') + 1}"
360
3959
  end
361
3960
  self.palette = {kind: :locations, query: +"", index: 0, matches: labels, items: items}
362
3961
  when :rename
@@ -365,10 +3964,11 @@ module Canopus
365
3964
  elsif result
366
3965
  apply_workspace_edit(result)
367
3966
  end
368
- when :formatting then client.apply_text_edits(current.buffer, result || [])
3967
+ when :formatting then current.buffer.edit(Sadr::Protocol.text_edits(current.buffer.rope, result || []), kind: :lsp)
369
3968
  when :codeAction, :codeLens
370
3969
  items = Array(result)
371
- self.palette = {kind: :code_actions, query: +"", index: 0, matches: items.map { |item| item["title"] || item.dig("command", "title") || "Code lens" }, items: items, client: client, editor: current, version: current.buffer.version, lens: kind == :codeLens}
3970
+ self.palette = {kind: :code_actions, query: +"", index: 0, matches: items.map { |item| item["title"] || item.dig("command", "title") || "Code lens" },
3971
+ items: items, client: client, item_clients: item_clients, editor: current, version: current.buffer.version, lens: kind == :codeLens}
372
3972
  update_palette
373
3973
  when :documentSymbol
374
3974
  symbols = []
@@ -379,33 +3979,19 @@ module Canopus
379
3979
  end
380
3980
  end
381
3981
  collect.call(Array(result), 0)
382
- uri = LSP::Protocol.uri(current.buffer.path)
3982
+ uri = Sadr::Protocol.uri(current.buffer.path)
383
3983
  self.palette = {kind: :symbols, query: +"", index: 0, matches: symbols.map { |symbol| "#{' ' * symbol['_depth']}#{symbol['name']}" },
384
3984
  items: symbols.map { |symbol| symbol.merge("uri" => uri, "range" => symbol["selectionRange"] || symbol["range"]) }}
385
3985
  when :diagnostic
386
- client.diagnostics[LSP::Protocol.uri(current.buffer.path)] = result.fetch("items", []) if result
3986
+ if result
3987
+ uri = Sadr::Protocol.uri(current.buffer.path)
3988
+ accept_diagnostic_notification(client, {"uri" => uri, "version" => current.buffer.version,
3989
+ "diagnostics" => result.fetch("items", [])})
3990
+ end
387
3991
  when :semantic_tokens
388
3992
  types = client.capabilities.dig("semanticTokensProvider", "legend", "tokenTypes") || []
389
3993
  @semantic_styles ||= {}
390
- @semantic_styles[current.buffer] = [current.buffer.version, result.map { |token| token.merge(name: types[token[:type]]) }]
391
- when :inlayHint
392
- hints = result || []
393
- raise Error, "invalid inlay hints" unless hints.is_a?(Array)
394
- raise Error, "too many inlay hints" if hints.length > 10_000
395
- blocks = hints.map do |hint|
396
- raise Error, "invalid inlay hint" unless hint.is_a?(Hash) && hint["position"].is_a?(Hash)
397
- label = hint["label"]
398
- if label.is_a?(Array)
399
- raise Error, "invalid inlay hint label" unless label.all? { |part| part.is_a?(Hash) && part["value"].is_a?(String) && part["value"].valid_encoding? }
400
- label = label.map { |part| part["value"] }.join
401
- end
402
- row = hint["position"]["line"]
403
- raise Error, "invalid inlay hint" unless label.is_a?(String) && label.valid_encoding? && row.is_a?(Integer) && row.between?(0, current.buffer.line_count - 1)
404
- [row, label]
405
- end
406
- map = current.display_map
407
- map.block_map.blocks.keys.each { |id| map.remove_block(id) if id.is_a?(Array) && id.first == :inlay }
408
- blocks.each_with_index { |(row, label), index| map.insert_block([:inlay, index], row: row, text: label, kind: :inlay) }
3994
+ @semantic_styles[current.buffer] = [current.buffer.version, result.map { |token| token.to_h.merge(name: types[token[:type]]) }]
409
3995
  end
410
3996
  @window&.request_frame
411
3997
  end