klenod-lsp 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +114 -0
- data/lib/klenod/lsp/analysis.rb +29 -0
- data/lib/klenod/lsp/cli.rb +33 -0
- data/lib/klenod/lsp/diagnostics.rb +110 -0
- data/lib/klenod/lsp/documents.rb +72 -0
- data/lib/klenod/lsp/graph_index.rb +193 -0
- data/lib/klenod/lsp/languages/css.rb +45 -0
- data/lib/klenod/lsp/languages/haml/classes.rb +202 -0
- data/lib/klenod/lsp/languages/haml/completion.rb +60 -0
- data/lib/klenod/lsp/languages/haml/props.rb +111 -0
- data/lib/klenod/lsp/languages/haml/rename.rb +42 -0
- data/lib/klenod/lsp/languages/haml/ruby_regions.rb +85 -0
- data/lib/klenod/lsp/languages/haml.rb +87 -0
- data/lib/klenod/lsp/languages/imports.rb +428 -0
- data/lib/klenod/lsp/languages/ruby.rb +41 -0
- data/lib/klenod/lsp/languages/syntax.rb +64 -0
- data/lib/klenod/lsp/languages.rb +38 -0
- data/lib/klenod/lsp/renames.rb +141 -0
- data/lib/klenod/lsp/routes.rb +105 -0
- data/lib/klenod/lsp/server.rb +568 -0
- data/lib/klenod/lsp/symbols.rb +177 -0
- data/lib/klenod/lsp/text.rb +78 -0
- data/lib/klenod/lsp/version.rb +7 -0
- data/lib/klenod/lsp/workspace.rb +155 -0
- data/lib/klenod/lsp.rb +9 -0
- metadata +96 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "json"
|
|
5
|
+
require "language_server-protocol"
|
|
6
|
+
require "logger"
|
|
7
|
+
|
|
8
|
+
require_relative "documents"
|
|
9
|
+
require_relative "graph_index"
|
|
10
|
+
require_relative "languages"
|
|
11
|
+
require_relative "renames"
|
|
12
|
+
require_relative "symbols"
|
|
13
|
+
require_relative "text"
|
|
14
|
+
require_relative "version"
|
|
15
|
+
require_relative "workspace"
|
|
16
|
+
|
|
17
|
+
module Klenod
|
|
18
|
+
module LSP
|
|
19
|
+
# A single-threaded Language Server Protocol server over stdio.
|
|
20
|
+
#
|
|
21
|
+
# Frameworks start it with their own build context, collected in
|
|
22
|
+
# analysis mode so plugins skip asset work:
|
|
23
|
+
#
|
|
24
|
+
# context = config.context(mode: :development, analysis: true)
|
|
25
|
+
# Klenod::LSP::Server.new(context: context, entrypoints: config.entrypoints).start
|
|
26
|
+
#
|
|
27
|
+
# Open documents are transformed with the context's plugins and their
|
|
28
|
+
# build errors published as diagnostics. The same context also holds a
|
|
29
|
+
# collected module graph for cross-file features, filled in the
|
|
30
|
+
# background. Nothing is ever evaluated.
|
|
31
|
+
class Server
|
|
32
|
+
Protocol = LanguageServer::Protocol
|
|
33
|
+
Interface = Protocol::Interface
|
|
34
|
+
Constant = Protocol::Constant
|
|
35
|
+
|
|
36
|
+
DIAGNOSTICS_DEBOUNCE = 0.1
|
|
37
|
+
COLLECTION_DEBOUNCE = 0.25
|
|
38
|
+
|
|
39
|
+
HANDLERS = {
|
|
40
|
+
"initialize" => :handle_initialize,
|
|
41
|
+
"initialized" => :handle_initialized,
|
|
42
|
+
"shutdown" => :handle_shutdown,
|
|
43
|
+
"exit" => :handle_exit,
|
|
44
|
+
"textDocument/didOpen" => :handle_did_open,
|
|
45
|
+
"textDocument/didChange" => :handle_did_change,
|
|
46
|
+
"textDocument/didSave" => :handle_did_save,
|
|
47
|
+
"textDocument/didClose" => :handle_did_close,
|
|
48
|
+
"textDocument/definition" => :handle_definition,
|
|
49
|
+
"textDocument/hover" => :handle_hover,
|
|
50
|
+
"textDocument/completion" => :handle_completion,
|
|
51
|
+
"textDocument/documentLink" => :handle_document_link,
|
|
52
|
+
"textDocument/codeAction" => :handle_code_action,
|
|
53
|
+
"textDocument/references" => :handle_references,
|
|
54
|
+
"workspace/willRenameFiles" => :handle_will_rename_files,
|
|
55
|
+
"textDocument/documentSymbol" => :handle_document_symbol,
|
|
56
|
+
"workspace/symbol" => :handle_workspace_symbol,
|
|
57
|
+
"textDocument/codeLens" => :handle_code_lens,
|
|
58
|
+
"textDocument/prepareRename" => :handle_prepare_rename,
|
|
59
|
+
"textDocument/rename" => :handle_rename,
|
|
60
|
+
"workspace/didChangeConfiguration" => :handle_noop,
|
|
61
|
+
"workspace/didChangeWatchedFiles" => :handle_did_change_watched_files,
|
|
62
|
+
"$/cancelRequest" => :handle_noop,
|
|
63
|
+
"$/setTrace" => :handle_noop
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
# Reports background collection through window/workDoneProgress when
|
|
67
|
+
# the client supports it, and stays silent otherwise.
|
|
68
|
+
class WorkDoneProgress
|
|
69
|
+
TOKEN = "klenod-lsp.graph-index"
|
|
70
|
+
|
|
71
|
+
def initialize(server, enabled:)
|
|
72
|
+
@server = server
|
|
73
|
+
@enabled = enabled
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def begin(total)
|
|
77
|
+
return unless @enabled
|
|
78
|
+
|
|
79
|
+
@server.request("window/workDoneProgress/create", Interface::WorkDoneProgressCreateParams.new(token: TOKEN))
|
|
80
|
+
notify(Interface::WorkDoneProgressBegin.new(kind: "begin", title: "Klenod: indexing modules", message: "0 of #{total}", percentage: 0))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def report(done, total)
|
|
84
|
+
return unless @enabled
|
|
85
|
+
|
|
86
|
+
percentage = total.zero? ? 100 : (done * 100 / total)
|
|
87
|
+
notify(Interface::WorkDoneProgressReport.new(kind: "report", message: "#{done} of #{total}", percentage: percentage))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def finish
|
|
91
|
+
return unless @enabled
|
|
92
|
+
|
|
93
|
+
notify(Interface::WorkDoneProgressEnd.new(kind: "end", message: "Klenod: modules indexed"))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def notify(value)
|
|
99
|
+
@server.notify("$/progress", Interface::ProgressParams.new(token: TOKEN, value: value))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def initialize(context:, entrypoints: [], input: $stdin, output: $stdout, logger: nil)
|
|
104
|
+
@reader = Protocol::Transport::Io::Reader.new(input)
|
|
105
|
+
@writer = Protocol::Transport::Io::Writer.new(output)
|
|
106
|
+
@logger = logger || Logger.new($stderr, progname: "klenod-lsp")
|
|
107
|
+
@workspace = Workspace.new(context: context)
|
|
108
|
+
@documents = Documents.new(@workspace)
|
|
109
|
+
@index = GraphIndex.new(workspace: @workspace, entrypoints: entrypoints, logger: @logger)
|
|
110
|
+
@pending_collections = {}
|
|
111
|
+
@closed_diagnostics = {}
|
|
112
|
+
@client_capabilities = {}
|
|
113
|
+
@position_encoding = Constant::PositionEncodingKind::UTF16
|
|
114
|
+
@next_request_id = 0
|
|
115
|
+
@shutdown_requested = false
|
|
116
|
+
@exit_requested = false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Runs until the client sends `exit` or closes the input. Returns the
|
|
120
|
+
# exit status the protocol expects: 0 after `shutdown`, 1 otherwise.
|
|
121
|
+
#
|
|
122
|
+
# Everything runs inside one Async reactor: reading the client is
|
|
123
|
+
# fiber-aware, and background collection is a task in the same
|
|
124
|
+
# reactor, so no locking is needed around the graph.
|
|
125
|
+
def start
|
|
126
|
+
with_protocol_stdout do
|
|
127
|
+
Sync do |task|
|
|
128
|
+
@task = task
|
|
129
|
+
@reader.read do |message|
|
|
130
|
+
dispatch(message)
|
|
131
|
+
break if @exit_requested
|
|
132
|
+
end
|
|
133
|
+
ensure
|
|
134
|
+
@pending_collections.each_value(&:stop)
|
|
135
|
+
@index.stop
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
@shutdown_requested ? 0 : 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def notify(method_name, params)
|
|
143
|
+
@writer.write(method: method_name, params: encode_positions(params))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Server-to-client requests. Their responses arrive without a method
|
|
147
|
+
# and are ignored by dispatch, because nothing here depends on them.
|
|
148
|
+
def request(method_name, params)
|
|
149
|
+
@writer.write(id: "klenod-lsp-#{@next_request_id += 1}", method: method_name, params: encode_positions(params))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# Plugins may print while transforming. The protocol stream is the
|
|
155
|
+
# writer's IO, so anything written to $stdout meanwhile goes to stderr,
|
|
156
|
+
# which editors show in their language server log.
|
|
157
|
+
def with_protocol_stdout
|
|
158
|
+
previous_stdout = $stdout
|
|
159
|
+
$stdout = $stderr
|
|
160
|
+
yield
|
|
161
|
+
ensure
|
|
162
|
+
$stdout = previous_stdout
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def dispatch(message)
|
|
166
|
+
method_name = message[:method]
|
|
167
|
+
return unless method_name
|
|
168
|
+
|
|
169
|
+
handler = HANDLERS[method_name]
|
|
170
|
+
if handler
|
|
171
|
+
result = send(handler, message)
|
|
172
|
+
reply(message, result) if request?(message)
|
|
173
|
+
elsif request?(message)
|
|
174
|
+
reply_error(message, Constant::ErrorCodes::METHOD_NOT_FOUND, "Unsupported method: #{method_name}")
|
|
175
|
+
else
|
|
176
|
+
@logger.debug { "Ignoring notification #{method_name}" }
|
|
177
|
+
end
|
|
178
|
+
rescue Languages::ImportNavigation::InvalidRename => error
|
|
179
|
+
reply_error(message, Constant::ErrorCodes::INVALID_PARAMS, error.message) if request?(message)
|
|
180
|
+
rescue => error
|
|
181
|
+
@logger.error { "#{error.class}: #{error.message}\n#{Array(error.backtrace).join("\n")}" }
|
|
182
|
+
reply_error(message, Constant::ErrorCodes::INTERNAL_ERROR, "#{error.class}: #{error.message}") if request?(message)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def request?(message)
|
|
186
|
+
message.key?(:id)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def reply(message, result)
|
|
190
|
+
uri = message.dig(:params, :textDocument, :uri)&.to_s
|
|
191
|
+
@writer.write(id: message[:id], result: encode_positions(result, uri: uri))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def reply_error(message, code, text)
|
|
195
|
+
@writer.write(id: message[:id], error: Interface::ResponseError.new(code: code, message: text))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Positions are kept as Ruby character offsets internally and converted
|
|
199
|
+
# at the protocol boundary to the encoding selected by the client.
|
|
200
|
+
def handle_initialize(message)
|
|
201
|
+
@client_capabilities = message.dig(:params, :capabilities) || {}
|
|
202
|
+
encodings = Array(@client_capabilities.dig(:general, :positionEncodings))
|
|
203
|
+
supported = [Constant::PositionEncodingKind::UTF32, Constant::PositionEncodingKind::UTF8, Constant::PositionEncodingKind::UTF16]
|
|
204
|
+
@position_encoding = supported.find { |encoding| encodings.include?(encoding) } || Constant::PositionEncodingKind::UTF16
|
|
205
|
+
position_encoding = @position_encoding unless @position_encoding == Constant::PositionEncodingKind::UTF16
|
|
206
|
+
|
|
207
|
+
Interface::InitializeResult.new(
|
|
208
|
+
capabilities: Interface::ServerCapabilities.new(
|
|
209
|
+
position_encoding: position_encoding,
|
|
210
|
+
text_document_sync: Interface::TextDocumentSyncOptions.new(
|
|
211
|
+
open_close: true,
|
|
212
|
+
change: Constant::TextDocumentSyncKind::FULL,
|
|
213
|
+
save: true
|
|
214
|
+
),
|
|
215
|
+
definition_provider: true,
|
|
216
|
+
hover_provider: true,
|
|
217
|
+
completion_provider: Interface::CompletionOptions.new(trigger_characters: ["%", "/", "\"", "'", ".", ":"]),
|
|
218
|
+
document_link_provider: Interface::DocumentLinkOptions.new,
|
|
219
|
+
code_action_provider: Interface::CodeActionOptions.new(code_action_kinds: [Constant::CodeActionKind::QUICK_FIX]),
|
|
220
|
+
references_provider: true,
|
|
221
|
+
document_symbol_provider: true,
|
|
222
|
+
workspace_symbol_provider: true,
|
|
223
|
+
code_lens_provider: Interface::CodeLensOptions.new,
|
|
224
|
+
rename_provider: Interface::RenameOptions.new(prepare_provider: true),
|
|
225
|
+
workspace: {
|
|
226
|
+
fileOperations: Interface::FileOperationOptions.new(
|
|
227
|
+
will_rename: Interface::FileOperationRegistrationOptions.new(
|
|
228
|
+
filters: [
|
|
229
|
+
Interface::FileOperationFilter.new(
|
|
230
|
+
scheme: "file",
|
|
231
|
+
pattern: Interface::FileOperationPattern.new(glob: File.join(@workspace.source_dir, "**"))
|
|
232
|
+
)
|
|
233
|
+
]
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
server_info: {name: "klenod", version: VERSION}
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def handle_noop(_message)
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Ask the editor to report file changes under the source directory so
|
|
247
|
+
# the graph and diagnostics follow files created, changed, or removed
|
|
248
|
+
# outside the open documents, then start indexing in the background.
|
|
249
|
+
# Clients without dynamic registration need a static watcher
|
|
250
|
+
# configuration instead.
|
|
251
|
+
def handle_initialized(_message)
|
|
252
|
+
register_file_watchers if @client_capabilities.dig(:workspace, :didChangeWatchedFiles, :dynamicRegistration)
|
|
253
|
+
progress_supported = @client_capabilities.dig(:window, :workDoneProgress) == true
|
|
254
|
+
@index.start(@task, progress: WorkDoneProgress.new(self, enabled: progress_supported)) { publish_workspace_diagnostics }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def register_file_watchers
|
|
258
|
+
request(
|
|
259
|
+
"client/registerCapability",
|
|
260
|
+
Interface::RegistrationParams.new(
|
|
261
|
+
registrations: [
|
|
262
|
+
Interface::Registration.new(
|
|
263
|
+
id: "klenod-lsp.watched-files",
|
|
264
|
+
method: "workspace/didChangeWatchedFiles",
|
|
265
|
+
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
|
|
266
|
+
watchers: [Interface::FileSystemWatcher.new(glob_pattern: File.join(@workspace.source_dir, "**", "*"))]
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Changes on disk go through the build's invalidation, which keeps the
|
|
275
|
+
# graph's records, resolver cache, and companion ownership current.
|
|
276
|
+
# Every open document whose record may have changed is re-analyzed. A
|
|
277
|
+
# document's own file is skipped: the editor already reported that
|
|
278
|
+
# save through didSave.
|
|
279
|
+
def handle_did_change_watched_files(message)
|
|
280
|
+
changed_paths = []
|
|
281
|
+
removed_paths = []
|
|
282
|
+
Array(message.dig(:params, :changes)).each do |change|
|
|
283
|
+
path = @workspace.path_for_uri(change[:uri].to_s)
|
|
284
|
+
next unless path
|
|
285
|
+
|
|
286
|
+
if change[:type] == Constant::FileChangeType::DELETED
|
|
287
|
+
removed_paths << path
|
|
288
|
+
else
|
|
289
|
+
changed_paths << path
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
return if changed_paths.empty? && removed_paths.empty?
|
|
293
|
+
|
|
294
|
+
@workspace.routes.invalidate
|
|
295
|
+
affected = @index.invalidate(changed_paths, removed_paths)
|
|
296
|
+
own_paths = changed_paths + removed_paths
|
|
297
|
+
|
|
298
|
+
@documents.each do |document|
|
|
299
|
+
next if own_paths.include?(document.path)
|
|
300
|
+
next unless Languages.for(document)
|
|
301
|
+
next unless affected.include?(document.module_id.to_s)
|
|
302
|
+
|
|
303
|
+
@documents.invalidate(document.uri)
|
|
304
|
+
publish_diagnostics(document)
|
|
305
|
+
end
|
|
306
|
+
publish_workspace_diagnostics
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Modules the index could not collect get diagnostics even while
|
|
310
|
+
# closed, so a rename or deletion that breaks importers shows up in the
|
|
311
|
+
# editor's problem list. Open documents publish their own; a module
|
|
312
|
+
# that recovers has its diagnostics cleared.
|
|
313
|
+
def publish_workspace_diagnostics
|
|
314
|
+
current = {}
|
|
315
|
+
@index.failed.each_key do |module_id_string|
|
|
316
|
+
module_id = Klenod::Build::ModuleId.new(module_id_string)
|
|
317
|
+
next unless GraphIndex::ROOT_EXTENSIONS.include?(module_id.extname)
|
|
318
|
+
|
|
319
|
+
path = @workspace.path_for_module_id(module_id)
|
|
320
|
+
next unless path && File.file?(path)
|
|
321
|
+
|
|
322
|
+
uri = @workspace.uri_for_path(path)
|
|
323
|
+
next if @documents.fetch(uri)
|
|
324
|
+
|
|
325
|
+
document = Document.new(uri: uri, path: path, module_id: module_id, text: File.read(path), version: nil)
|
|
326
|
+
language = Languages.for(document)
|
|
327
|
+
next unless language
|
|
328
|
+
|
|
329
|
+
diagnostics = language.diagnostics(@workspace.analyze(module_id, document.text), @workspace, @index)
|
|
330
|
+
current[uri] = diagnostics unless diagnostics.empty?
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
(@closed_diagnostics.keys - current.keys).each do |uri|
|
|
334
|
+
notify("textDocument/publishDiagnostics", Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []))
|
|
335
|
+
end
|
|
336
|
+
current.each do |uri, diagnostics|
|
|
337
|
+
next if @closed_diagnostics[uri] == JSON.generate(diagnostics)
|
|
338
|
+
|
|
339
|
+
notify("textDocument/publishDiagnostics", Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: diagnostics))
|
|
340
|
+
end
|
|
341
|
+
@closed_diagnostics = current.transform_values { |diagnostics| JSON.generate(diagnostics) }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def handle_shutdown(_message)
|
|
345
|
+
@shutdown_requested = true
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def handle_exit(_message)
|
|
350
|
+
@exit_requested = true
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def handle_did_open(message)
|
|
354
|
+
text_document = message.dig(:params, :textDocument)
|
|
355
|
+
document = @documents.open(uri: text_document[:uri], text: text_document[:text], version: text_document[:version])
|
|
356
|
+
publish_diagnostics(document)
|
|
357
|
+
overlay(document)
|
|
358
|
+
collect_document(document, force: true)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Editors send a change per keystroke; diagnostics wait for a short
|
|
362
|
+
# pause and the graph record for a slightly longer one. Requests that
|
|
363
|
+
# arrive meanwhile analyze the current text on demand.
|
|
364
|
+
def handle_did_change(message)
|
|
365
|
+
params = message[:params]
|
|
366
|
+
change = Array(params[:contentChanges]).last
|
|
367
|
+
return unless change
|
|
368
|
+
|
|
369
|
+
document = @documents.change(uri: params.dig(:textDocument, :uri), text: change[:text], version: params.dig(:textDocument, :version))
|
|
370
|
+
return unless Languages.for(document)
|
|
371
|
+
|
|
372
|
+
@pending_collections.delete(document.uri)&.stop
|
|
373
|
+
@pending_collections[document.uri] =
|
|
374
|
+
@task.async do |task|
|
|
375
|
+
task.sleep(DIAGNOSTICS_DEBOUNCE)
|
|
376
|
+
publish_diagnostics(document)
|
|
377
|
+
overlay(document)
|
|
378
|
+
task.sleep(COLLECTION_DEBOUNCE - DIAGNOSTICS_DEBOUNCE)
|
|
379
|
+
@pending_collections.delete(document.uri)
|
|
380
|
+
@index.ensure_collected(document.module_id, force: true)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Companion files may have changed on disk, so the cached analysis is
|
|
385
|
+
# dropped even though the document version did not change.
|
|
386
|
+
def handle_did_save(message)
|
|
387
|
+
document = @documents.fetch(message.dig(:params, :textDocument, :uri))
|
|
388
|
+
return unless document
|
|
389
|
+
|
|
390
|
+
@documents.invalidate(document.uri)
|
|
391
|
+
publish_diagnostics(document)
|
|
392
|
+
collect_document(document, force: true)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# The record follows disk again once the editor lets go of the buffer.
|
|
396
|
+
def handle_did_close(message)
|
|
397
|
+
uri = message.dig(:params, :textDocument, :uri)
|
|
398
|
+
document = @documents.close(uri)
|
|
399
|
+
return unless document && Languages.for(document)
|
|
400
|
+
|
|
401
|
+
@pending_collections.delete(uri)&.stop
|
|
402
|
+
@workspace.context.graph.clear_source_override(document.module_id)
|
|
403
|
+
collect_document(document, force: true)
|
|
404
|
+
notify("textDocument/publishDiagnostics", Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []))
|
|
405
|
+
publish_workspace_diagnostics
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# The graph reads open documents from their buffers rather than disk,
|
|
409
|
+
# and reuses the diagnostics transform when the buffer had no errors so
|
|
410
|
+
# collection does not transform the same text twice.
|
|
411
|
+
def overlay(document)
|
|
412
|
+
return unless Languages.for(document)
|
|
413
|
+
|
|
414
|
+
analysis = @documents.cached_analysis(document)
|
|
415
|
+
transform = analysis&.transform if analysis && analysis.build_error.nil? && analysis.resolve_errors.empty?
|
|
416
|
+
@workspace.context.graph.override_source(document.module_id, document.text, transform: transform)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def collect_document(document, force: false)
|
|
420
|
+
return unless Languages.for(document)
|
|
421
|
+
|
|
422
|
+
@pending_collections.delete(document.uri)&.stop
|
|
423
|
+
@index.ensure_collected(document.module_id, force: force)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def handle_definition(message)
|
|
427
|
+
with_position(message) { |language, analysis, position| language.definition(analysis, position, @workspace, @index) }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def handle_hover(message)
|
|
431
|
+
with_position(message) { |language, analysis, position| language.hover(analysis, position, @workspace, @index) }
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def handle_completion(message)
|
|
435
|
+
with_position(message) { |language, analysis, position| language.completion(analysis, position, @workspace, @index) }
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def handle_references(message)
|
|
439
|
+
with_position(message) do |language, analysis, position|
|
|
440
|
+
include_declaration = message.dig(:params, :context, :includeDeclaration) == true
|
|
441
|
+
language.references(analysis, position, @workspace, @index, include_declaration: include_declaration)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# The editor asks before renaming or moving files, and applies the
|
|
446
|
+
# returned edits first; the watched-file events that follow bring the
|
|
447
|
+
# graph up to date.
|
|
448
|
+
def handle_will_rename_files(message)
|
|
449
|
+
files = Array(message.dig(:params, :files)).map { |file| [file[:oldUri].to_s, file[:newUri].to_s] }
|
|
450
|
+
Renames.call(files, @index, @workspace)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def handle_prepare_rename(message)
|
|
454
|
+
with_position(message) { |language, analysis, position| language.prepare_rename(analysis, position) }
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def handle_rename(message)
|
|
458
|
+
with_position(message) do |language, analysis, position|
|
|
459
|
+
language.rename(analysis, position, message.dig(:params, :newName).to_s, @workspace)
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def handle_code_lens(message)
|
|
464
|
+
with_document(message) { |language, analysis| language.code_lenses(analysis, @workspace, @index) }
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def handle_document_symbol(message)
|
|
468
|
+
with_document(message) { |language, analysis| language.document_symbols(analysis) }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def handle_workspace_symbol(message)
|
|
472
|
+
Symbols.workspace_symbols(message.dig(:params, :query), @index, @workspace)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def handle_document_link(message)
|
|
476
|
+
with_document(message) { |language, analysis| language.document_links(analysis, @workspace) }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def handle_code_action(message)
|
|
480
|
+
with_document(message) do |language, analysis|
|
|
481
|
+
range = message.dig(:params, :range)
|
|
482
|
+
language.code_actions(analysis, range.dig(:start, :line)..range.dig(:end, :line), @workspace)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Document requests share the same shape: nothing for documents the
|
|
487
|
+
# server does not handle, otherwise the language handler answers from
|
|
488
|
+
# the document's analysis.
|
|
489
|
+
def with_document(message)
|
|
490
|
+
document = @documents.fetch(message.dig(:params, :textDocument, :uri))
|
|
491
|
+
language = document && Languages.for(document)
|
|
492
|
+
return nil unless language
|
|
493
|
+
|
|
494
|
+
yield language, @documents.analysis_for(document)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def with_position(message)
|
|
498
|
+
with_document(message) do |language, analysis|
|
|
499
|
+
params = message[:params]
|
|
500
|
+
line = params.dig(:position, :line)
|
|
501
|
+
character = params.dig(:position, :character)
|
|
502
|
+
line_text = analysis.lines[line].to_s
|
|
503
|
+
position = Text::Position.new(line: line, character: Text.ruby_character(line_text, character, @position_encoding))
|
|
504
|
+
yield language, analysis, position
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def encode_positions(value, uri: nil)
|
|
509
|
+
serialized = JSON.parse(JSON.generate(value), symbolize_names: true)
|
|
510
|
+
encode_position_nodes(serialized, uri, {})
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def encode_position_nodes(value, uri, sources)
|
|
514
|
+
case value
|
|
515
|
+
when Array
|
|
516
|
+
value.map { |item| encode_position_nodes(item, uri, sources) }
|
|
517
|
+
when Hash
|
|
518
|
+
current_uri = value[:uri]&.to_s || uri
|
|
519
|
+
if value.key?(:line) && value.key?(:character)
|
|
520
|
+
line_text = source_line(current_uri, value[:line], sources)
|
|
521
|
+
return value.merge(character: Text.protocol_character(line_text, value[:character], @position_encoding)) if line_text
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
value.to_h do |key, child|
|
|
525
|
+
encoded =
|
|
526
|
+
if key == :changes && child.is_a?(Hash)
|
|
527
|
+
child.to_h { |change_uri, edits| [change_uri, encode_position_nodes(edits, change_uri.to_s, sources)] }
|
|
528
|
+
else
|
|
529
|
+
encode_position_nodes(child, current_uri, sources)
|
|
530
|
+
end
|
|
531
|
+
[key, encoded]
|
|
532
|
+
end
|
|
533
|
+
else
|
|
534
|
+
value
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def source_line(uri, line, sources)
|
|
539
|
+
return nil unless uri
|
|
540
|
+
|
|
541
|
+
lines = sources.fetch(uri) do
|
|
542
|
+
document = @documents.fetch(uri)
|
|
543
|
+
source = document&.text
|
|
544
|
+
unless source
|
|
545
|
+
path = @workspace.path_for_uri(uri)
|
|
546
|
+
source = File.read(path) if path && File.file?(path)
|
|
547
|
+
end
|
|
548
|
+
sources[uri] = source&.lines(chomp: true)
|
|
549
|
+
end
|
|
550
|
+
lines&.[](line)
|
|
551
|
+
rescue SystemCallError
|
|
552
|
+
sources[uri] = nil
|
|
553
|
+
nil
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def publish_diagnostics(document)
|
|
557
|
+
language = Languages.for(document)
|
|
558
|
+
return unless language
|
|
559
|
+
|
|
560
|
+
diagnostics = language.diagnostics(@documents.analysis_for(document), @workspace, @index)
|
|
561
|
+
notify(
|
|
562
|
+
"textDocument/publishDiagnostics",
|
|
563
|
+
Interface::PublishDiagnosticsParams.new(uri: document.uri, version: document.version, diagnostics: diagnostics)
|
|
564
|
+
)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|