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
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "../error"
|
|
6
|
+
|
|
7
|
+
module Canopus
|
|
8
|
+
module Debug
|
|
9
|
+
class Session
|
|
10
|
+
DEFAULT_TIMEOUT = 10
|
|
11
|
+
CONNECT_INTERVAL = 0.05
|
|
12
|
+
|
|
13
|
+
attr_reader :client
|
|
14
|
+
|
|
15
|
+
def initialize(root:, configuration:, adapter:, breakpoints:, client: nil, timeout: DEFAULT_TIMEOUT)
|
|
16
|
+
@root = File.realpath(root)
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
@adapter = adapter
|
|
19
|
+
@breakpoints = breakpoints
|
|
20
|
+
@client = client
|
|
21
|
+
@timeout = timeout
|
|
22
|
+
unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
23
|
+
raise ArgumentError, "debug session timeout must be positive and finite"
|
|
24
|
+
end
|
|
25
|
+
@handlers = {}
|
|
26
|
+
@lock = Mutex.new
|
|
27
|
+
@closed = false
|
|
28
|
+
rescue SystemCallError, TypeError => error
|
|
29
|
+
raise Error, "invalid debug session: #{error.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on(event, &handler)
|
|
33
|
+
raise ArgumentError, "debug event handler required" unless handler
|
|
34
|
+
|
|
35
|
+
@lock.synchronize { (@handlers[event.to_sym] ||= []) << handler }
|
|
36
|
+
handler
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stopped_thread_id = @lock.synchronize { @stopped_thread_id }
|
|
40
|
+
|
|
41
|
+
def stack_frames(levels: 100)
|
|
42
|
+
client, thread_id = inspection_state
|
|
43
|
+
client.stack_trace(thread_id, start: 0, levels: levels)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def scopes(frame_id) = inspection_client.scopes(frame_id)
|
|
47
|
+
def variables(reference) = inspection_client.variables(reference)
|
|
48
|
+
def evaluate(expression, frame_id:, context: "watch") = inspection_client.evaluate(expression, frame_id: frame_id, context: context)
|
|
49
|
+
|
|
50
|
+
def start
|
|
51
|
+
deadline = monotonic_time + @timeout
|
|
52
|
+
initialized = Queue.new
|
|
53
|
+
client = @client || build_client(deadline)
|
|
54
|
+
install_client(client)
|
|
55
|
+
client.on(:initialized) { initialized << true }
|
|
56
|
+
client.on(:stopped) { |event| stopped(client, event) }
|
|
57
|
+
client.on(:continued) { clear_stopped_thread; emit(:continued) }
|
|
58
|
+
client.on(:output) { |event| emit(:output, event) }
|
|
59
|
+
client.on(:terminated) { clear_stopped_thread; emit(:terminated) }
|
|
60
|
+
client.on(:exited) { clear_stopped_thread; emit(:terminated) }
|
|
61
|
+
|
|
62
|
+
capabilities = client.start(adapter_id: adapter_id, timeout: remaining(deadline))
|
|
63
|
+
request = request_start(client)
|
|
64
|
+
wait_for_initialized(initialized, request, deadline)
|
|
65
|
+
set_breakpoints(client, capabilities, deadline)
|
|
66
|
+
if capabilities["supportsConfigurationDoneRequest"]
|
|
67
|
+
client.configuration_done.await(timeout: remaining(deadline))
|
|
68
|
+
end
|
|
69
|
+
request.await(timeout: remaining(deadline))
|
|
70
|
+
self
|
|
71
|
+
rescue StandardError => error
|
|
72
|
+
begin
|
|
73
|
+
request.await(timeout: 0) if request&.done?
|
|
74
|
+
rescue StandardError => start_error
|
|
75
|
+
error = start_error
|
|
76
|
+
end
|
|
77
|
+
begin
|
|
78
|
+
close
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
raise debug_error(error)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def close
|
|
86
|
+
client, waiter, log = @lock.synchronize do
|
|
87
|
+
next if @closed
|
|
88
|
+
|
|
89
|
+
@closed = true
|
|
90
|
+
[@client, @process, @log]
|
|
91
|
+
end
|
|
92
|
+
return nil unless client || waiter || log
|
|
93
|
+
|
|
94
|
+
failure = nil
|
|
95
|
+
begin
|
|
96
|
+
client&.close
|
|
97
|
+
rescue StandardError => error
|
|
98
|
+
failure = error
|
|
99
|
+
ensure
|
|
100
|
+
begin
|
|
101
|
+
stop_process(waiter)
|
|
102
|
+
rescue StandardError => error
|
|
103
|
+
failure ||= error
|
|
104
|
+
ensure
|
|
105
|
+
begin
|
|
106
|
+
log&.close!
|
|
107
|
+
rescue StandardError => error
|
|
108
|
+
failure ||= error
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
raise debug_error(failure) if failure
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def inspection_state
|
|
119
|
+
@lock.synchronize do
|
|
120
|
+
raise Error, "debug session is not stopped" if @closed || !@client || !@stopped_thread_id
|
|
121
|
+
|
|
122
|
+
[@client, @stopped_thread_id]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def inspection_client = inspection_state.first
|
|
127
|
+
|
|
128
|
+
def clear_stopped_thread = @lock.synchronize { @stopped_thread_id = nil }
|
|
129
|
+
|
|
130
|
+
def build_client(deadline)
|
|
131
|
+
command = @adapter.fetch("command")
|
|
132
|
+
case @adapter.fetch("transport")
|
|
133
|
+
when "stdio"
|
|
134
|
+
Megrez::Session.stdio(command: command, env: debug_environment,
|
|
135
|
+
cwd: @configuration["cwd"] || @root)
|
|
136
|
+
when "tcp"
|
|
137
|
+
build_tcp_client(command, deadline)
|
|
138
|
+
else
|
|
139
|
+
raise Error, "unsupported debug adapter transport"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_tcp_client(command, deadline)
|
|
144
|
+
unless File.basename(command.first) == "rdbg"
|
|
145
|
+
raise Error, "tcp debug adapters currently require rdbg"
|
|
146
|
+
end
|
|
147
|
+
raise Error, "rdbg tcp adapters only support launch" unless @configuration["request"] == "launch"
|
|
148
|
+
|
|
149
|
+
host, port = "127.0.0.1", available_port
|
|
150
|
+
arguments = command.map { |argument| argument == "--open" ? "--open=vscode" : argument }
|
|
151
|
+
arguments << "--open=vscode" unless arguments.any? { |argument| argument.start_with?("--open=") }
|
|
152
|
+
arguments.concat(["--host", host, "--port", port.to_s])
|
|
153
|
+
if @configuration["request"] == "launch" && @configuration["program"]
|
|
154
|
+
arguments.concat(["--", @configuration.fetch("program"), *@configuration.fetch("args", [])])
|
|
155
|
+
end
|
|
156
|
+
log = Tempfile.new(["canopus-debug-", ".log"])
|
|
157
|
+
pid = Process.spawn(debug_environment, *arguments, chdir: @configuration["cwd"] || @root,
|
|
158
|
+
out: log, err: log)
|
|
159
|
+
waiter = Process.detach(pid)
|
|
160
|
+
rejected = @lock.synchronize do
|
|
161
|
+
if @closed
|
|
162
|
+
true
|
|
163
|
+
else
|
|
164
|
+
@log, @process = log, waiter
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
if rejected
|
|
169
|
+
stop_process(waiter)
|
|
170
|
+
log.close!
|
|
171
|
+
raise Error, "debug session is closed"
|
|
172
|
+
end
|
|
173
|
+
connect_tcp(host, port, deadline)
|
|
174
|
+
rescue SystemCallError => error
|
|
175
|
+
log&.close! unless defined?(@log) && @log.equal?(log)
|
|
176
|
+
raise Error, "debug adapter spawn failed: #{error.message}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def connect_tcp(host, port, deadline)
|
|
180
|
+
loop do
|
|
181
|
+
raise Error, "debug session is closed" if closed?
|
|
182
|
+
if @process&.join(0)
|
|
183
|
+
raise Error, "debug adapter exited before accepting DAP#{adapter_log}"
|
|
184
|
+
end
|
|
185
|
+
begin
|
|
186
|
+
return Megrez::Session.tcp(host: host, port: port,
|
|
187
|
+
connect_timeout: [remaining(deadline), 0.2].min)
|
|
188
|
+
rescue Megrez::Error
|
|
189
|
+
raise if monotonic_time >= deadline
|
|
190
|
+
|
|
191
|
+
sleep([CONNECT_INTERVAL, remaining(deadline)].min)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def install_client(client)
|
|
197
|
+
rejected = @lock.synchronize do
|
|
198
|
+
if @closed
|
|
199
|
+
true
|
|
200
|
+
else
|
|
201
|
+
@client = client
|
|
202
|
+
false
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
if rejected
|
|
206
|
+
client.close
|
|
207
|
+
raise Error, "debug session is closed"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def request_start(client)
|
|
212
|
+
arguments = @configuration.reject { |key, _| %w[name type request].include?(key) }
|
|
213
|
+
if @adapter["transport"] == "tcp" && File.basename(@adapter.fetch("command").first) == "rdbg"
|
|
214
|
+
arguments = arguments.reject { |key, _| %w[program args cwd env].include?(key) }.merge("localfs" => true)
|
|
215
|
+
end
|
|
216
|
+
@configuration.fetch("request") == "launch" ? client.launch(arguments) : client.attach(arguments)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def wait_for_initialized(queue, request, deadline)
|
|
220
|
+
loop do
|
|
221
|
+
initialized = queue.pop(true)
|
|
222
|
+
request.await(timeout: 0) if request.done?
|
|
223
|
+
return initialized
|
|
224
|
+
rescue ThreadError
|
|
225
|
+
request.await(timeout: 0) if request.done?
|
|
226
|
+
raise Error, "debug adapter did not initialize" if monotonic_time >= deadline
|
|
227
|
+
raise Error, "debug session is closed" if closed?
|
|
228
|
+
|
|
229
|
+
sleep([CONNECT_INTERVAL, remaining(deadline)].min)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def set_breakpoints(client, capabilities, deadline)
|
|
234
|
+
@breakpoints.entries.select(&:enabled).group_by(&:path).sort.each do |path, entries|
|
|
235
|
+
validate_breakpoint_capabilities!(entries, capabilities)
|
|
236
|
+
source = File.join(@root, path)
|
|
237
|
+
values = entries.map do |entry|
|
|
238
|
+
Megrez::SourceBreakpoint.new(line: entry.line, column: nil,
|
|
239
|
+
condition: capabilities["supportsConditionalBreakpoints"] ? entry.condition : nil,
|
|
240
|
+
hit_condition: capabilities["supportsHitConditionalBreakpoints"] ? entry.hit_condition : nil,
|
|
241
|
+
log_message: capabilities["supportsLogPoints"] ? entry.log_message : nil)
|
|
242
|
+
end
|
|
243
|
+
client.set_breakpoints(source, values).await(timeout: remaining(deadline))
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def validate_breakpoint_capabilities!(entries, capabilities)
|
|
248
|
+
requirements = {
|
|
249
|
+
condition: "supportsConditionalBreakpoints",
|
|
250
|
+
hit_condition: "supportsHitConditionalBreakpoints",
|
|
251
|
+
log_message: "supportsLogPoints"
|
|
252
|
+
}
|
|
253
|
+
requirements.each do |field, capability|
|
|
254
|
+
if !capabilities[capability] && entries.any? { |entry| !entry.public_send(field).nil? }
|
|
255
|
+
raise Error, "debug adapter does not support #{field.to_s.tr('_', ' ')}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def stopped(client, event)
|
|
261
|
+
thread_id = event["threadId"] || event[:threadId]
|
|
262
|
+
raise Error, "debug stop event has no thread" unless thread_id.is_a?(Integer) && thread_id.positive?
|
|
263
|
+
|
|
264
|
+
generation = client.generation
|
|
265
|
+
frame = client.stack_trace(thread_id, start: 0, levels: 1).await(timeout: @timeout).first
|
|
266
|
+
raise Error, "debug stop event has no stack frame" unless frame
|
|
267
|
+
return unless client.state == :stopped && client.generation == generation
|
|
268
|
+
|
|
269
|
+
accepted = @lock.synchronize do
|
|
270
|
+
next false if @closed || client.state != :stopped || client.generation != generation
|
|
271
|
+
|
|
272
|
+
@stopped_thread_id = thread_id
|
|
273
|
+
true
|
|
274
|
+
end
|
|
275
|
+
return unless accepted
|
|
276
|
+
|
|
277
|
+
emit(:stopped, frame)
|
|
278
|
+
rescue StandardError => error
|
|
279
|
+
return if generation && (client.state != :stopped || client.generation != generation)
|
|
280
|
+
|
|
281
|
+
emit(:error, debug_error(error))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def emit(event, value = nil)
|
|
285
|
+
handlers = @lock.synchronize { @closed ? [] : (@handlers[event] || []).dup }
|
|
286
|
+
handlers.each { |handler| handler.call(value) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def debug_environment
|
|
290
|
+
value = @configuration.fetch("env", {})
|
|
291
|
+
valid = value.is_a?(Hash) && value.all? do |key, item|
|
|
292
|
+
key.is_a?(String) && (item.nil? || item.is_a?(String))
|
|
293
|
+
end
|
|
294
|
+
raise Error, "debug environment must contain string values" unless valid
|
|
295
|
+
|
|
296
|
+
ENV.each_key.grep(/\ABUNDLE/).to_h { |key| [key, nil] }
|
|
297
|
+
.merge("RUBYOPT" => nil, "RUBYLIB" => nil)
|
|
298
|
+
.merge(value)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def adapter_id
|
|
302
|
+
File.basename(@adapter.fetch("command").first) == "rdbg" ? "rdbg" : @configuration.fetch("type")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def available_port
|
|
306
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
307
|
+
server.local_address.ip_port
|
|
308
|
+
ensure
|
|
309
|
+
server&.close
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def stop_process(waiter)
|
|
313
|
+
return unless waiter
|
|
314
|
+
return if waiter.join(0)
|
|
315
|
+
|
|
316
|
+
Process.kill(Gem.win_platform? ? "KILL" : "TERM", waiter.pid)
|
|
317
|
+
return if waiter.join(1)
|
|
318
|
+
|
|
319
|
+
return if Gem.win_platform?
|
|
320
|
+
|
|
321
|
+
Process.kill("KILL", waiter.pid)
|
|
322
|
+
waiter.join(1)
|
|
323
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
324
|
+
nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def adapter_log
|
|
328
|
+
return "" unless @log
|
|
329
|
+
|
|
330
|
+
@log.flush
|
|
331
|
+
text = File.binread(@log.path, 2_048).force_encoding(Encoding::UTF_8).scrub.strip
|
|
332
|
+
text.empty? ? "" : ": #{text}"
|
|
333
|
+
rescue IOError, SystemCallError
|
|
334
|
+
""
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def remaining(deadline)
|
|
338
|
+
value = deadline - monotonic_time
|
|
339
|
+
raise Megrez::Timeout, "debug session timed out" unless value.positive?
|
|
340
|
+
|
|
341
|
+
value
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def closed? = @lock.synchronize { @closed }
|
|
345
|
+
def monotonic_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
346
|
+
|
|
347
|
+
def debug_error(error)
|
|
348
|
+
return error if error.is_a?(Error)
|
|
349
|
+
|
|
350
|
+
Error.new("debug session failed: #{error.message}")
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
data/lib/canopus/decoration.rb
CHANGED
|
@@ -39,7 +39,9 @@ module Canopus
|
|
|
39
39
|
first, last = normalize_rows(row_range)
|
|
40
40
|
state = [buffer.respond_to?(:version) ? buffer.version : nil,
|
|
41
41
|
context&.object_id,
|
|
42
|
-
context&.respond_to?(:selections) ? context.selections : buffer.respond_to?(:selections) ? buffer.selections : nil
|
|
42
|
+
context&.respond_to?(:selections) ? context.selections : buffer.respond_to?(:selections) ? buffer.selections : nil,
|
|
43
|
+
context&.respond_to?(:tab_size) ? context.tab_size : nil,
|
|
44
|
+
context&.respond_to?(:language_document) ? context.language_document.object_id : nil]
|
|
43
45
|
suppliers = @lock.synchronize do
|
|
44
46
|
@suppliers.map { |source, supplier| [source, supplier, @generations[source]] }
|
|
45
47
|
end
|
|
@@ -63,7 +65,7 @@ module Canopus
|
|
|
63
65
|
end
|
|
64
66
|
supplied
|
|
65
67
|
end
|
|
66
|
-
items.sort_by { |item| item.priority }.freeze
|
|
68
|
+
items.each_with_index.sort_by { |item, index| [item.priority, index] }.map!(&:first).freeze
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def invalidate(source, buffer: nil, rows: nil)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Canopus asks lazy ropes for a bounded line window while moving cursors. Keep
|
|
4
|
+
# this small compatibility shim until that helper is part of Denebola's API.
|
|
5
|
+
unless Denebola::LazyRope.method_defined?(:line_window)
|
|
6
|
+
class Denebola::LazyRope
|
|
7
|
+
def line_end(row)
|
|
8
|
+
start = line_start(row)
|
|
9
|
+
ending = row + 1 < line_count(exact: true) ? line_start(row + 1) : bytesize
|
|
10
|
+
tail_start = [ending - 3, start].max
|
|
11
|
+
tail = send(:read_bytes, tail_start, [ending - tail_start, 3].min)
|
|
12
|
+
ending - (tail.end_with?("\r\n") ? 2 : tail.end_with?("\r", "\n") ? 1 : 0)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def line_window(row, from: 0, max_bytes: 16_384)
|
|
16
|
+
start, ending = line_start(row), line_end(row)
|
|
17
|
+
offset = (start + from).clamp(start, ending)
|
|
18
|
+
offset -= 1 while offset > start && offset < ending && (send(:read_bytes, offset, 1).getbyte(0) & 0xc0) == 0x80
|
|
19
|
+
value = send(:read_bytes, offset, [max_bytes, ending - offset].min)
|
|
20
|
+
finish = value.bytesize
|
|
21
|
+
while finish.positive? && !value.byteslice(0, finish).force_encoding(Encoding::UTF_8).valid_encoding?
|
|
22
|
+
finish -= 1
|
|
23
|
+
end
|
|
24
|
+
[value.byteslice(0, finish).to_s.force_encoding(Encoding::UTF_8), offset - start]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "sadr" unless defined?(Sadr::Protocol) && defined?(Sadr::Error)
|
|
5
|
+
|
|
6
|
+
module Canopus
|
|
7
|
+
module Diagnostics
|
|
8
|
+
SOURCES = %i[lsp settings task test].freeze
|
|
9
|
+
SEVERITIES = {error: 1, warning: 2, information: 3, hint: 4}.freeze
|
|
10
|
+
URI_LIMIT = 16_384
|
|
11
|
+
MESSAGE_LIMIT = 4_096
|
|
12
|
+
PUBLICATION_LIMIT = 10_000
|
|
13
|
+
TOTAL_LIMIT = 100_000
|
|
14
|
+
TOTAL_BYTES_LIMIT = 32 << 20
|
|
15
|
+
ITEM_BYTES_LIMIT = 1 << 20
|
|
16
|
+
VALUE_DEPTH_LIMIT = 100
|
|
17
|
+
|
|
18
|
+
Entry = Data.define(:source, :uri, :diagnostic)
|
|
19
|
+
|
|
20
|
+
class Registry
|
|
21
|
+
attr_reader :version
|
|
22
|
+
|
|
23
|
+
def initialize(&on_change)
|
|
24
|
+
@entries = {}
|
|
25
|
+
@sizes = {}
|
|
26
|
+
@counts = SEVERITIES.keys.to_h { |severity| [severity, 0] }
|
|
27
|
+
@total = @bytes = 0
|
|
28
|
+
@lock = Mutex.new
|
|
29
|
+
@on_change = on_change
|
|
30
|
+
@version = 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def publish(source, uri, diagnostics, notify: true)
|
|
34
|
+
raise ArgumentError, "invalid diagnostics notification option" unless [true, false].include?(notify)
|
|
35
|
+
source = normalize_source(source)
|
|
36
|
+
uri = normalize_uri(uri)
|
|
37
|
+
values, bytes = normalize_diagnostics(diagnostics)
|
|
38
|
+
@lock.synchronize do
|
|
39
|
+
key = [source, uri]
|
|
40
|
+
previous = @entries.fetch(key, [])
|
|
41
|
+
total = @total - previous.length + values.length
|
|
42
|
+
raise ArgumentError, "too many diagnostics" if total > TOTAL_LIMIT
|
|
43
|
+
total_bytes = @bytes - @sizes.fetch(key, 0) + bytes
|
|
44
|
+
raise ArgumentError, "diagnostics use too much memory" if total_bytes > TOTAL_BYTES_LIMIT
|
|
45
|
+
|
|
46
|
+
previous.each { |diagnostic| @counts[SEVERITIES.key(diagnostic.fetch("severity", 1))] -= 1 }
|
|
47
|
+
values.each { |diagnostic| @counts[SEVERITIES.key(diagnostic.fetch("severity", 1))] += 1 }
|
|
48
|
+
if values.empty?
|
|
49
|
+
@entries.delete(key)
|
|
50
|
+
@sizes.delete(key)
|
|
51
|
+
else
|
|
52
|
+
@entries[key] = values
|
|
53
|
+
@sizes[key] = bytes
|
|
54
|
+
end
|
|
55
|
+
@total, @bytes = total, total_bytes
|
|
56
|
+
@version += 1
|
|
57
|
+
end
|
|
58
|
+
@on_change&.call(uri) if notify
|
|
59
|
+
values
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def for_uri(uri)
|
|
63
|
+
uri = normalize_uri(uri)
|
|
64
|
+
@lock.synchronize do
|
|
65
|
+
@entries.flat_map do |(source, current), diagnostics|
|
|
66
|
+
current == uri ? diagnostics.map { |diagnostic| Entry.new(source, current, diagnostic).freeze } : []
|
|
67
|
+
end.freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def all(severity: nil, source: nil)
|
|
72
|
+
severity = normalize_severity(severity) if severity
|
|
73
|
+
source = normalize_source(source) if source
|
|
74
|
+
@lock.synchronize do
|
|
75
|
+
@entries.flat_map do |(entry_source, uri), diagnostics|
|
|
76
|
+
next [] if source && source != entry_source
|
|
77
|
+
|
|
78
|
+
diagnostics.filter_map do |diagnostic|
|
|
79
|
+
next if severity && diagnostic.fetch("severity", 1) != severity
|
|
80
|
+
Entry.new(entry_source, uri, diagnostic).freeze
|
|
81
|
+
end
|
|
82
|
+
end.freeze
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def counts
|
|
87
|
+
@lock.synchronize { @counts.dup.freeze }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def normalize_source(source)
|
|
93
|
+
raise ArgumentError, "invalid diagnostic source" unless SOURCES.include?(source)
|
|
94
|
+
source
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def normalize_uri(uri)
|
|
98
|
+
valid = uri.is_a?(String) && uri.encoding == Encoding::UTF_8 && uri.valid_encoding? &&
|
|
99
|
+
uri.bytesize <= URI_LIMIT && !uri.include?("\0")
|
|
100
|
+
raise ArgumentError, "invalid diagnostic URI" unless valid
|
|
101
|
+
Sadr::Protocol.path(uri)
|
|
102
|
+
uri.dup.freeze
|
|
103
|
+
rescue Sadr::Error
|
|
104
|
+
raise ArgumentError, "invalid diagnostic URI"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def normalize_severity(severity)
|
|
108
|
+
severity = SEVERITIES.fetch(severity, severity) if severity.is_a?(Symbol)
|
|
109
|
+
raise ArgumentError, "invalid diagnostic severity" unless severity.is_a?(Integer) && severity.between?(1, 4)
|
|
110
|
+
severity
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def normalize_diagnostics(diagnostics)
|
|
114
|
+
unless diagnostics.is_a?(Array) && diagnostics.length <= PUBLICATION_LIMIT
|
|
115
|
+
raise ArgumentError, "invalid diagnostics"
|
|
116
|
+
end
|
|
117
|
+
Sadr::Protocol.diagnostics(diagnostics)
|
|
118
|
+
bytes = 0
|
|
119
|
+
values = diagnostics.map do |diagnostic|
|
|
120
|
+
message = diagnostic.fetch("message")
|
|
121
|
+
valid = message.encoding == Encoding::UTF_8 && message.valid_encoding? &&
|
|
122
|
+
message.bytesize <= MESSAGE_LIMIT && !message.include?("\0")
|
|
123
|
+
raise ArgumentError, "invalid diagnostic message" unless valid
|
|
124
|
+
|
|
125
|
+
validate_value(diagnostic)
|
|
126
|
+
encoded = JSON.generate(diagnostic)
|
|
127
|
+
raise ArgumentError, "diagnostic exceeds 1 MiB" if encoded.bytesize > ITEM_BYTES_LIMIT
|
|
128
|
+
bytes += encoded.bytesize
|
|
129
|
+
raise ArgumentError, "diagnostics use too much memory" if bytes > TOTAL_BYTES_LIMIT
|
|
130
|
+
freeze_value(JSON.parse(encoded))
|
|
131
|
+
end.freeze
|
|
132
|
+
[values, bytes]
|
|
133
|
+
rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError, Sadr::Error, KeyError, TypeError
|
|
134
|
+
raise ArgumentError, "invalid diagnostics"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def freeze_value(value)
|
|
138
|
+
value.each { |key, child| key.freeze; freeze_value(child) } if value.is_a?(Hash)
|
|
139
|
+
value.each { |child| freeze_value(child) } if value.is_a?(Array)
|
|
140
|
+
value.freeze
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_value(value, depth = 0)
|
|
144
|
+
raise ArgumentError, "diagnostic is too deeply nested" if depth > VALUE_DEPTH_LIMIT
|
|
145
|
+
|
|
146
|
+
case value
|
|
147
|
+
when Hash
|
|
148
|
+
raise ArgumentError, "invalid diagnostic" unless value.length <= PUBLICATION_LIMIT
|
|
149
|
+
value.each do |key, child|
|
|
150
|
+
validate_string(key)
|
|
151
|
+
validate_value(child, depth + 1)
|
|
152
|
+
end
|
|
153
|
+
when Array
|
|
154
|
+
raise ArgumentError, "invalid diagnostic" if value.length > PUBLICATION_LIMIT
|
|
155
|
+
value.each { |child| validate_value(child, depth + 1) }
|
|
156
|
+
when String then validate_string(value)
|
|
157
|
+
when Float
|
|
158
|
+
raise ArgumentError, "invalid diagnostic" unless value.finite?
|
|
159
|
+
when NilClass, TrueClass, FalseClass, Integer
|
|
160
|
+
nil
|
|
161
|
+
else
|
|
162
|
+
raise ArgumentError, "invalid diagnostic"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def validate_string(value)
|
|
167
|
+
valid = value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? &&
|
|
168
|
+
value.bytesize <= MESSAGE_LIMIT && !value.include?("\0")
|
|
169
|
+
raise ArgumentError, "invalid diagnostic text" unless valid
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -62,10 +62,14 @@ class Canopus::DisplayMap::OverlayMap
|
|
|
62
62
|
blocks[row] << Block.new(item, row, height.to_f, (height.to_f / line_height).ceil, position)
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
|
-
inlines.
|
|
65
|
+
inline_values = inlines.values.flatten.each_with_index
|
|
66
|
+
.sort_by { |value, index| [value.offset, value.item.priority, index] }.map!(&:first).freeze
|
|
67
|
+
inlines.each_value do |values|
|
|
68
|
+
ordered = values.each_with_index.sort_by { |value, index| [value.offset, value.item.priority, index] }.map!(&:first)
|
|
69
|
+
values.replace(ordered).freeze
|
|
70
|
+
end
|
|
66
71
|
blocks.each_value { |values| values.sort_by! { |value| value.item.priority }.freeze }
|
|
67
72
|
inlines, blocks = inlines.to_h.freeze, blocks.to_h.freeze
|
|
68
|
-
inline_values = inlines.values.flatten.sort_by(&:offset).freeze
|
|
69
73
|
return [] if @inlines == inlines && @blocks == blocks && @line_height == line_height
|
|
70
74
|
|
|
71
75
|
affected = (@inlines.keys | @blocks.keys | inlines.keys | blocks.keys).select do |row|
|
|
@@ -47,12 +47,13 @@ module Canopus
|
|
|
47
47
|
def insert_snippet(source, variables: {}, workspace_root: nil, clipboard: nil)
|
|
48
48
|
raise Error, "snippet variables must be a Hash" unless variables.is_a?(Hash)
|
|
49
49
|
# Parse every cursor expansion before changing text or disposing a live session.
|
|
50
|
-
|
|
50
|
+
selections = merged_selections(@selections, touching: false)
|
|
51
|
+
snippets = selections.each_with_index.map do |selection, index|
|
|
51
52
|
builtins = snippet_variables(workspace_root: workspace_root, clipboard: clipboard, selection: selection, cursor_index: index)
|
|
52
53
|
Snippet.new(source, variables: builtins.merge(variables.transform_keys(&:to_s)))
|
|
53
54
|
end
|
|
54
55
|
starts, delta = [], 0
|
|
55
|
-
|
|
56
|
+
selections.zip(snippets).each do |selection, snippet|
|
|
56
57
|
starts << selection.start + delta
|
|
57
58
|
delta += snippet.text.bytesize - selection.range.size
|
|
58
59
|
end
|