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,600 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
class Workspace
|
|
5
|
+
module DebugAware
|
|
6
|
+
DEBUG_HOVER_EXPRESSION_LIMIT = 256
|
|
7
|
+
DEBUG_HOVER_TEXT_LIMIT = 4_096
|
|
8
|
+
DEBUG_HOVER_SCOPE_LIMIT = 16
|
|
9
|
+
DEBUG_HOVER_VARIABLE_LIMIT = 256
|
|
10
|
+
DEBUG_HOVER_TOKEN_LIMIT = 4_096
|
|
11
|
+
DEBUG_HOVER_ROW_LIMIT = 256
|
|
12
|
+
DEBUG_HOVER_NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/n
|
|
13
|
+
DEBUG_HOVER_DIRECT_NAME = /\A(?:@@?|\$)[A-Za-z_][A-Za-z0-9_]*\z/n
|
|
14
|
+
DEBUG_HOVER_DIRECT_TOKENS = %w[Name.Variable.Instance Name.Variable.Class Name.Variable.Global].freeze
|
|
15
|
+
|
|
16
|
+
BreakpointClick = Data.define(:workspace) do
|
|
17
|
+
def call(editor, row) = workspace.toggle_breakpoint(editor, row)
|
|
18
|
+
def right_click(editor, row) = workspace.show_breakpoint_menu(editor, row)
|
|
19
|
+
end
|
|
20
|
+
private_constant :BreakpointClick
|
|
21
|
+
|
|
22
|
+
def initialize_breakpoints
|
|
23
|
+
@breakpoints = Debug::Breakpoints.new(root: @root) do |_path|
|
|
24
|
+
@decorations.invalidate(:breakpoint)
|
|
25
|
+
@debug_panel&.refresh_breakpoints
|
|
26
|
+
@window&.request_frame
|
|
27
|
+
end
|
|
28
|
+
@breakpoint_click = BreakpointClick.new(self)
|
|
29
|
+
@decorations.register(:breakpoint) { |buffer, rows, current| breakpoint_decorations(buffer, rows, current) }
|
|
30
|
+
@debug_generation = 0
|
|
31
|
+
@debug_session = @debug_thread = @debug_position = nil
|
|
32
|
+
@debug_panel = Debug::Panel.new(breakpoints: @breakpoints,
|
|
33
|
+
post: ->(&block) { post(&block) },
|
|
34
|
+
select_frame: ->(session, frame) { select_debug_frame(session, frame) },
|
|
35
|
+
select_breakpoint: ->(entry) { select_debug_breakpoint(entry) },
|
|
36
|
+
report: ->(text) { notify(text) }, request_frame: -> { @window&.request_frame })
|
|
37
|
+
@debug_console = Debug::Console.new(post: ->(&block) { post(&block) },
|
|
38
|
+
request_frame: -> { @window&.request_frame })
|
|
39
|
+
@decorations.register(:debug_position) { |buffer, rows| debug_position_decorations(buffer, rows) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :debug_session, :debug_position, :debug_panel, :debug_console
|
|
43
|
+
|
|
44
|
+
def debug_tree = @debug_panel.tree
|
|
45
|
+
def debug_console_tree = @debug_console.tree
|
|
46
|
+
def debug_watches = @debug_panel.watches
|
|
47
|
+
|
|
48
|
+
def add_debug_watch(expression) = @debug_panel.add_watch(expression)
|
|
49
|
+
def remove_debug_watch(expression) = @debug_panel.remove_watch(expression)
|
|
50
|
+
|
|
51
|
+
def show_debug_watch_add
|
|
52
|
+
self.palette = {kind: :debug_watch_add, query: +"", index: 0, matches: []}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def show_debug_watch_remove
|
|
56
|
+
if debug_watches.empty?
|
|
57
|
+
self.message = "No debug watch expressions"
|
|
58
|
+
return false
|
|
59
|
+
end
|
|
60
|
+
self.palette = {kind: :debug_watch_remove, query: +"", index: 0,
|
|
61
|
+
matches: debug_watches.dup, items: debug_watches}
|
|
62
|
+
update_palette
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def accept_debug_watch_palette
|
|
66
|
+
state = @palette
|
|
67
|
+
self.palette = nil
|
|
68
|
+
if state[:kind] == :debug_watch_add
|
|
69
|
+
add_debug_watch(state[:query])
|
|
70
|
+
else
|
|
71
|
+
index = state[:indices] ? state[:indices][state[:index]] : state[:index]
|
|
72
|
+
expression = index && state[:items][index]
|
|
73
|
+
expression && remove_debug_watch(expression)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def show_debug_console
|
|
78
|
+
raise Error, "debug session is not stopped" unless @debug_session && @debug_panel.selected_frame
|
|
79
|
+
|
|
80
|
+
@panels.show("debug_console")
|
|
81
|
+
self.palette = {kind: :debug_console, query: +"", index: 0, matches: []}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def accept_debug_console_palette
|
|
85
|
+
expression = @palette.fetch(:query)
|
|
86
|
+
self.palette = nil
|
|
87
|
+
@debug_console.evaluate(expression)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def debug_hover(current = editor, offset = current&.primary&.head)
|
|
91
|
+
session, frame = @debug_session, @debug_panel.selected_frame
|
|
92
|
+
unless session && frame && debug_hover_editor?(current) && offset.is_a?(Integer)
|
|
93
|
+
clear_debug_hover
|
|
94
|
+
return false
|
|
95
|
+
end
|
|
96
|
+
candidate = debug_hover_expression(current, offset)
|
|
97
|
+
unless candidate
|
|
98
|
+
clear_debug_hover
|
|
99
|
+
return false
|
|
100
|
+
end
|
|
101
|
+
expression, direct = candidate
|
|
102
|
+
key = [session, @debug_generation, frame.id, current, current.buffer.version, expression].freeze
|
|
103
|
+
return true if @debug_hover_key == key
|
|
104
|
+
|
|
105
|
+
clear_debug_hover
|
|
106
|
+
token = @debug_hover_token = Object.new.freeze
|
|
107
|
+
@debug_hover_key = key
|
|
108
|
+
direct ? request_debug_hover_evaluate(token, key) : request_debug_hover_scopes(token, key)
|
|
109
|
+
true
|
|
110
|
+
rescue StandardError
|
|
111
|
+
clear_debug_hover
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def dismiss_hover
|
|
116
|
+
clear_debug_hover
|
|
117
|
+
super
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def breakpoint_decorations(buffer, rows, current = nil)
|
|
121
|
+
return [] unless breakpoint_buffer?(buffer)
|
|
122
|
+
last = [rows.end, buffer.line_count].min
|
|
123
|
+
entries = @breakpoints.for_path(buffer.path).filter_map do |entry|
|
|
124
|
+
row = entry.line - 1
|
|
125
|
+
[row, entry] if row.between?(rows.begin, last - 1)
|
|
126
|
+
end.to_h
|
|
127
|
+
requested = current.is_a?(Editor) && current.buffer.equal?(buffer) ? (rows.begin...last) : entries.keys.sort
|
|
128
|
+
requested.map do |row|
|
|
129
|
+
entry = entries[row]
|
|
130
|
+
state = entry ? (entry.enabled ? "Enabled" : "Disabled") : "Set"
|
|
131
|
+
details = entry && [entry.condition, entry.hit_condition, entry.log_message].compact
|
|
132
|
+
label = "#{state} breakpoint on line #{row + 1}"
|
|
133
|
+
label += ": #{details.join(', ')}" if details && !details.empty?
|
|
134
|
+
color = entry ? (entry.enabled ? :error : :muted) : "#0000"
|
|
135
|
+
style = {color: color, gutter_offset: 7, gutter_width: 3, hit_offset: 7, hit_width: 4}
|
|
136
|
+
Decoration::Item.new(:gutter, nil, row, label, style, 20, :breakpoint, @breakpoint_click)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def toggle_breakpoint(current, row)
|
|
141
|
+
return unless breakpoint_row?(current, row)
|
|
142
|
+
@breakpoints.toggle(current.buffer.path, row + 1)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def show_breakpoint_menu(current, row)
|
|
146
|
+
return unless breakpoint_row?(current, row)
|
|
147
|
+
path, line = current.buffer.path, row + 1
|
|
148
|
+
entry = @breakpoints.for_path(path).find { |item| item.line == line }
|
|
149
|
+
actions = if entry
|
|
150
|
+
[[:condition, "Edit condition"], [:hit_condition, "Edit hit count"],
|
|
151
|
+
[:log_message, "Edit log message"], [:enabled, entry.enabled ? "Disable breakpoint" : "Enable breakpoint"],
|
|
152
|
+
[:remove, "Remove breakpoint"]]
|
|
153
|
+
else
|
|
154
|
+
[[:add, "Add breakpoint"], [:condition, "Add conditional breakpoint"],
|
|
155
|
+
[:hit_condition, "Add hit-count breakpoint"], [:log_message, "Add logpoint"]]
|
|
156
|
+
end
|
|
157
|
+
self.palette = {kind: :breakpoint_actions, query: +"", index: 0, matches: actions.map(&:last),
|
|
158
|
+
actions: actions.map(&:first),
|
|
159
|
+
breakpoint: {path: path, line: line, buffer: current.buffer,
|
|
160
|
+
version: current.buffer.version, entry: entry}}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def accept_breakpoint_palette
|
|
164
|
+
state = @palette.fetch(:breakpoint)
|
|
165
|
+
unless breakpoint_palette_current?(state)
|
|
166
|
+
self.palette = nil
|
|
167
|
+
self.message = "Buffer changed; open the breakpoint menu again"
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
if @palette[:kind] == :breakpoint_edit
|
|
171
|
+
value = @palette[:query].empty? ? nil : @palette[:query]
|
|
172
|
+
entry = breakpoint_entry(state)
|
|
173
|
+
options = {@palette.fetch(:field) => value}
|
|
174
|
+
self.palette = nil
|
|
175
|
+
return entry ? @breakpoints.update(state[:path], state[:line], **options) :
|
|
176
|
+
@breakpoints.add(state[:path], state[:line], **options)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
action = @palette.fetch(:actions).fetch(@palette[:index])
|
|
180
|
+
entry = breakpoint_entry(state)
|
|
181
|
+
case action
|
|
182
|
+
when :add
|
|
183
|
+
self.palette = nil
|
|
184
|
+
@breakpoints.add(state[:path], state[:line])
|
|
185
|
+
when :remove
|
|
186
|
+
self.palette = nil
|
|
187
|
+
@breakpoints.remove(state[:path], state[:line])
|
|
188
|
+
when :enabled
|
|
189
|
+
raise Error, "breakpoint does not exist" unless entry
|
|
190
|
+
self.palette = nil
|
|
191
|
+
@breakpoints.update(state[:path], state[:line], enabled: !entry.enabled)
|
|
192
|
+
else
|
|
193
|
+
value = entry&.public_send(action)
|
|
194
|
+
self.palette = {kind: :breakpoint_edit, query: +(value || ""), index: 0, matches: [],
|
|
195
|
+
breakpoint: state, field: action}
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def attach_breakpoints(buffer)
|
|
200
|
+
@breakpoints.attach(buffer) if breakpoint_buffer?(buffer)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def relocate_breakpoints(buffer)
|
|
204
|
+
@breakpoints.detach(buffer)
|
|
205
|
+
attach_breakpoints(buffer)
|
|
206
|
+
@decorations.invalidate(:breakpoint, buffer: buffer)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def detach_breakpoints(buffer) = @breakpoints.detach(buffer)
|
|
210
|
+
|
|
211
|
+
def start_debugging(configuration = nil)
|
|
212
|
+
loader = Debug::Configuration.new(root: @root, settings: @settings)
|
|
213
|
+
if configuration.nil?
|
|
214
|
+
configurations = loader.configurations
|
|
215
|
+
raise Error, "no debug configurations are available" if configurations.empty?
|
|
216
|
+
if configurations.length > 1
|
|
217
|
+
self.palette = {kind: :debug_configurations, query: +"", index: 0,
|
|
218
|
+
matches: configurations.map { |item| item.fetch("name") }, items: configurations}
|
|
219
|
+
return nil
|
|
220
|
+
end
|
|
221
|
+
configuration = configurations.first
|
|
222
|
+
end
|
|
223
|
+
context = debug_configuration_context
|
|
224
|
+
resolved = loader.resolve(configuration, **context)
|
|
225
|
+
adapter = loader.adapter(resolved.fetch("type"), **context)
|
|
226
|
+
stop_debugging if @debug_session
|
|
227
|
+
generation = @debug_generation += 1
|
|
228
|
+
session = build_debug_session(configuration: resolved, adapter: adapter)
|
|
229
|
+
session.on(:stopped) { |frame| post { handle_debug_stop(session, generation, frame) } }
|
|
230
|
+
session.on(:continued) { post { handle_debug_continue(session, generation) } }
|
|
231
|
+
session.on(:output) { |event| handle_debug_output(session, generation, event) }
|
|
232
|
+
session.on(:terminated) { post { finish_debug_session(session, generation) } }
|
|
233
|
+
session.on(:error) { |error| post { report_debug_error(session, generation, error) } }
|
|
234
|
+
@debug_session = session
|
|
235
|
+
@debug_console.attach(session)
|
|
236
|
+
@debug_thread = Thread.new do
|
|
237
|
+
session.start
|
|
238
|
+
post { notify("Debug session started: #{resolved.fetch('name')}") if current_debug_session?(session, generation) }
|
|
239
|
+
rescue StandardError => error
|
|
240
|
+
post { fail_debug_session(session, generation, error) }
|
|
241
|
+
end
|
|
242
|
+
@debug_thread.report_on_exception = false
|
|
243
|
+
session
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def stop_debugging
|
|
247
|
+
session, thread = @debug_session, @debug_thread
|
|
248
|
+
@debug_generation += 1
|
|
249
|
+
@debug_session = @debug_thread = nil
|
|
250
|
+
@debug_panel.clear
|
|
251
|
+
@debug_console.detach
|
|
252
|
+
clear_debug_hover
|
|
253
|
+
clear_debug_position
|
|
254
|
+
failure = nil
|
|
255
|
+
begin
|
|
256
|
+
session&.close
|
|
257
|
+
rescue StandardError => error
|
|
258
|
+
failure = error
|
|
259
|
+
ensure
|
|
260
|
+
thread.join if thread && thread != Thread.current
|
|
261
|
+
end
|
|
262
|
+
raise failure if failure
|
|
263
|
+
|
|
264
|
+
session
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def debug_position_decorations(buffer, rows)
|
|
268
|
+
position = @debug_position
|
|
269
|
+
return [] unless position && buffer.path == position[:path] && rows.cover?(position[:row])
|
|
270
|
+
|
|
271
|
+
[Decoration::Item.new(:line, nil, position[:row], nil, "#f9758333", 10, :debug_position, nil)]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
def build_debug_session(configuration:, adapter:)
|
|
277
|
+
Debug::Session.new(root: @root, configuration: configuration, adapter: adapter,
|
|
278
|
+
breakpoints: @breakpoints)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def debug_configuration_context
|
|
282
|
+
current = editor
|
|
283
|
+
return {} unless current && current.buffer.path
|
|
284
|
+
|
|
285
|
+
selection = current.primary
|
|
286
|
+
{file: current.buffer.path,
|
|
287
|
+
line_number: current.buffer.rope.point_at(selection.head).row + 1,
|
|
288
|
+
selected_text: selection.empty? ? nil : current.buffer.rope.byteslice(selection.range).to_s}
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def show_debug_frame(session, generation, frame)
|
|
292
|
+
return unless current_debug_session?(session, generation)
|
|
293
|
+
|
|
294
|
+
source = frame.source
|
|
295
|
+
path = source && (source["path"] || source[:path])
|
|
296
|
+
show_debug_location(path, frame.line)
|
|
297
|
+
rescue StandardError => error
|
|
298
|
+
clear_debug_position
|
|
299
|
+
notify("Debug stop could not be shown: #{error.message}")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def show_debug_location(path, line, highlight: true)
|
|
303
|
+
path = debug_source_path(path)
|
|
304
|
+
raise Error, "debug location has an invalid line" unless line.is_a?(Integer) && line.positive?
|
|
305
|
+
|
|
306
|
+
current = open(path)
|
|
307
|
+
raise Error, "debug location is outside the file" if line > current.buffer.line_count
|
|
308
|
+
|
|
309
|
+
if highlight
|
|
310
|
+
@debug_position = {path: current.buffer.path, row: line - 1}.freeze
|
|
311
|
+
@decorations.invalidate(:debug_position)
|
|
312
|
+
end
|
|
313
|
+
current.select(current.buffer.rope.line_start(line - 1))
|
|
314
|
+
current.reveal_cursor
|
|
315
|
+
@window&.request_frame
|
|
316
|
+
true
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def handle_debug_stop(session, generation, frame)
|
|
320
|
+
return unless current_debug_session?(session, generation)
|
|
321
|
+
|
|
322
|
+
@debug_panel.stopped(session, frame)
|
|
323
|
+
@debug_console.stopped(session, frame)
|
|
324
|
+
clear_debug_hover
|
|
325
|
+
@panels.show("debug")
|
|
326
|
+
show_debug_frame(session, generation, frame)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def handle_debug_continue(session, generation)
|
|
330
|
+
return unless current_debug_session?(session, generation)
|
|
331
|
+
|
|
332
|
+
@debug_panel.continued(session)
|
|
333
|
+
@debug_console.continued(session)
|
|
334
|
+
clear_debug_hover
|
|
335
|
+
clear_debug_position
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def handle_debug_output(session, generation, event)
|
|
339
|
+
@debug_console.output(session, event) if current_debug_session?(session, generation)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def select_debug_frame(session, frame)
|
|
343
|
+
return false unless @debug_session.equal?(session) && !@closed
|
|
344
|
+
|
|
345
|
+
@debug_console.stopped(session, frame)
|
|
346
|
+
clear_debug_hover
|
|
347
|
+
show_debug_frame(session, @debug_generation, frame)
|
|
348
|
+
true
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def select_debug_breakpoint(entry)
|
|
352
|
+
show_debug_location(entry.path, entry.line, highlight: false)
|
|
353
|
+
rescue StandardError => error
|
|
354
|
+
notify("Breakpoint could not be shown: #{error.message}")
|
|
355
|
+
false
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def debug_source_path(value)
|
|
359
|
+
valid = value.is_a?(String) && value.valid_encoding? &&
|
|
360
|
+
(value.encoding == Encoding::UTF_8 || value.ascii_only?) && value.bytesize.between?(1, 65_536) &&
|
|
361
|
+
!value.match?(/[\x00-\x1f\x7f]/)
|
|
362
|
+
raise Error, "debug stack frame has an invalid source" unless valid
|
|
363
|
+
|
|
364
|
+
path = File.realpath(File.expand_path(value, @root))
|
|
365
|
+
boundary = @root.end_with?(File::SEPARATOR) ? @root : "#{@root}#{File::SEPARATOR}"
|
|
366
|
+
raise Error, "debug stack frame is outside the workspace" unless path.start_with?(boundary)
|
|
367
|
+
raise Error, "debug stack frame source is not a file" unless File.file?(path)
|
|
368
|
+
|
|
369
|
+
path
|
|
370
|
+
rescue SystemCallError => error
|
|
371
|
+
raise Error, "invalid debug stack frame source: #{error.message}"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def finish_debug_session(session, generation)
|
|
375
|
+
return unless current_debug_session?(session, generation)
|
|
376
|
+
|
|
377
|
+
@debug_session = @debug_thread = nil
|
|
378
|
+
@debug_generation += 1
|
|
379
|
+
@debug_panel.clear
|
|
380
|
+
@debug_console.detach
|
|
381
|
+
clear_debug_hover
|
|
382
|
+
clear_debug_position
|
|
383
|
+
session.close
|
|
384
|
+
notify("Debug session ended")
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def fail_debug_session(session, generation, error)
|
|
388
|
+
return unless current_debug_session?(session, generation)
|
|
389
|
+
|
|
390
|
+
@debug_session = @debug_thread = nil
|
|
391
|
+
@debug_generation += 1
|
|
392
|
+
@debug_panel.clear
|
|
393
|
+
@debug_console.detach
|
|
394
|
+
clear_debug_hover
|
|
395
|
+
clear_debug_position
|
|
396
|
+
session.close
|
|
397
|
+
notify(error.message)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def report_debug_error(session, generation, error)
|
|
401
|
+
notify(error.message) if current_debug_session?(session, generation)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def current_debug_session?(session, generation)
|
|
405
|
+
@debug_session.equal?(session) && @debug_generation == generation && !@closed
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def clear_debug_position
|
|
409
|
+
return unless @debug_position
|
|
410
|
+
|
|
411
|
+
@debug_position = nil
|
|
412
|
+
@decorations.invalidate(:debug_position)
|
|
413
|
+
@window&.request_frame
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def debug_hover_editor?(current)
|
|
417
|
+
return false unless current.is_a?(Editor) && current.equal?(editor)
|
|
418
|
+
|
|
419
|
+
path = current.buffer.path
|
|
420
|
+
boundary = @root.end_with?(File::SEPARATOR) ? @root : "#{@root}#{File::SEPARATOR}"
|
|
421
|
+
path.is_a?(String) && path.start_with?(boundary)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def debug_hover_expression(current, offset)
|
|
425
|
+
document, buffer = current.language_document, current.buffer
|
|
426
|
+
return unless document.background? && document.definition.name == "ruby"
|
|
427
|
+
|
|
428
|
+
size = buffer.rope.bytesize
|
|
429
|
+
return unless offset.between?(0, size)
|
|
430
|
+
point = buffer.rope.point_at(offset)
|
|
431
|
+
return unless document.tokens_current?(point.row)
|
|
432
|
+
|
|
433
|
+
local = offset - buffer.rope.line_start(point.row)
|
|
434
|
+
cursor = 0
|
|
435
|
+
tokens = document.tokens_for(point.row)
|
|
436
|
+
index = tokens.index do |_name, text|
|
|
437
|
+
first, cursor = cursor, cursor + text.bytesize
|
|
438
|
+
local >= first && local < cursor
|
|
439
|
+
end
|
|
440
|
+
return unless index
|
|
441
|
+
|
|
442
|
+
name, value = tokens.fetch(index)
|
|
443
|
+
direct = DEBUG_HOVER_DIRECT_TOKENS.include?(name) && value.match?(DEBUG_HOVER_DIRECT_NAME)
|
|
444
|
+
return unless direct || (name == "Name" && value.match?(DEBUG_HOVER_NAME))
|
|
445
|
+
return if value.bytesize > DEBUG_HOVER_EXPRESSION_LIMIT || debug_hover_call_token?(tokens, index) ||
|
|
446
|
+
debug_hover_string_interpolation?(document, point.row, tokens, index)
|
|
447
|
+
|
|
448
|
+
[value.dup.freeze, direct].freeze
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def debug_hover_call_token?(tokens, index)
|
|
452
|
+
significant = ->(name, _text) { !name.start_with?("Text", "Comment") }
|
|
453
|
+
before = tokens[0...index].reverse.find { |token| significant.call(*token) }&.last
|
|
454
|
+
after = tokens[(index + 1)..]&.find { |token| significant.call(*token) }&.last
|
|
455
|
+
[".", "&.", "::"].include?(before) || after&.match?(/\A(?:\.|&\.|::|\(|\[)/)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def debug_hover_string_interpolation?(document, row, tokens, index)
|
|
459
|
+
depth = 0
|
|
460
|
+
remaining = DEBUG_HOVER_TOKEN_LIMIT
|
|
461
|
+
first = [row - DEBUG_HOVER_ROW_LIMIT + 1, 0].max
|
|
462
|
+
row.downto(first) do |current_row|
|
|
463
|
+
break unless document.tokens_current?(current_row)
|
|
464
|
+
|
|
465
|
+
values = current_row == row ? tokens.first(index) : document.tokens_for(current_row)
|
|
466
|
+
return true if (remaining -= values.length).negative?
|
|
467
|
+
|
|
468
|
+
values.reverse_each do |name, text|
|
|
469
|
+
next unless name == "Literal.String.Interpol"
|
|
470
|
+
|
|
471
|
+
if text == "}"
|
|
472
|
+
depth += 1
|
|
473
|
+
elsif text.end_with?("{")
|
|
474
|
+
return true if depth.zero?
|
|
475
|
+
depth -= 1
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
false
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def request_debug_hover_scopes(token, key)
|
|
483
|
+
session, _generation, frame_id = key
|
|
484
|
+
future = @debug_hover_request = session.scopes(frame_id)
|
|
485
|
+
future.on_complete do |scopes, error|
|
|
486
|
+
post do
|
|
487
|
+
next unless @debug_hover_token.equal?(token)
|
|
488
|
+
next clear_debug_hover unless debug_hover_current?(token, key)
|
|
489
|
+
next clear_debug_hover if error
|
|
490
|
+
|
|
491
|
+
values = scopes.first(DEBUG_HOVER_SCOPE_LIMIT).reject do |scope|
|
|
492
|
+
scope.variables_reference.zero? || scope.expensive
|
|
493
|
+
end
|
|
494
|
+
request_debug_hover_variables(token, key, values, 0)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
rescue StandardError
|
|
498
|
+
clear_debug_hover if @debug_hover_token.equal?(token)
|
|
499
|
+
false
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def request_debug_hover_variables(token, key, scopes, index)
|
|
503
|
+
return clear_debug_hover unless debug_hover_current?(token, key)
|
|
504
|
+
return clear_debug_hover unless (scope = scopes[index])
|
|
505
|
+
|
|
506
|
+
session, = key
|
|
507
|
+
future = @debug_hover_request = session.variables(scope.variables_reference)
|
|
508
|
+
future.on_complete do |variables, error|
|
|
509
|
+
post do
|
|
510
|
+
next unless @debug_hover_token.equal?(token)
|
|
511
|
+
next clear_debug_hover unless debug_hover_current?(token, key)
|
|
512
|
+
|
|
513
|
+
expression = key.last
|
|
514
|
+
if !error && variables.first(DEBUG_HOVER_VARIABLE_LIMIT).any? { |variable| variable.name == expression }
|
|
515
|
+
request_debug_hover_evaluate(token, key)
|
|
516
|
+
else
|
|
517
|
+
request_debug_hover_variables(token, key, scopes, index + 1)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
rescue StandardError
|
|
522
|
+
clear_debug_hover if @debug_hover_token.equal?(token)
|
|
523
|
+
false
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def request_debug_hover_evaluate(token, key)
|
|
527
|
+
return clear_debug_hover unless debug_hover_current?(token, key)
|
|
528
|
+
|
|
529
|
+
session, _generation, frame_id, _current, _version, expression = key
|
|
530
|
+
future = @debug_hover_request = session.evaluate(expression, frame_id: frame_id, context: "hover")
|
|
531
|
+
future.on_complete do |variable, error|
|
|
532
|
+
post { accept_debug_hover(token, key, variable, error) }
|
|
533
|
+
end
|
|
534
|
+
rescue StandardError
|
|
535
|
+
clear_debug_hover if @debug_hover_token.equal?(token)
|
|
536
|
+
false
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def debug_hover_current?(token, key)
|
|
540
|
+
return false unless @debug_hover_token.equal?(token) && @debug_hover_key == key
|
|
541
|
+
|
|
542
|
+
session, generation, frame_id, current, version, = key
|
|
543
|
+
frame = @debug_panel.selected_frame
|
|
544
|
+
current_debug_session?(session, generation) && frame&.id == frame_id &&
|
|
545
|
+
current.equal?(editor) && current.buffer.version == version
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def accept_debug_hover(token, key, variable, error)
|
|
549
|
+
return unless @debug_hover_token.equal?(token)
|
|
550
|
+
return clear_debug_hover unless debug_hover_current?(token, key)
|
|
551
|
+
|
|
552
|
+
@debug_hover_request = nil
|
|
553
|
+
return clear_debug_hover if error
|
|
554
|
+
|
|
555
|
+
expression = key.last
|
|
556
|
+
value = bounded_debug_hover_text(variable.respond_to?(:value) ? variable.value : nil)
|
|
557
|
+
type = bounded_debug_hover_text(variable.respond_to?(:type) ? variable.type : nil)
|
|
558
|
+
@hover_card = bounded_debug_hover_text("#{expression} = #{value}#{type.empty? ? "" : " · #{type}"}")
|
|
559
|
+
@hover_markup = false
|
|
560
|
+
@debug_hover_visible = true
|
|
561
|
+
@window&.request_frame
|
|
562
|
+
true
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def bounded_debug_hover_text(value)
|
|
566
|
+
value.to_s.byteslice(0, DEBUG_HOVER_TEXT_LIMIT).to_s.dup.force_encoding(Encoding::UTF_8).scrub("")
|
|
567
|
+
.gsub(/[\x00-\x1f\x7f]+/, " ").strip
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def clear_debug_hover
|
|
571
|
+
changed = @debug_hover_request || @debug_hover_key || @debug_hover_visible
|
|
572
|
+
@debug_hover_request&.cancel
|
|
573
|
+
@debug_hover_request = @debug_hover_token = @debug_hover_key = nil
|
|
574
|
+
@hover_card = nil if @debug_hover_visible
|
|
575
|
+
@debug_hover_visible = false
|
|
576
|
+
@window&.request_frame if changed
|
|
577
|
+
nil
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def breakpoint_entry(state)
|
|
581
|
+
@breakpoints.for_path(state.fetch(:path)).find { |entry| entry.line == state.fetch(:line) }
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def breakpoint_palette_current?(state)
|
|
585
|
+
buffer = state[:buffer]
|
|
586
|
+
buffer.is_a?(Buffer) && @buffers.value?(buffer) && buffer.version == state[:version] &&
|
|
587
|
+
buffer.path == state[:path] && breakpoint_entry(state).equal?(state[:entry])
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def breakpoint_buffer?(buffer)
|
|
591
|
+
path = buffer.respond_to?(:path) && buffer.path
|
|
592
|
+
path && path.start_with?(root.end_with?(File::SEPARATOR) ? root : "#{root}#{File::SEPARATOR}")
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def breakpoint_row?(current, row)
|
|
596
|
+
current.is_a?(Editor) && breakpoint_buffer?(current.buffer) && row.is_a?(Integer) && row.between?(0, current.buffer.line_count - 1)
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|
|
@@ -40,7 +40,7 @@ module Canopus
|
|
|
40
40
|
|
|
41
41
|
def local_path(uri)
|
|
42
42
|
raise Error, "resource URI is too long" unless uri.is_a?(String) && uri.bytesize <= 16_384
|
|
43
|
-
path = File.expand_path(
|
|
43
|
+
path = File.expand_path(Sadr::Protocol.path(uri))
|
|
44
44
|
raise Error, "resource path is too long" if path.bytesize > 4_096
|
|
45
45
|
unless path.start_with?(@root + File::SEPARATOR)
|
|
46
46
|
parent, pieces = File.dirname(path), [File.basename(path)]
|
|
@@ -159,7 +159,7 @@ module Canopus
|
|
|
159
159
|
@edit_count += 1
|
|
160
160
|
raise Error, "workspace edit text exceeds safety limits" if @text_bytes > (32 << 20) || @edit_count > 100_000
|
|
161
161
|
range = entry.fetch("range")
|
|
162
|
-
[
|
|
162
|
+
[Sadr::Protocol.offset(node.rope, range.fetch("start"))...Sadr::Protocol.offset(node.rope, range.fetch("end")), entry.fetch("newText")]
|
|
163
163
|
end
|
|
164
164
|
node.rope = node.rope.apply_edits(changes)
|
|
165
165
|
node.version += 1 unless changes.empty?
|