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,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "zaniah/ui"
5
+
6
+ module Canopus
7
+ module Debug
8
+ class Panel
9
+ ITEM_LIMIT = 1_000
10
+ WATCH_LIMIT = 100
11
+ TEXT_LIMIT = 4_096
12
+ DEPTH_LIMIT = 32
13
+ ROOT_STACK = [:debug, :stack].freeze
14
+ ROOT_VARIABLES = [:debug, :variables].freeze
15
+ ROOT_WATCHES = [:debug, :watches].freeze
16
+ ROOT_BREAKPOINTS = [:debug, :breakpoints].freeze
17
+ ROOTS = [ROOT_STACK, ROOT_VARIABLES, ROOT_WATCHES, ROOT_BREAKPOINTS].freeze
18
+
19
+ attr_reader :tree, :watches, :expanded_paths, :selected_frame
20
+
21
+ def initialize(breakpoints:, post:, select_frame:, select_breakpoint:, report:, request_frame:)
22
+ callbacks = [post, select_frame, select_breakpoint, report, request_frame]
23
+ raise ArgumentError, "debug panel callbacks must be callable" unless callbacks.all? { |item| item.respond_to?(:call) }
24
+
25
+ @breakpoints = breakpoints
26
+ @post, @select_frame, @select_breakpoint = post, select_frame, select_breakpoint
27
+ @report, @request_frame = report, request_frame
28
+ @generation = 0
29
+ @pending = Set.new
30
+ @watches = [].freeze
31
+ @watch_tokens = {}
32
+ @expanded_paths = Set.new
33
+ @expanded_roots = ROOTS.to_set
34
+ @stack_frames, @watch_results = [], {}
35
+ @tree = Zaniah::UI::TreeView.new(nodes, height: 600)
36
+ .on_select { |value, _event, _context| select(value) }
37
+ .on_toggle { |id, open| remember_expansion(id, open) }
38
+ restore_expansions
39
+ end
40
+
41
+ def stopped(session, frame)
42
+ reset_requests
43
+ @session, @selected_frame = session, frame
44
+ @stack_frames = [frame].freeze
45
+ @watch_results = @watches.to_h { |expression| [expression, :loading] }
46
+ @tree.replace(nodes)
47
+ restore_expansions
48
+ request_stack
49
+ evaluate_watches
50
+ @request_frame.call
51
+ self
52
+ end
53
+
54
+ def continued(session)
55
+ return false unless @session.equal?(session)
56
+
57
+ reset_requests
58
+ @session = @selected_frame = nil
59
+ @stack_frames = [].freeze
60
+ @watch_results = {}
61
+ @tree.replace(nodes)
62
+ restore_expansions
63
+ @request_frame.call
64
+ true
65
+ end
66
+
67
+ def clear
68
+ reset_requests
69
+ @session = @selected_frame = nil
70
+ @stack_frames = [].freeze
71
+ @watch_results = {}
72
+ @tree.replace(nodes)
73
+ restore_expansions
74
+ @request_frame.call
75
+ nil
76
+ end
77
+
78
+ def refresh_breakpoints
79
+ @tree.replace_children(ROOT_BREAKPOINTS, breakpoint_nodes)
80
+ @request_frame.call
81
+ nil
82
+ end
83
+
84
+ def add_watch(expression)
85
+ expression = watch_expression(expression)
86
+ return false if @watches.include?(expression)
87
+ raise Error, "too many watch expressions" if @watches.length >= WATCH_LIMIT
88
+
89
+ @watches = [*@watches, expression].freeze
90
+ token = @watch_tokens[expression] = Object.new.freeze
91
+ @watch_results[expression] = @session && @selected_frame ? :loading : nil
92
+ @tree.replace_children(ROOT_WATCHES, watch_nodes)
93
+ evaluate_watch(expression, token) if @session && @selected_frame
94
+ @request_frame.call
95
+ true
96
+ end
97
+
98
+ def remove_watch(expression)
99
+ replacement = @watches.reject { |item| item == expression }
100
+ return false if replacement.length == @watches.length
101
+
102
+ @watches = replacement.freeze
103
+ @watch_tokens.delete(expression)
104
+ @watch_results.delete(expression)
105
+ @expanded_paths.delete_if { |path| path.first == :watch && path.dig(1, 0) == expression }
106
+ @tree.replace_children(ROOT_WATCHES, watch_nodes)
107
+ @request_frame.call
108
+ true
109
+ end
110
+
111
+ private
112
+
113
+ def nodes
114
+ [root_node(ROOT_STACK, "Call Stack", ->(_value) { stack_nodes }),
115
+ root_node(ROOT_VARIABLES, "Variables", ->(_value) { load_scopes }),
116
+ root_node(ROOT_WATCHES, "Watch", ->(_value) { watch_nodes }),
117
+ root_node(ROOT_BREAKPOINTS, "Breakpoints", ->(_value) { breakpoint_nodes })].freeze
118
+ end
119
+
120
+ def root_node(id, label, loader) = {id: id, label: label, value: nil, children: loader}.freeze
121
+
122
+ def stack_nodes
123
+ return [status_node(:stack, "Not stopped")] if @stack_frames.empty?
124
+
125
+ @stack_frames.each_with_index.map do |frame, index|
126
+ path = frame.source && (frame.source["path"] || frame.source[:path])
127
+ label = path ? text(frame.name, " · ", display_basename(path), ":", frame.line) : text(frame.name)
128
+ {id: [:debug_frame, index].freeze, label: label,
129
+ value: {kind: :frame, generation: @generation, frame: frame}.freeze}.freeze
130
+ end.freeze
131
+ end
132
+
133
+ def breakpoint_nodes
134
+ entries = @breakpoints.entries.first(ITEM_LIMIT)
135
+ return [status_node(:breakpoints, "No breakpoints")] if entries.empty?
136
+
137
+ entries.map do |entry|
138
+ state = entry.enabled ? "" : " (disabled)"
139
+ {id: [:debug_breakpoint, entry.path, entry.line].freeze,
140
+ label: text(entry.path, ":", entry.line, state),
141
+ value: {kind: :breakpoint, entry: entry}.freeze}.freeze
142
+ end.freeze
143
+ end
144
+
145
+ def watch_nodes
146
+ return [status_node(:watches, "No watch expressions")] if @watches.empty?
147
+
148
+ @watches.map do |expression|
149
+ result = @watch_results[expression]
150
+ label = case result
151
+ when :loading then text(expression, " = …")
152
+ when Hash then text(expression, " = ", result.fetch(:label))
153
+ else text(expression, " = Not available")
154
+ end
155
+ path = [expression_component(expression)].freeze
156
+ node = {id: watch_variable_id(path), label: label,
157
+ value: {kind: :watch, expression: expression}.freeze}
158
+ if result.is_a?(Hash) && result[:expandable]
159
+ id = watch_variable_id(path)
160
+ node[:children] = watch_variables_loader(expression, path, id, @watch_tokens.fetch(expression))
161
+ end
162
+ node.freeze
163
+ end.freeze
164
+ end
165
+
166
+ def request_stack
167
+ generation = @generation
168
+ await(@session.stack_frames(levels: 100), generation) do |frames, error|
169
+ if error
170
+ @tree.replace_children(ROOT_STACK, [status_node(:stack_error, "Unavailable")])
171
+ @report.call(text("Cannot load call stack: ", error.message))
172
+ else
173
+ frames = frames.first(ITEM_LIMIT)
174
+ @stack_frames = (frames.empty? ? [@selected_frame] : frames).freeze
175
+ @tree.replace_children(ROOT_STACK, stack_nodes)
176
+ end
177
+ end
178
+ rescue StandardError => error
179
+ @report.call(text("Cannot load call stack: ", error.message))
180
+ end
181
+
182
+ def load_scopes
183
+ return [status_node(:variables, "Not stopped")] unless active?
184
+
185
+ generation, session, frame = @generation, @session, @selected_frame
186
+ await(session.scopes(frame.id), generation) do |scopes, error|
187
+ if error
188
+ replace_unavailable(ROOT_VARIABLES, error)
189
+ else
190
+ @tree.replace_children(ROOT_VARIABLES, scope_nodes(scopes))
191
+ restore_expansions
192
+ end
193
+ end
194
+ [status_node(:variables_loading, "Loading…")]
195
+ rescue StandardError => error
196
+ unavailable(error)
197
+ end
198
+
199
+ def scope_nodes(scopes)
200
+ occurrences = Hash.new(0)
201
+ values = scopes.first(ITEM_LIMIT).each_with_index.filter_map do |scope, index|
202
+ component = path_component(scope.name, occurrences)
203
+ next oversized_node(:scope, index, scope.name) unless component
204
+
205
+ path = [component].freeze
206
+ node = {id: variable_id(path), label: text(scope.name), value: {kind: :scope}.freeze}
207
+ node[:children] = variables_loader(path, variable_id(path)) unless scope.variables_reference.zero?
208
+ node.freeze
209
+ end
210
+ values.empty? ? [status_node(:variables_empty, "No variables")] : values.freeze
211
+ end
212
+
213
+ def load_variables(path, node_id)
214
+ return [status_node([:stale, node_id], "Not stopped")] unless active?
215
+ return [status_node([:depth, node_id], "Maximum depth reached")] if path.length >= DEPTH_LIMIT
216
+
217
+ generation = @generation
218
+ resolve_reference(path, generation) do |reference, error|
219
+ if error
220
+ replace_unavailable(node_id, error)
221
+ else
222
+ await(@session.variables(reference), generation) do |variables, request_error|
223
+ if request_error
224
+ replace_unavailable(node_id, request_error)
225
+ else
226
+ @tree.replace_children(node_id, variable_nodes(variables, path, :variables))
227
+ restore_expansions
228
+ end
229
+ end
230
+ end
231
+ end
232
+ [status_node([:loading, node_id], "Loading…")]
233
+ rescue StandardError => error
234
+ unavailable(error)
235
+ end
236
+
237
+ def load_watch_variables(expression, path, node_id, token)
238
+ return [status_node([:stale, node_id], "Not stopped")] unless active? && watch_current?(expression, token)
239
+ return [status_node([:depth, node_id], "Maximum depth reached")] if path.length >= DEPTH_LIMIT
240
+
241
+ generation = @generation
242
+ await(@session.evaluate(expression, frame_id: @selected_frame.id), generation) do |variable, error|
243
+ next unless watch_current?(expression, token)
244
+
245
+ if error
246
+ replace_unavailable(node_id, error)
247
+ else
248
+ traverse_reference(variable.variables_reference, path.drop(1), generation) do |reference, resolve_error|
249
+ next unless watch_current?(expression, token)
250
+
251
+ if resolve_error
252
+ replace_unavailable(node_id, resolve_error)
253
+ else
254
+ await(@session.variables(reference), generation) do |variables, request_error|
255
+ next unless watch_current?(expression, token)
256
+
257
+ if request_error
258
+ replace_unavailable(node_id, request_error)
259
+ else
260
+ @tree.replace_children(node_id, variable_nodes(variables, path, :watch))
261
+ restore_expansions
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
268
+ [status_node([:loading, node_id], "Loading…")]
269
+ rescue StandardError => error
270
+ unavailable(error)
271
+ end
272
+
273
+ def variable_nodes(variables, parent, kind)
274
+ occurrences = Hash.new(0)
275
+ values = variables.first(ITEM_LIMIT).each_with_index.filter_map do |variable, index|
276
+ component = path_component(variable.name, occurrences)
277
+ next oversized_node(:variable, [parent, index], variable.name) unless component
278
+
279
+ path = [*parent, component].freeze
280
+ id = kind == :watch ? watch_variable_id(path) : variable_id(path)
281
+ label = text(variable.name, " = ", variable.value, variable.type && " · ", variable.type)
282
+ node = {id: id, label: label, value: {kind: :variable}.freeze}
283
+ unless variable.variables_reference.zero?
284
+ node[:children] = if kind == :watch
285
+ expression = parent.first.first
286
+ watch_variables_loader(expression, path, id, @watch_tokens.fetch(expression))
287
+ else
288
+ variables_loader(path, id)
289
+ end
290
+ end
291
+ node.freeze
292
+ end
293
+ values.empty? ? [status_node([:empty, parent], "No variables")] : values.freeze
294
+ end
295
+
296
+ def resolve_reference(path, generation, &done)
297
+ session, frame = @session, @selected_frame
298
+ await(session.scopes(frame.id), generation) do |scopes, error|
299
+ if error
300
+ done.call(nil, error)
301
+ else
302
+ scope = occurrence(scopes, path.first)
303
+ if !scope || scope.variables_reference.zero?
304
+ done.call(nil, Error.new("variable path is no longer available"))
305
+ else
306
+ traverse_reference(scope.variables_reference, path.drop(1), generation, &done)
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ def traverse_reference(reference, path, generation, &done)
313
+ return done.call(reference, nil) if path.empty?
314
+ return done.call(nil, Error.new("variable path is no longer available")) if reference.zero?
315
+
316
+ await(@session.variables(reference), generation) do |variables, error|
317
+ if error
318
+ done.call(nil, error)
319
+ else
320
+ variable = occurrence(variables, path.first)
321
+ variable ? traverse_reference(variable.variables_reference, path.drop(1), generation, &done) :
322
+ done.call(nil, Error.new("variable path is no longer available"))
323
+ end
324
+ end
325
+ end
326
+
327
+ def evaluate_watches
328
+ @watches.each { |expression| evaluate_watch(expression, @watch_tokens.fetch(expression)) }
329
+ end
330
+
331
+ def evaluate_watch(expression, token)
332
+ generation, session, frame = @generation, @session, @selected_frame
333
+ await(session.evaluate(expression, frame_id: frame.id), generation) do |variable, error|
334
+ next unless watch_current?(expression, token)
335
+
336
+ @watch_results[expression] = if error
337
+ {label: text("Unavailable: ", error.message), expandable: false}.freeze
338
+ else
339
+ {label: text(variable.value, variable.type && " · ", variable.type),
340
+ expandable: !variable.variables_reference.zero?}.freeze
341
+ end
342
+ @tree.replace_children(ROOT_WATCHES, watch_nodes)
343
+ restore_expansions
344
+ @request_frame.call
345
+ end
346
+ rescue StandardError => error
347
+ return unless watch_current?(expression, token)
348
+
349
+ @watch_results[expression] = {label: text("Unavailable: ", error.message), expandable: false}.freeze
350
+ @tree.replace_children(ROOT_WATCHES, watch_nodes)
351
+ end
352
+
353
+ def await(future, generation, &accept)
354
+ @pending.add(future)
355
+ future.on_complete do |value, error|
356
+ @post.call do
357
+ @pending.delete(future)
358
+ accept.call(value, error) if generation == @generation && active?
359
+ end
360
+ end
361
+ future
362
+ end
363
+
364
+ def reset_requests
365
+ @generation += 1
366
+ pending, @pending = @pending, Set.new
367
+ pending.each(&:cancel)
368
+ end
369
+
370
+ def active? = !!(@session && @selected_frame)
371
+
372
+ def select(value)
373
+ return false unless value.is_a?(Hash)
374
+
375
+ case value[:kind]
376
+ when :frame
377
+ return false unless value[:generation] == @generation && @session
378
+
379
+ frame = value.fetch(:frame)
380
+ return true if @selected_frame == frame
381
+
382
+ reset_requests
383
+ @selected_frame = frame
384
+ @tree.replace_children(ROOT_STACK, stack_nodes)
385
+ @select_frame.call(@session, @selected_frame)
386
+ @watch_results = @watches.to_h { |expression| [expression, :loading] }
387
+ @tree.invalidate(ROOT_VARIABLES)
388
+ restore_expansions
389
+ @tree.replace_children(ROOT_WATCHES, watch_nodes)
390
+ evaluate_watches
391
+ true
392
+ when :breakpoint
393
+ @select_breakpoint.call(value.fetch(:entry))
394
+ else false
395
+ end
396
+ end
397
+
398
+ def remember_expansion(id, open)
399
+ if ROOTS.include?(id)
400
+ open ? @expanded_roots.add(id) : @expanded_roots.delete(id)
401
+ elsif id.is_a?(Array) && id.first == :debug_variable
402
+ remember_path([:variables, *id.drop(1)].freeze, open)
403
+ elsif id.is_a?(Array) && id.first == :debug_watch_variable
404
+ remember_path([:watch, *id.drop(1)].freeze, open)
405
+ end
406
+ end
407
+
408
+ def remember_path(path, open)
409
+ open ? @expanded_paths.add(path) : @expanded_paths.delete(path)
410
+ end
411
+
412
+ def restore_expansions
413
+ @expanded_roots.each { |id| @tree.expand(id) }
414
+ @expanded_paths.sort_by(&:length).each do |path|
415
+ id = path.first == :variables ? variable_id(path.drop(1)) : watch_variable_id(path.drop(1))
416
+ @tree.expand(id)
417
+ end
418
+ end
419
+
420
+ def replace_unavailable(node_id, error)
421
+ @tree.replace_children(node_id, [status_node([:error, node_id], "Unavailable")])
422
+ @report.call(text("Cannot load variables: ", error.message))
423
+ end
424
+
425
+ def unavailable(error)
426
+ @report.call(text("Cannot load variables: ", error.message))
427
+ [status_node([:error, error.object_id], "Unavailable")]
428
+ end
429
+
430
+ def path_component(name, occurrences)
431
+ return unless name.bytesize <= TEXT_LIMIT
432
+
433
+ index = occurrences[name]
434
+ occurrences[name] += 1
435
+ [name.dup.freeze, index].freeze
436
+ end
437
+
438
+ def expression_component(expression) = [expression, 0].freeze
439
+
440
+ def occurrence(values, component)
441
+ name, index = component
442
+ values.select { |value| value.name == name }[index]
443
+ end
444
+
445
+ def variable_id(path) = [:debug_variable, *path].freeze
446
+ def watch_variable_id(path) = [:debug_watch_variable, *path].freeze
447
+
448
+ def variables_loader(path, node_id) = ->(_value) { load_variables(path, node_id) }
449
+
450
+ def watch_variables_loader(expression, path, node_id, token)
451
+ ->(_value) { load_watch_variables(expression, path, node_id, token) }
452
+ end
453
+
454
+ def watch_current?(expression, token) = @watch_tokens[expression].equal?(token)
455
+
456
+ def oversized_node(kind, index, name)
457
+ {id: [:debug_oversized, kind, index].freeze, label: text(name), value: nil}.freeze
458
+ end
459
+
460
+ def status_node(id, label)
461
+ {id: [:debug_status, id].freeze, label: label, value: nil}.freeze
462
+ end
463
+
464
+ def watch_expression(value)
465
+ valid = value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? &&
466
+ value.bytesize.between?(1, TEXT_LIMIT) && !value.include?("\0") && !value.strip.empty?
467
+ raise ArgumentError, "invalid watch expression" unless valid
468
+
469
+ value.dup.freeze
470
+ end
471
+
472
+ def display_basename(value)
473
+ path = bounded_text(value).delete("\0")
474
+ path.empty? ? "" : File.basename(path)
475
+ end
476
+
477
+ def bounded_text(value)
478
+ value.to_s.byteslice(0, TEXT_LIMIT).to_s.dup.force_encoding(Encoding::UTF_8).scrub("")
479
+ end
480
+
481
+ def text(*values)
482
+ value = values.compact.map { |item| bounded_text(item) }.join.gsub(/\s+/, " ").strip
483
+ value.length > 200 ? value.each_char.first(199).join + "…" : value
484
+ end
485
+ end
486
+ end
487
+ end