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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +191 -19
  4. data/assets/keymaps/emacs.jsonc +5 -0
  5. data/assets/keymaps/jetbrains.jsonc +5 -0
  6. data/assets/keymaps/sublime.jsonc +5 -0
  7. data/assets/keymaps/vscode.jsonc +5 -0
  8. data/docs/debugging.md +143 -0
  9. data/docs/distribution.md +40 -9
  10. data/docs/lsp.md +131 -0
  11. data/docs/performance.md +18 -0
  12. data/docs/providers.md +36 -0
  13. data/docs/release-log.md +12 -0
  14. data/docs/tasks.md +64 -0
  15. data/docs/testing.md +49 -0
  16. data/docs/vim.md +3 -0
  17. data/exe/canopus +4 -1
  18. data/lib/canopus/buffer.rb +291 -50
  19. data/lib/canopus/cli.rb +5 -2
  20. data/lib/canopus/command.rb +13 -4
  21. data/lib/canopus/controller.rb +183 -40
  22. data/lib/canopus/debug/breakpoints.rb +550 -0
  23. data/lib/canopus/debug/configuration.rb +291 -0
  24. data/lib/canopus/debug/console.rb +243 -0
  25. data/lib/canopus/debug/panel.rb +487 -0
  26. data/lib/canopus/debug/session.rb +354 -0
  27. data/lib/canopus/decoration.rb +4 -2
  28. data/lib/canopus/denebola_compat.rb +27 -0
  29. data/lib/canopus/diagnostics.rb +173 -0
  30. data/lib/canopus/display_map/overlay_map.rb +6 -2
  31. data/lib/canopus/editor/snippet_expandable.rb +3 -2
  32. data/lib/canopus/editor.rb +122 -31
  33. data/lib/canopus/git/state.rb +100 -0
  34. data/lib/canopus/language/background_analysis.rb +93 -14
  35. data/lib/canopus/language/document.rb +70 -34
  36. data/lib/canopus/language/symbol.rb +1 -0
  37. data/lib/canopus/language/syntax_worker.rb +61 -9
  38. data/lib/canopus/language.rb +3 -0
  39. data/lib/canopus/minimap.rb +201 -0
  40. data/lib/canopus/patch.rb +3 -1
  41. data/lib/canopus/plugins/api.rb +4 -2
  42. data/lib/canopus/plugins/isolated_runtime.rb +64 -7
  43. data/lib/canopus/plugins/registry.rb +114 -3
  44. data/lib/canopus/plugins.rb +8 -7
  45. data/lib/canopus/project.rb +15 -6
  46. data/lib/canopus/provider.rb +192 -0
  47. data/lib/canopus/regexp_compat.rb +36 -4
  48. data/lib/canopus/selection.rb +12 -0
  49. data/lib/canopus/settings/schema.rb +201 -0
  50. data/lib/canopus/settings.rb +328 -49
  51. data/lib/canopus/task/configuration.rb +356 -0
  52. data/lib/canopus/task/problem_matcher.rb +264 -0
  53. data/lib/canopus/task/runner.rb +314 -0
  54. data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
  55. data/lib/canopus/test_runner/discovery.rb +97 -0
  56. data/lib/canopus/test_runner/execution.rb +194 -0
  57. data/lib/canopus/test_runner/minitest.rb +116 -0
  58. data/lib/canopus/test_runner/rspec.rb +103 -0
  59. data/lib/canopus/test_runner/safe_walker.rb +37 -0
  60. data/lib/canopus/test_runner.rb +54 -0
  61. data/lib/canopus/theme/import.rb +131 -0
  62. data/lib/canopus/theme.rb +28 -4
  63. data/lib/canopus/version.rb +1 -1
  64. data/lib/canopus/vim/motionable.rb +1 -1
  65. data/lib/canopus/vim.rb +10 -16
  66. data/lib/canopus/workspace/auto_savable.rb +83 -0
  67. data/lib/canopus/workspace/debug_aware.rb +600 -0
  68. data/lib/canopus/workspace/edit/plan.rb +2 -2
  69. data/lib/canopus/workspace/editor_configurable.rb +113 -0
  70. data/lib/canopus/workspace/file_change_aware.rb +11 -3
  71. data/lib/canopus/workspace/git_aware.rb +80 -34
  72. data/lib/canopus/workspace/git_blame.rb +175 -0
  73. data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
  74. data/lib/canopus/workspace/git_diff_view.rb +641 -0
  75. data/lib/canopus/workspace/git_history.rb +171 -0
  76. data/lib/canopus/workspace/git_remote.rb +267 -0
  77. data/lib/canopus/workspace/git_staging.rb +583 -0
  78. data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
  79. data/lib/canopus/workspace/keymap_aware.rb +66 -0
  80. data/lib/canopus/workspace/language_aware.rb +3721 -135
  81. data/lib/canopus/workspace/language_server_configurable.rb +287 -71
  82. data/lib/canopus/workspace/persistent_undo.rb +169 -0
  83. data/lib/canopus/workspace/problems_aware.rb +131 -0
  84. data/lib/canopus/workspace/project_searchable.rb +88 -11
  85. data/lib/canopus/workspace/project_tree_editable.rb +3 -1
  86. data/lib/canopus/workspace/recoverable.rb +204 -0
  87. data/lib/canopus/workspace/session_persistable.rb +85 -13
  88. data/lib/canopus/workspace/settings_aware.rb +76 -2
  89. data/lib/canopus/workspace/task_aware.rb +194 -0
  90. data/lib/canopus/workspace/test_aware.rb +394 -0
  91. data/lib/canopus/workspace/trust.rb +73 -0
  92. data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
  93. data/lib/canopus/workspace/view.rb +399 -74
  94. data/lib/canopus/workspace.rb +688 -44
  95. data/lib/canopus.rb +20 -2
  96. data/sig/canopus.rbs +138 -31
  97. data/sig/controller.rbs +10 -3
  98. data/sig/debug.rbs +121 -0
  99. data/sig/decoration.rbs +12 -2
  100. data/sig/diagnostics.rbs +26 -0
  101. data/sig/minimap.rbs +20 -0
  102. data/sig/provider.rbs +29 -0
  103. data/sig/services.rbs +1 -16
  104. data/sig/task.rbs +92 -0
  105. data/sig/test_runner.rbs +109 -0
  106. data/sig/trust.rbs +17 -0
  107. data/sig/workspace_edits.rbs +2 -2
  108. data/sig/workspace_services.rbs +234 -2
  109. data/tools/check_dependencies.rb +21 -2
  110. data/tools/package.rb +172 -14
  111. data/tools/package_signature_test.rb +33 -0
  112. data/tools/package_test.rb +29 -0
  113. data/tools/sign_package.rb +123 -0
  114. data/tools/update.rb +189 -0
  115. data/tools/update_test.rb +71 -0
  116. metadata +206 -31
  117. data/lib/canopus/lsp/client.rb +0 -307
  118. data/lib/canopus/lsp/error.rb +0 -7
  119. data/lib/canopus/lsp/future/subscription.rb +0 -9
  120. data/lib/canopus/lsp/future.rb +0 -87
  121. data/lib/canopus/lsp/protocol.rb +0 -83
  122. data/lib/canopus/lsp/server_error.rb +0 -13
  123. data/lib/canopus/lsp/timeout.rb +0 -7
  124. data/lib/canopus/lsp/transport.rb +0 -123
  125. data/lib/canopus/lsp.rb +0 -19
  126. data/lib/canopus/project/search.rb +0 -164
  127. data/lib/canopus/project/search_worker/cancelled.rb +0 -3
  128. data/lib/canopus/project/search_worker/runner.rb +0 -9
  129. data/lib/canopus/project/search_worker.rb +0 -108
  130. data/lib/canopus/terminal/cell.rb +0 -7
  131. data/lib/canopus/terminal/grid.rb +0 -321
  132. data/lib/canopus/terminal/pty.rb +0 -178
  133. data/lib/canopus/terminal/scrollback.rb +0 -38
  134. data/lib/canopus/terminal/vt.rb +0 -398
  135. data/lib/canopus/terminal.rb +0 -13
  136. data/sig/lsp.rbs +0 -99
  137. 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(LSP::Protocol.path(uri))
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
- [LSP::Protocol.offset(node.rope, range.fetch("start"))...LSP::Protocol.offset(node.rope, range.fetch("end")), entry.fetch("newText")]
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?