canopus 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
|
@@ -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
|
|
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
|
-
|
|
84
|
+
@main_queue << block
|
|
35
85
|
@window&.request_frame
|
|
36
86
|
end
|
|
37
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
client
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 []
|
|
90
|
-
uri =
|
|
91
|
-
@
|
|
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|
|
|
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(
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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(
|
|
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
|
|
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" },
|
|
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 =
|
|
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
|
-
|
|
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
|