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
data/lib/canopus/lsp/client.rb
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Canopus
|
|
4
|
-
module LSP
|
|
5
|
-
class Client
|
|
6
|
-
attr_reader :capabilities, :transport, :diagnostics, :state, :errors, :server_info, :position_encoding
|
|
7
|
-
def initialize(command:, root: Dir.pwd, dispatch: ->(&block) { block.call }, restart: true, env: {}, initialization_options: nil, configuration: {})
|
|
8
|
-
@command, @root, @dispatch, @restart = command, File.expand_path(root), dispatch, restart
|
|
9
|
-
@env, @initialization_options, @configuration = env, initialization_options, configuration
|
|
10
|
-
@pending, @handlers, @documents, @diagnostics, @semantic = {}, {}, {}, {}, {}
|
|
11
|
-
@sequence, @lock, @state, @restarts, @epoch = 0, Mutex.new, :stopped, 0, 0
|
|
12
|
-
@errors, @capabilities = [], {}
|
|
13
|
-
end
|
|
14
|
-
def start(timeout: 10)
|
|
15
|
-
raise Error, "language server is already started" if [:starting, :running].include?(@state)
|
|
16
|
-
@closing = false
|
|
17
|
-
@state = :starting
|
|
18
|
-
epoch = (@epoch += 1)
|
|
19
|
-
@semantic.clear
|
|
20
|
-
@transport = Transport.new(@command, cwd: @root, env: @env) { |message, error| receive(message, error, epoch) }
|
|
21
|
-
result = request("initialize", {processId: Process.pid, rootUri: Protocol.uri(@root),
|
|
22
|
-
clientInfo: {name: "Canopus", version: (defined?(Canopus::VERSION) ? Canopus::VERSION : "0.1.0")},
|
|
23
|
-
workspaceFolders: [{uri: Protocol.uri(@root), name: File.basename(@root)}], initializationOptions: @initialization_options,
|
|
24
|
-
capabilities: {general: {positionEncodings: ["utf-16"]},
|
|
25
|
-
window: {workDoneProgress: true},
|
|
26
|
-
textDocument: {synchronization: {dynamicRegistration: false, didSave: true},
|
|
27
|
-
completion: {completionItem: {snippetSupport: true, documentationFormat: %w[markdown plaintext], resolveSupport: {properties: %w[documentation detail additionalTextEdits]}}},
|
|
28
|
-
hover: {contentFormat: %w[markdown plaintext]},
|
|
29
|
-
signatureHelp: {signatureInformation: {documentationFormat: %w[markdown plaintext], parameterInformation: {labelOffsetSupport: true}}},
|
|
30
|
-
documentSymbol: {hierarchicalDocumentSymbolSupport: true},
|
|
31
|
-
codeAction: {codeActionLiteralSupport: {codeActionKind: {valueSet: %w[quickfix refactor refactor.extract refactor.inline refactor.rewrite source source.organizeImports]}}, resolveSupport: {properties: ["edit"]}},
|
|
32
|
-
publishDiagnostics: {relatedInformation: true, versionSupport: true},
|
|
33
|
-
diagnostic: {dynamicRegistration: false, relatedDocumentSupport: false},
|
|
34
|
-
inlayHint: {dynamicRegistration: false}, codeLens: {dynamicRegistration: false},
|
|
35
|
-
semanticTokens: {requests: {full: {delta: true}}, tokenTypes: %w[namespace type class enum interface struct typeParameter parameter variable property enumMember event function method macro keyword modifier comment string number regexp operator decorator], tokenModifiers: %w[declaration definition readonly static deprecated abstract async modification documentation defaultLibrary], formats: ["relative"], overlappingTokenSupport: false, multilineTokenSupport: false}},
|
|
36
|
-
workspace: {applyEdit: true, configuration: true, workspaceFolders: true,
|
|
37
|
-
workspaceEdit: {documentChanges: true, resourceOperations: %w[create rename delete], failureHandling: "abort"}}}}).await(timeout: timeout)
|
|
38
|
-
raise Error, "invalid initialize result" unless result.is_a?(Hash) && result["capabilities"].is_a?(Hash)
|
|
39
|
-
@capabilities, @server_info = result["capabilities"], result["serverInfo"]
|
|
40
|
-
sync = @capabilities["textDocumentSync"]
|
|
41
|
-
mode = sync.is_a?(Hash) ? sync.fetch("change", 0) : sync
|
|
42
|
-
raise Error, "invalid text document synchronization mode" unless mode.nil? || [0, 1, 2].include?(mode)
|
|
43
|
-
@position_encoding = @capabilities.fetch("positionEncoding", "utf-16")
|
|
44
|
-
raise Error, "server selected unadvertised position encoding #{@position_encoding}" unless @position_encoding == "utf-16"
|
|
45
|
-
semantic = @capabilities["semanticTokensProvider"]
|
|
46
|
-
raise Error, "missing semantic token legend" if semantic && (!semantic.is_a?(Hash) || !semantic["legend"].is_a?(Hash))
|
|
47
|
-
Protocol.semantic_tokens([], legend: semantic["legend"]) if semantic.is_a?(Hash)
|
|
48
|
-
notify("initialized", {})
|
|
49
|
-
reopen_documents if @reopening_documents
|
|
50
|
-
@state = :running
|
|
51
|
-
self
|
|
52
|
-
rescue StandardError => error
|
|
53
|
-
@transport&.close if epoch
|
|
54
|
-
fail_pending(error) if epoch
|
|
55
|
-
@state = :failed if epoch
|
|
56
|
-
raise
|
|
57
|
-
end
|
|
58
|
-
def request(method, params = {})
|
|
59
|
-
id = @lock.synchronize { @sequence += 1 }
|
|
60
|
-
future = Future.new(id, on_error: method(:report_error)) { |number| cancel(number) }
|
|
61
|
-
@lock.synchronize { @pending[id] = future }
|
|
62
|
-
raise Error, "language server is not connected" unless @transport&.alive?
|
|
63
|
-
@transport.write(jsonrpc: "2.0", id: id, method: method.to_s, params: params)
|
|
64
|
-
future
|
|
65
|
-
rescue StandardError => error
|
|
66
|
-
@lock.synchronize { @pending.delete(id) }
|
|
67
|
-
future.fulfill(error: error)
|
|
68
|
-
future
|
|
69
|
-
end
|
|
70
|
-
def notify(method, params = {})
|
|
71
|
-
raise Error, "language server is not connected" unless @transport&.alive?
|
|
72
|
-
@transport.write(jsonrpc: "2.0", method: method.to_s, params: params)
|
|
73
|
-
end
|
|
74
|
-
def on(method, &handler)
|
|
75
|
-
raise ArgumentError, "handler required" unless handler
|
|
76
|
-
@handlers[method.to_s] = handler
|
|
77
|
-
end
|
|
78
|
-
def supports?(capability) = !!@capabilities[capability.to_s]
|
|
79
|
-
def workspace_symbols(query) = request("workspace/symbol", {query: query})
|
|
80
|
-
def resolve_completion(item) = request("completionItem/resolve", item)
|
|
81
|
-
def resolve_code_action(action) = request("codeAction/resolve", action)
|
|
82
|
-
def resolve_code_lens(lens) = request("codeLens/resolve", lens)
|
|
83
|
-
def execute_command(command, arguments: []) = request("workspace/executeCommand", {command: command, arguments: arguments})
|
|
84
|
-
|
|
85
|
-
def open_document(buffer, language_id:)
|
|
86
|
-
raise Error, "LSP document needs a path" unless buffer.path
|
|
87
|
-
uri = Protocol.uri(buffer.path)
|
|
88
|
-
@documents[uri]&.last&.detach
|
|
89
|
-
subscription = buffer.on_edit { |patch| change_document(uri, buffer, patch) }
|
|
90
|
-
@documents[uri] = [buffer, language_id, subscription]
|
|
91
|
-
notify("textDocument/didOpen", {textDocument: {uri: uri, languageId: language_id, version: buffer.version, text: buffer.text}}) if open_close?
|
|
92
|
-
uri
|
|
93
|
-
end
|
|
94
|
-
def close_document(uri)
|
|
95
|
-
@documents.delete(uri)&.last&.detach
|
|
96
|
-
@diagnostics.delete(uri)
|
|
97
|
-
@semantic.delete(uri)
|
|
98
|
-
notify("textDocument/didClose", {textDocument: {uri: uri}}) if open_close?
|
|
99
|
-
end
|
|
100
|
-
def save_document(uri)
|
|
101
|
-
sync = @capabilities["textDocumentSync"]
|
|
102
|
-
save = sync.is_a?(Hash) ? sync["save"] : sync.is_a?(Integer) && sync.positive?
|
|
103
|
-
return unless save
|
|
104
|
-
params = {textDocument: {uri: uri}}
|
|
105
|
-
params[:text] = @documents.fetch(uri).first.text if save.is_a?(Hash) && save["includeText"]
|
|
106
|
-
notify("textDocument/didSave", params)
|
|
107
|
-
end
|
|
108
|
-
def at(method, buffer, offset, **params)
|
|
109
|
-
request("textDocument/#{method}", {textDocument: {uri: Protocol.uri(buffer.path)}, position: Protocol.position(buffer.rope, offset), **params})
|
|
110
|
-
end
|
|
111
|
-
%w[completion hover definition typeDefinition implementation references rename signatureHelp].each do |method|
|
|
112
|
-
define_method(method) { |buffer, offset, **params| at(method, buffer, offset, **params) }
|
|
113
|
-
end
|
|
114
|
-
%w[documentSymbol formatting codeAction inlayHint codeLens diagnostic].each do |method|
|
|
115
|
-
define_method(method) { |buffer, **params| request("textDocument/#{method}", {textDocument: {uri: Protocol.uri(buffer.path)}, **params}) }
|
|
116
|
-
end
|
|
117
|
-
def semantic_tokens(buffer)
|
|
118
|
-
uri = Protocol.uri(buffer.path)
|
|
119
|
-
version = buffer.version
|
|
120
|
-
provider = @capabilities["semanticTokensProvider"]
|
|
121
|
-
return [] unless provider.is_a?(Hash) && provider["full"]
|
|
122
|
-
previous = @semantic[uri]
|
|
123
|
-
delta = previous && previous[0] && provider["full"].is_a?(Hash) && provider["full"]["delta"]
|
|
124
|
-
method = delta ? "textDocument/semanticTokens/full/delta" : "textDocument/semanticTokens/full"
|
|
125
|
-
params = {textDocument: {uri: uri}}
|
|
126
|
-
params[:previousResultId] = previous[0] if delta
|
|
127
|
-
result = request(method, params).await
|
|
128
|
-
return [] unless result && buffer.version == version
|
|
129
|
-
raise Error, "invalid semantic token result" unless result.is_a?(Hash) && (!result.key?("resultId") || result["resultId"].is_a?(String))
|
|
130
|
-
raise Error, "unexpected semantic token delta" if !delta && !result.key?("data")
|
|
131
|
-
data = result["data"] || Protocol.semantic_delta(previous[1], result["edits"])
|
|
132
|
-
tokens = Protocol.semantic_tokens(data, legend: provider["legend"])
|
|
133
|
-
@semantic[uri] = [result["resultId"], data]
|
|
134
|
-
tokens
|
|
135
|
-
end
|
|
136
|
-
def apply_text_edits(buffer, edits)
|
|
137
|
-
changes = edits.map do |edit|
|
|
138
|
-
raise Error, "invalid LSP text edit" unless edit.is_a?(Hash) && edit["range"].is_a?(Hash) && edit["newText"].is_a?(String) && edit["newText"].valid_encoding?
|
|
139
|
-
range = edit.fetch("range")
|
|
140
|
-
[Protocol.offset(buffer.rope, range.fetch("start"))...Protocol.offset(buffer.rope, range.fetch("end")), edit.fetch("newText")]
|
|
141
|
-
end
|
|
142
|
-
buffer.edit(changes, kind: :lsp)
|
|
143
|
-
end
|
|
144
|
-
def stop
|
|
145
|
-
@closing = true
|
|
146
|
-
begin
|
|
147
|
-
request("shutdown").await(timeout: 2) if @state == :running
|
|
148
|
-
notify("exit") if @transport&.alive?
|
|
149
|
-
rescue Error
|
|
150
|
-
nil
|
|
151
|
-
ensure
|
|
152
|
-
@epoch += 1
|
|
153
|
-
@documents.each_value { |_, _, subscription| subscription.detach }
|
|
154
|
-
@documents.clear
|
|
155
|
-
@semantic.clear
|
|
156
|
-
@diagnostics.clear
|
|
157
|
-
@transport&.close
|
|
158
|
-
fail_pending(Error.new("language server stopped"))
|
|
159
|
-
@state = :stopped
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
private
|
|
164
|
-
def open_close?
|
|
165
|
-
sync = @capabilities["textDocumentSync"]
|
|
166
|
-
sync.is_a?(Hash) ? sync["openClose"] : sync.is_a?(Integer) && sync.positive?
|
|
167
|
-
end
|
|
168
|
-
def change_document(uri, buffer, patch)
|
|
169
|
-
sync = @capabilities["textDocumentSync"]
|
|
170
|
-
mode = sync.is_a?(Hash) ? sync["change"] : sync
|
|
171
|
-
return unless [1, 2].include?(mode)
|
|
172
|
-
changes = if mode == 2 && patch.is_a?(Patch)
|
|
173
|
-
patch.edits.reverse.map { |edit| {range: Protocol.range(patch.before, edit.old_range), text: edit.new_text} }
|
|
174
|
-
else
|
|
175
|
-
[{text: buffer.text}]
|
|
176
|
-
end
|
|
177
|
-
notify("textDocument/didChange", {textDocument: {uri: uri, version: buffer.version}, contentChanges: changes})
|
|
178
|
-
rescue Error => error
|
|
179
|
-
report_error(error)
|
|
180
|
-
end
|
|
181
|
-
def cancel(id)
|
|
182
|
-
@lock.synchronize { @pending.delete(id) }
|
|
183
|
-
notify("$/cancelRequest", {id: id})
|
|
184
|
-
rescue Error
|
|
185
|
-
nil
|
|
186
|
-
end
|
|
187
|
-
def fail_pending(error)
|
|
188
|
-
pending = @lock.synchronize { values = @pending.values; @pending.clear; values }
|
|
189
|
-
pending.each { |future| future.fulfill(error: error) }
|
|
190
|
-
end
|
|
191
|
-
def report_error(error)
|
|
192
|
-
bounded = Error.new("#{error.class}: #{error.message}".scrub.byteslice(0, 2048).scrub(""))
|
|
193
|
-
@lock.synchronize do
|
|
194
|
-
@errors << bounded
|
|
195
|
-
@errors.shift if @errors.length > 200
|
|
196
|
-
end
|
|
197
|
-
@dispatch.call do
|
|
198
|
-
begin
|
|
199
|
-
@handlers["error"]&.call(bounded)
|
|
200
|
-
rescue StandardError
|
|
201
|
-
nil
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
rescue StandardError
|
|
205
|
-
nil
|
|
206
|
-
end
|
|
207
|
-
def receive(message, error, epoch = @epoch)
|
|
208
|
-
return unless epoch == @epoch
|
|
209
|
-
if error
|
|
210
|
-
running = @state == :running
|
|
211
|
-
@state = :failed
|
|
212
|
-
fail_pending(Error.new(error.message))
|
|
213
|
-
report_error(error)
|
|
214
|
-
restart_server if running && @restart && !@closing && @restarts < 3
|
|
215
|
-
elsif message.key?("id") && !message.key?("method")
|
|
216
|
-
future = @lock.synchronize { @pending.delete(message["id"]) }
|
|
217
|
-
future&.fulfill(message["result"], error: message["error"] && ServerError.new(message["error"]))
|
|
218
|
-
elsif message["method"]
|
|
219
|
-
method, params = message["method"], message.fetch("params", {})
|
|
220
|
-
@dispatch.call do
|
|
221
|
-
next unless epoch == @epoch && !@closing
|
|
222
|
-
begin
|
|
223
|
-
if method == "textDocument/publishDiagnostics"
|
|
224
|
-
raise Error, "invalid diagnostics notification" unless params.is_a?(Hash) && params["uri"].is_a?(String) && params["diagnostics"].is_a?(Array)
|
|
225
|
-
document = @documents[params["uri"]]
|
|
226
|
-
version = params["version"]
|
|
227
|
-
raise Error, "invalid diagnostic version" if !version.nil? && !version.is_a?(Integer)
|
|
228
|
-
next if document && version && version < document.first.version
|
|
229
|
-
@diagnostics[params["uri"]] = Protocol.diagnostics(params["diagnostics"])
|
|
230
|
-
end
|
|
231
|
-
known = @handlers.key?(method)
|
|
232
|
-
result = @handlers[method]&.call(params)
|
|
233
|
-
next unless message.key?("id")
|
|
234
|
-
unless known
|
|
235
|
-
known = true
|
|
236
|
-
result = case method
|
|
237
|
-
when "workspace/configuration"
|
|
238
|
-
params.fetch("items").map do |item|
|
|
239
|
-
section = item["section"]
|
|
240
|
-
section ? @configuration.dig(*section.split(".")) : @configuration
|
|
241
|
-
end
|
|
242
|
-
when "workspace/workspaceFolders"
|
|
243
|
-
[{uri: Protocol.uri(@root), name: File.basename(@root)}]
|
|
244
|
-
when "window/workDoneProgress/create", "workspace/semanticTokens/refresh", "workspace/inlayHint/refresh", "workspace/codeLens/refresh", "workspace/diagnostic/refresh"
|
|
245
|
-
@semantic.clear if method == "workspace/semanticTokens/refresh"
|
|
246
|
-
nil
|
|
247
|
-
else
|
|
248
|
-
known = false
|
|
249
|
-
nil
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
if !known
|
|
253
|
-
reply(message["id"], epoch, error: {code: -32601, message: "unsupported client request #{method}"})
|
|
254
|
-
elsif result.is_a?(Future)
|
|
255
|
-
result.then do |value, failure|
|
|
256
|
-
failure ? reply(message["id"], epoch, error: {code: -32603, message: failure.message.byteslice(0, 2048).scrub}) : reply(message["id"], epoch, value: value)
|
|
257
|
-
end
|
|
258
|
-
else
|
|
259
|
-
reply(message["id"], epoch, value: result)
|
|
260
|
-
end
|
|
261
|
-
rescue StandardError => failure
|
|
262
|
-
report_error(failure)
|
|
263
|
-
reply(message["id"], epoch, error: {code: -32603, message: "client request handler failed"}) if message.key?("id")
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
rescue StandardError => failure
|
|
268
|
-
report_error(failure)
|
|
269
|
-
reply(message["id"], epoch, error: {code: -32603, message: "client dispatch failed"}) if message&.key?("id") && message.key?("method")
|
|
270
|
-
end
|
|
271
|
-
def reply(id, epoch, value: nil, error: nil)
|
|
272
|
-
return unless epoch == @epoch && !@closing
|
|
273
|
-
response = {jsonrpc: "2.0", id: id}
|
|
274
|
-
error ? response[:error] = error : response[:result] = value
|
|
275
|
-
@transport.write(response)
|
|
276
|
-
rescue StandardError => failure
|
|
277
|
-
report_error(failure)
|
|
278
|
-
end
|
|
279
|
-
def restart_server
|
|
280
|
-
return if @restart_thread&.alive?
|
|
281
|
-
previous = @transport
|
|
282
|
-
@restart_thread = Thread.new do
|
|
283
|
-
@reopening_documents = true
|
|
284
|
-
begin
|
|
285
|
-
previous.close
|
|
286
|
-
until @closing || @restarts >= 3
|
|
287
|
-
@restarts += 1
|
|
288
|
-
sleep(0.2 * @restarts)
|
|
289
|
-
break if @closing
|
|
290
|
-
begin
|
|
291
|
-
start
|
|
292
|
-
break
|
|
293
|
-
rescue StandardError => failure
|
|
294
|
-
report_error(failure)
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
ensure
|
|
298
|
-
@reopening_documents = false
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
def reopen_documents
|
|
303
|
-
@documents.values.dup.each { |buffer, language, _| open_document(buffer, language_id: language) }
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
data/lib/canopus/lsp/error.rb
DELETED
data/lib/canopus/lsp/future.rb
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Canopus
|
|
4
|
-
module LSP
|
|
5
|
-
class Future
|
|
6
|
-
attr_reader :id, :callback_errors
|
|
7
|
-
def initialize(id, on_error: nil, &cancel)
|
|
8
|
-
@id, @cancel, @on_error, @lock, @ready = id, cancel, on_error, Mutex.new, ConditionVariable.new
|
|
9
|
-
@callbacks, @callback_errors = [], []
|
|
10
|
-
end
|
|
11
|
-
def fulfill(value = nil, error: nil)
|
|
12
|
-
callbacks = @lock.synchronize do
|
|
13
|
-
return if @done
|
|
14
|
-
@value, @error, @done = value, error, true
|
|
15
|
-
@ready.broadcast
|
|
16
|
-
saved, @callbacks = @callbacks, []
|
|
17
|
-
saved
|
|
18
|
-
end
|
|
19
|
-
callbacks.each { |callback| invoke(callback) }
|
|
20
|
-
self
|
|
21
|
-
end
|
|
22
|
-
def then(&callback)
|
|
23
|
-
raise ArgumentError, "callback required" unless callback
|
|
24
|
-
on_complete { callback.call(@value, @error) }
|
|
25
|
-
self
|
|
26
|
-
end
|
|
27
|
-
def done? = @lock.synchronize { !!@done }
|
|
28
|
-
def on_complete(&callback)
|
|
29
|
-
raise ArgumentError, "callback required" unless callback
|
|
30
|
-
ready = @lock.synchronize do
|
|
31
|
-
@callbacks << callback unless @done
|
|
32
|
-
@done
|
|
33
|
-
end
|
|
34
|
-
invoke(callback) if ready
|
|
35
|
-
Subscription.new { @lock.synchronize { @callbacks.delete(callback) } }
|
|
36
|
-
end
|
|
37
|
-
def await(timeout: 10)
|
|
38
|
-
raise ArgumentError, "timeout must be finite and nonnegative" unless timeout.nil? || (timeout.is_a?(Numeric) && timeout.finite? && timeout >= 0)
|
|
39
|
-
if defined?(Zaniah::TaskExecutor) && Zaniah::TaskExecutor.current && !done?
|
|
40
|
-
return Zaniah::TaskExecutor.current.await(self, timeout: timeout)
|
|
41
|
-
end
|
|
42
|
-
deadline = timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
43
|
-
@lock.synchronize do
|
|
44
|
-
until @done
|
|
45
|
-
remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
46
|
-
raise Timeout, "LSP request #{@id} timed out" if remaining && remaining <= 0
|
|
47
|
-
@ready.wait(@lock, remaining)
|
|
48
|
-
end
|
|
49
|
-
raise @error if @error
|
|
50
|
-
@value
|
|
51
|
-
end
|
|
52
|
-
rescue StandardError => error
|
|
53
|
-
if error.is_a?(Timeout) || (defined?(Zaniah::Task::Timeout) && error.is_a?(Zaniah::Task::Timeout))
|
|
54
|
-
cancel
|
|
55
|
-
raise Timeout, "LSP request #{@id} timed out"
|
|
56
|
-
end
|
|
57
|
-
raise
|
|
58
|
-
end
|
|
59
|
-
def cancel
|
|
60
|
-
callback = @lock.synchronize do
|
|
61
|
-
return false if @done || @cancelling
|
|
62
|
-
@cancelling = true
|
|
63
|
-
@cancel
|
|
64
|
-
end
|
|
65
|
-
invoke(-> { callback.call(@id) }) if callback
|
|
66
|
-
fulfill(error: Error.new("request cancelled"))
|
|
67
|
-
true
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
def invoke(callback)
|
|
72
|
-
callback.call
|
|
73
|
-
rescue StandardError => error
|
|
74
|
-
bounded = Error.new("#{error.class}: #{error.message}".scrub.byteslice(0, 2048).scrub(""))
|
|
75
|
-
@lock.synchronize do
|
|
76
|
-
@callback_errors << bounded
|
|
77
|
-
@callback_errors.shift if @callback_errors.length > 32
|
|
78
|
-
end
|
|
79
|
-
begin
|
|
80
|
-
@on_error&.call(bounded)
|
|
81
|
-
rescue StandardError
|
|
82
|
-
nil
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
data/lib/canopus/lsp/protocol.rb
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Canopus
|
|
4
|
-
module LSP
|
|
5
|
-
module Protocol
|
|
6
|
-
module_function
|
|
7
|
-
def uri(path)
|
|
8
|
-
absolute = File.expand_path(path).tr("\\", "/")
|
|
9
|
-
absolute = "/#{absolute}" if absolute.match?(/\A[A-Za-z]:/)
|
|
10
|
-
"file://" + URI::RFC2396_PARSER.escape(absolute, /[^a-zA-Z0-9\-._~\/:]/)
|
|
11
|
-
end
|
|
12
|
-
def path(uri)
|
|
13
|
-
parsed = URI.parse(uri)
|
|
14
|
-
raise Error, "expected local file URI" unless parsed.scheme == "file" && [nil, "", "localhost"].include?(parsed.host) && parsed.query.nil? && parsed.fragment.nil? && parsed.path&.start_with?("/")
|
|
15
|
-
path = URI::RFC2396_PARSER.unescape(parsed.path)
|
|
16
|
-
raise Error, "invalid file URI path" if path.include?("\0") || !path.valid_encoding?
|
|
17
|
-
RUBY_PLATFORM.match?(/mswin|mingw/) ? path.sub(%r{\A/([A-Za-z]:/)}, '\\1') : path
|
|
18
|
-
rescue URI::InvalidURIError => error
|
|
19
|
-
raise Error, error.message
|
|
20
|
-
end
|
|
21
|
-
def position(rope, offset)
|
|
22
|
-
point = rope.utf16_point_at(offset)
|
|
23
|
-
{line: point.row, character: point.column}
|
|
24
|
-
end
|
|
25
|
-
def offset(rope, position)
|
|
26
|
-
raise Error, "invalid LSP position" unless position.is_a?(Hash) && uint?(position["line"]) && uint?(position["character"])
|
|
27
|
-
row, column = position.fetch("line"), position.fetch("character")
|
|
28
|
-
start = rope.line_start(row)
|
|
29
|
-
finish = start + rope.line(row).bytesize
|
|
30
|
-
units = rope.utf16_offset_at(finish) - rope.utf16_offset_at(start)
|
|
31
|
-
rope.offset_at_utf16(rope.utf16_offset_at(start) + [column, units].min)
|
|
32
|
-
end
|
|
33
|
-
def range(rope, range)
|
|
34
|
-
{start: position(rope, range.begin), end: position(rope, range.end + (range.exclude_end? ? 0 : 1))}
|
|
35
|
-
end
|
|
36
|
-
def semantic_delta(data, edits)
|
|
37
|
-
raise Error, "invalid semantic token delta" unless data.is_a?(Array) && edits.is_a?(Array)
|
|
38
|
-
output = data.dup
|
|
39
|
-
last = 0
|
|
40
|
-
edits.each do |edit|
|
|
41
|
-
raise Error, "invalid semantic token delta" unless edit.is_a?(Hash) && uint?(edit["start"]) && uint?(edit["deleteCount"]) && edit.fetch("data", []).is_a?(Array)
|
|
42
|
-
end
|
|
43
|
-
sorted = edits.sort_by { |edit| edit.fetch("start") }
|
|
44
|
-
sorted.each do |edit|
|
|
45
|
-
start, count = edit.fetch("start"), edit.fetch("deleteCount")
|
|
46
|
-
raise Error, "invalid semantic token delta" unless start.is_a?(Integer) && count.is_a?(Integer) && start >= last && count >= 0 && start + count <= data.length
|
|
47
|
-
last = start + count
|
|
48
|
-
end
|
|
49
|
-
sorted.reverse_each { |edit| output[edit.fetch("start"), edit.fetch("deleteCount")] = edit.fetch("data", []) }
|
|
50
|
-
semantic_tokens(output)
|
|
51
|
-
output
|
|
52
|
-
end
|
|
53
|
-
def uint?(value) = value.is_a?(Integer) && value.between?(0, 0x7fffffff)
|
|
54
|
-
def diagnostics(values)
|
|
55
|
-
valid = values.is_a?(Array) && values.all? do |value|
|
|
56
|
-
next false unless value.is_a?(Hash) && value["message"].is_a?(String) && value["range"].is_a?(Hash)
|
|
57
|
-
range = value["range"]
|
|
58
|
-
points = %w[start end].map { |key| range[key] }
|
|
59
|
-
points.all? { |point| point.is_a?(Hash) && uint?(point["line"]) && uint?(point["character"]) } &&
|
|
60
|
-
([points[0]["line"], points[0]["character"]] <=> [points[1]["line"], points[1]["character"]]) <= 0 &&
|
|
61
|
-
(!value.key?("severity") || (value["severity"].is_a?(Integer) && value["severity"].between?(1, 4)))
|
|
62
|
-
end
|
|
63
|
-
raise Error, "invalid LSP diagnostics" unless valid
|
|
64
|
-
values
|
|
65
|
-
end
|
|
66
|
-
def semantic_tokens(data, legend: nil)
|
|
67
|
-
raise Error, "invalid semantic token tuple count" unless data.is_a?(Array) && data.length % 5 == 0
|
|
68
|
-
if legend
|
|
69
|
-
raise Error, "invalid semantic token legend" unless legend.is_a?(Hash) && %w[tokenTypes tokenModifiers].all? { |key| legend[key].is_a?(Array) && legend[key].all? { |name| name.is_a?(String) } }
|
|
70
|
-
end
|
|
71
|
-
row, column = 0, 0
|
|
72
|
-
data.each_slice(5).map do |delta_row, delta_column, length, type, modifiers|
|
|
73
|
-
raise Error, "invalid semantic token value" unless [delta_row, delta_column, length, type, modifiers].all? { |v| uint?(v) } && length.positive?
|
|
74
|
-
raise Error, "semantic token exceeds legend" if legend && (type >= legend["tokenTypes"].length || modifiers.bit_length > legend["tokenModifiers"].length)
|
|
75
|
-
row += delta_row
|
|
76
|
-
column = delta_row.zero? ? column + delta_column : delta_column
|
|
77
|
-
raise Error, "semantic token position overflow" unless uint?(row) && uint?(column + length)
|
|
78
|
-
{line: row, character: column, length: length, type: type, modifiers: modifiers}
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
data/lib/canopus/lsp/timeout.rb
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Canopus
|
|
4
|
-
module LSP
|
|
5
|
-
class Transport
|
|
6
|
-
MAX_MESSAGE = 32 << 20
|
|
7
|
-
attr_reader :stderr_lines, :pid
|
|
8
|
-
def initialize(command, cwd: nil, env: {}, &receive)
|
|
9
|
-
raise ArgumentError, "command must be a nonempty argument array" unless command.is_a?(Array) && !command.empty? && command.all? { |part| part.is_a?(String) && !part.include?("\0") }
|
|
10
|
-
raise ArgumentError, "receiver required" unless receive
|
|
11
|
-
options = cwd ? {chdir: cwd} : {}
|
|
12
|
-
@stdin, @stdout, @stderr, @process = Open3.popen3(env, *command, **options)
|
|
13
|
-
@stdin.binmode
|
|
14
|
-
@stdout.binmode
|
|
15
|
-
@pid, @write_lock, @stderr_lines = @process.pid, Mutex.new, []
|
|
16
|
-
@reader = Thread.new do
|
|
17
|
-
begin
|
|
18
|
-
loop do
|
|
19
|
-
message = self.class.read_message(@stdout)
|
|
20
|
-
break unless message
|
|
21
|
-
receive.call(message, nil)
|
|
22
|
-
end
|
|
23
|
-
receive.call(nil, Error.new("language server closed stdout")) unless @closing
|
|
24
|
-
rescue StandardError => error
|
|
25
|
-
begin
|
|
26
|
-
receive.call(nil, error) unless @closing
|
|
27
|
-
rescue StandardError
|
|
28
|
-
nil
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
@logger = Thread.new do
|
|
33
|
-
while (line = @stderr.gets("\n", 8192))
|
|
34
|
-
@stderr_lines << line.scrub.byteslice(0, 8192).scrub("")
|
|
35
|
-
@stderr_lines.shift if @stderr_lines.length > 200
|
|
36
|
-
end
|
|
37
|
-
rescue IOError
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
def self.read_message(io)
|
|
42
|
-
headers, count = {}, 0
|
|
43
|
-
loop do
|
|
44
|
-
line = io.gets("\r\n", 8193)
|
|
45
|
-
return nil if line.nil? && headers.empty?
|
|
46
|
-
raise Error, "truncated or oversized LSP header" unless line && line.end_with?("\r\n") && line.bytesize <= 8192
|
|
47
|
-
break if line == "\r\n"
|
|
48
|
-
count += line.bytesize
|
|
49
|
-
raise Error, "oversized LSP headers" if count > 16384
|
|
50
|
-
raise Error, "non-ASCII LSP header" unless line.ascii_only?
|
|
51
|
-
key, value = line.strip.split(":", 2)
|
|
52
|
-
raise Error, "invalid LSP header" unless value && key.match?(/\A[A-Za-z][A-Za-z0-9-]*\z/)
|
|
53
|
-
key = key.downcase
|
|
54
|
-
raise Error, "duplicate LSP header" if headers.key?(key)
|
|
55
|
-
headers[key] = value.strip
|
|
56
|
-
end
|
|
57
|
-
raw_length = headers["content-length"]
|
|
58
|
-
raise Error, "missing or invalid Content-Length" unless raw_length&.match?(/\A\d+\z/)
|
|
59
|
-
length = Integer(raw_length, 10)
|
|
60
|
-
raise Error, "oversized LSP message" unless length.between?(1, MAX_MESSAGE)
|
|
61
|
-
charset = headers["content-type"]&.match(/charset\s*=\s*"?([^;"\s]+)/i)&.[](1)
|
|
62
|
-
raise Error, "unsupported LSP character encoding" if charset && !%w[utf-8 utf8].include?(charset.downcase)
|
|
63
|
-
body = io.read(length)
|
|
64
|
-
raise Error, "truncated LSP body" unless body && body.bytesize == length
|
|
65
|
-
body.force_encoding(Encoding::UTF_8)
|
|
66
|
-
raise Error, "invalid LSP UTF-8 body" unless body.valid_encoding?
|
|
67
|
-
validate_message(JSON.parse(body))
|
|
68
|
-
rescue JSON::ParserError => error
|
|
69
|
-
raise Error, "invalid LSP JSON: #{error.message.byteslice(0, 256)}"
|
|
70
|
-
end
|
|
71
|
-
def self.validate_message(message)
|
|
72
|
-
raise Error, "invalid JSON-RPC message" unless message.is_a?(Hash) && message["jsonrpc"] == "2.0"
|
|
73
|
-
if message.key?("method")
|
|
74
|
-
raise Error, "invalid JSON-RPC method" unless message["method"].is_a?(String) && !message["method"].empty?
|
|
75
|
-
raise Error, "invalid JSON-RPC parameters" if message.key?("params") && !message["params"].is_a?(Hash) && !message["params"].is_a?(Array)
|
|
76
|
-
raise Error, "request contains a response" if message.key?("result") || message.key?("error")
|
|
77
|
-
else
|
|
78
|
-
raise Error, "invalid JSON-RPC response" unless message.key?("id") && (message.key?("result") ^ message.key?("error"))
|
|
79
|
-
if message.key?("error")
|
|
80
|
-
error = message["error"]
|
|
81
|
-
raise Error, "invalid JSON-RPC error" unless error.is_a?(Hash) && error["code"].is_a?(Integer) && error["message"].is_a?(String)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
id = message["id"]
|
|
85
|
-
raise Error, "invalid JSON-RPC id" if message.key?("id") && !id.is_a?(Integer) && !id.is_a?(String) && !(id.nil? && !message.key?("method"))
|
|
86
|
-
message
|
|
87
|
-
end
|
|
88
|
-
def write(message)
|
|
89
|
-
raise Error, "expected JSON-RPC object" unless message.is_a?(Hash)
|
|
90
|
-
normalized = message.transform_keys(&:to_s)
|
|
91
|
-
normalized["error"] = normalized["error"].transform_keys(&:to_s) if normalized["error"].is_a?(Hash)
|
|
92
|
-
self.class.validate_message(normalized)
|
|
93
|
-
body = JSON.generate(message).b
|
|
94
|
-
raise Error, "oversized LSP message" unless body.bytesize.between?(1, MAX_MESSAGE)
|
|
95
|
-
@write_lock.synchronize do
|
|
96
|
-
@stdin.write("Content-Length: #{body.bytesize}\r\n\r\n")
|
|
97
|
-
@stdin.write(body)
|
|
98
|
-
@stdin.flush
|
|
99
|
-
end
|
|
100
|
-
rescue IOError, Errno::EPIPE => error
|
|
101
|
-
raise Error, "language server write failed: #{error.message}"
|
|
102
|
-
end
|
|
103
|
-
def alive? = @process.alive?
|
|
104
|
-
def close
|
|
105
|
-
return if @closing
|
|
106
|
-
@closing = true
|
|
107
|
-
@stdin.close unless @stdin.closed?
|
|
108
|
-
unless @process.join(1)
|
|
109
|
-
Process.kill("TERM", @pid) rescue Errno::ESRCH
|
|
110
|
-
unless @process.join(1)
|
|
111
|
-
Process.kill("KILL", @pid) rescue Errno::ESRCH
|
|
112
|
-
@process.join
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
[@stdout, @stderr].each { |io| io.close unless io.closed? }
|
|
116
|
-
[@reader, @logger].each do |thread|
|
|
117
|
-
next if thread == Thread.current
|
|
118
|
-
thread.kill unless thread.join(1)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
data/lib/canopus/lsp.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "open3"
|
|
5
|
-
require "uri"
|
|
6
|
-
require "thread"
|
|
7
|
-
|
|
8
|
-
module Canopus
|
|
9
|
-
module LSP; end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
require_relative "lsp/error"
|
|
13
|
-
require_relative "lsp/timeout"
|
|
14
|
-
require_relative "lsp/server_error"
|
|
15
|
-
require_relative "lsp/protocol"
|
|
16
|
-
require_relative "lsp/future"
|
|
17
|
-
require_relative "lsp/future/subscription"
|
|
18
|
-
require_relative "lsp/transport"
|
|
19
|
-
require_relative "lsp/client"
|