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
@@ -2,27 +2,47 @@
2
2
 
3
3
  require "json"
4
4
  require "fileutils"
5
+ require "shellwords"
6
+ sadr_path = ENV["SADR_PATH"]
7
+ sadr_root = File.expand_path("../..", __dir__)
8
+ sadr_path ? require(File.expand_path("lib/sadr", File.expand_path(sadr_path, sadr_root))) : require("sadr")
5
9
  require_relative "settings"
6
10
  require_relative "theme"
7
11
  require_relative "vim"
8
- require_relative "lsp"
9
12
  require_relative "pane"
10
13
  require_relative "project/tree"
11
14
  require_relative "command"
12
15
  require_relative "panel"
13
16
  require_relative "decoration"
17
+ require_relative "provider"
18
+ require_relative "minimap"
19
+ require_relative "diagnostics"
14
20
 
15
21
  module Canopus
16
22
  class Workspace
17
23
  ClosedTab = Data.define(:path, :selections, :scroll_x, :scroll_y, :pane_id, :index)
18
- attr_reader :panes, :active_pane, :buffers, :actions, :commands, :settings, :theme, :project, :clients, :root, :docks, :panels, :decorations
19
- attr_reader :terminals, :active_terminal_index
24
+ LANGUAGE_SERVER_CAPABILITIES = {
25
+ "completion" => "completionProvider", "diagnostics" => nil, "codeAction" => "codeActionProvider",
26
+ "formatting" => "documentFormattingProvider", "definition" => "definitionProvider",
27
+ "typeDefinition" => "typeDefinitionProvider", "implementation" => "implementationProvider",
28
+ "hover" => "hoverProvider", "signatureHelp" => "signatureHelpProvider", "references" => "referencesProvider",
29
+ "rename" => "renameProvider", "documentSymbol" => "documentSymbolProvider", "codeLens" => "codeLensProvider",
30
+ "inlayHint" => "inlayHintProvider", "semanticTokens" => "semanticTokensProvider",
31
+ "documentHighlight" => "documentHighlightProvider", "foldingRange" => "foldingRangeProvider",
32
+ "selectionRange" => "selectionRangeProvider", "callHierarchy" => "callHierarchyProvider",
33
+ "typeHierarchy" => "typeHierarchyProvider", "documentLink" => "documentLinkProvider",
34
+ "linkedEditingRange" => "linkedEditingRangeProvider", "workspaceSymbol" => "workspaceSymbolProvider"
35
+ }.freeze
36
+ attr_reader :panes, :active_pane, :buffers, :actions, :commands, :settings, :theme, :project, :clients, :root, :docks, :panels, :decorations, :providers, :minimap, :diagnostics, :breakpoints, :trust
37
+ attr_reader :terminals, :active_terminal_index, :terminal_layout
20
38
  attr_accessor :window, :terminal_composition, :selected_project_path, :performance
21
39
  attr_reader :message, :palette
22
40
 
23
41
  def initialize(root: Dir.pwd, settings: nil)
24
42
  @root = File.realpath(root)
43
+ @trust = Trust.new(@root)
25
44
  @settings = settings || Settings.new(Settings.user_path, File.join(@root, ".canopus", "settings.jsonc"))
45
+ @main_queue = Queue.new
26
46
  @panes, @buffers = [Pane.new], {}
27
47
  @active_pane = @panes.first
28
48
  @layout = {pane: @active_pane}
@@ -41,6 +61,14 @@ module Canopus
41
61
  @panels.register(Panel::Definition.new("terminal", "Terminal", nil, :bottom, -> { terminal }, nil))
42
62
  @panels.register(Panel::Definition.new("search", "Search", nil, :left, -> { palette_open(:project_search) }, nil))
43
63
  @panels.register(Panel::Definition.new("explorer", "Explorer", nil, :left, -> { project_tree }, nil))
64
+ @panels.register(Panel::Definition.new("scm", "Source Control", nil, :left, -> { scm_panel }, nil), visible: false)
65
+ @panels.register(Panel::Definition.new("hierarchy", "Hierarchy", nil, :right, -> { hierarchy_tree }, nil), visible: false)
66
+ @panels.register(Panel::Definition.new("problems", "Problems", nil, :right, -> { problems_tree }, nil), visible: false)
67
+ @panels.register(Panel::Definition.new("debug", "Debug", nil, :left, -> { debug_tree }, nil), visible: false)
68
+ @panels.register(Panel::Definition.new("debug_console", "Debug Console", nil, :bottom, -> { debug_console_tree }, nil), visible: false)
69
+ @panels.register(Panel::Definition.new("output", "Output", nil, :bottom, -> { task_output }, nil), visible: false)
70
+ @panels.register(Panel::Definition.new("test", "Tests", nil, :left, -> { test_tree }, nil), visible: false)
71
+ @panels.hide("hierarchy")
44
72
  @decorations = Decoration::Registry.new
45
73
  @decorations.register(:selection_match) do |buffer, rows, current|
46
74
  selections = current.is_a?(Editor) && current.buffer.equal?(buffer) ? current.selections : buffer.selections
@@ -54,9 +82,30 @@ module Canopus
54
82
  end
55
83
  end
56
84
  @decorations.register(:git) { |buffer, rows| git_decorations(buffer, rows) }
85
+ @decorations.register(:scm_diff) { |buffer, rows| scm_diff_decorations(buffer, rows) }
86
+ @decorations.register(:merge_conflict) { |buffer, rows| git_conflict_decorations(buffer, rows) }
87
+ @decorations.register(:blame) { |buffer, rows, current| git_blame_decorations(buffer, rows, current) }
88
+ @decorations.register(:diagnostics) { |buffer, rows| diagnostic_decorations(buffer, rows) }
89
+ @decorations.register(:document_highlight) { |buffer, rows, current| document_highlight_decorations(buffer, rows, current) }
90
+ @decorations.register(:document_link) { |buffer, rows, current| document_link_decorations(buffer, rows, current) }
91
+ @decorations.register(:inlay_hint) { |buffer, rows| inlay_hint_decorations(buffer, rows) }
92
+ @decorations.register(:code_lens) { |buffer, rows| code_lens_decorations(buffer, rows) }
93
+ @decorations.register(:bracket) { |buffer, rows, current| bracket_decorations(buffer, rows, current) }
94
+ initialize_breakpoints
95
+ @minimap = Minimap.new
96
+ @diagnostics = Diagnostics::Registry.new do |uri|
97
+ @window ? post { diagnostics_changed(uri) } : diagnostics_changed(uri)
98
+ end
99
+ publish_settings_diagnostics(@settings.paths)
100
+ @providers = Provider::Registry.new
101
+ @providers.register_completion(:lsp, priority: 100) { |buffer, offset, context| lsp_completions(buffer, offset, context) }
57
102
  @languages, @terminals = {}, []
58
103
  @active_terminal_index = 0
104
+ @terminal_layout = nil
59
105
  @closed_tabs, @terminal_names = [], {}
106
+ @terminal_profiles = {}.compare_by_identity
107
+ initialize_tasks
108
+ initialize_tests
60
109
  register_actions
61
110
  end
62
111
  def editor = @active_pane.active
@@ -66,7 +115,12 @@ module Canopus
66
115
  end
67
116
  def terminal_visible = @panels.visible?("terminal") && @docks[:bottom][:visible]
68
117
  def terminal_visible=(visible)
69
- visible ? @panels.show("terminal") : @panels.hide("terminal")
118
+ if visible
119
+ @panels.hide("output") if @panels.visible?("output")
120
+ @panels.show("terminal")
121
+ else
122
+ @panels.hide("terminal")
123
+ end
70
124
  end
71
125
  def terminal = @terminals[@active_terminal_index]
72
126
  def terminal=(value)
@@ -76,10 +130,20 @@ module Canopus
76
130
  @terminal_resize_times&.clear
77
131
  @terminals = value ? [value] : []
78
132
  @active_terminal_index = 0
133
+ @terminal_layout = value ? {terminal: value} : nil
134
+ @terminal_profiles.clear
79
135
  value
80
136
  end
81
137
  def palette=(value)
138
+ @palette_generation = @palette_generation.to_i + 1
139
+ if @workspace_symbol_request && value&.dig(:workspace_symbol_generation) != @workspace_symbol_generation
140
+ cancel_workspace_symbol_search
141
+ end
82
142
  previous, @palette = @palette, @closed ? nil : value
143
+ rename = previous&.dig(:kind) == :rename && previous[:rename]
144
+ if rename && !previous.equal?(@submitting_rename_palette) && !rename.equal?(@palette&.dig(:rename))
145
+ release_rename_snapshot(rename)
146
+ end
83
147
  response = previous&.dig(:response)
84
148
  unless response.nil? || response.equal?(@palette&.dig(:response))
85
149
  response.fulfill({"applied" => false, "failureReason" => value ? "Replaced by another dialog" : "User cancelled"})
@@ -117,6 +181,7 @@ module Canopus
117
181
  def refresh_files
118
182
  @files = @project_tree = @project_entries = nil
119
183
  @finder_index = nil
184
+ refresh_tests if @test_discovery_started && !@closed
120
185
  if @palette && @palette[:kind] == :files
121
186
  @palette.delete(:search)
122
187
  update_palette
@@ -126,9 +191,16 @@ module Canopus
126
191
  def vim = @vim_states[editor] ||= Vim.new(editor).tap { |state| state.on_command = ->(command) { ex_command(command) } }
127
192
  def open(path)
128
193
  absolute = canonical_path(path)
129
- buffer = @buffers[absolute] ||= File.file?(absolute) ? Buffer.open(absolute) : Buffer.new("", path: absolute)
194
+ buffer = @buffers[absolute]
195
+ unless buffer
196
+ buffer = File.file?(absolute) ? Buffer.open(absolute) : Buffer.new("", path: absolute)
197
+ @buffers[absolute] = buffer
198
+ mark_persistent_undo_path(buffer, path)
199
+ load_persistent_undo(buffer)
200
+ end
130
201
  @vim_states[editor]&.deactivate unless editor&.buffer.equal?(buffer)
131
202
  opened = @active_pane.open(buffer)
203
+ attach_breakpoints(buffer)
132
204
  @recent_files ||= []
133
205
  @recent_files.delete(absolute)
134
206
  @recent_files.unshift(absolute)
@@ -137,6 +209,7 @@ module Canopus
137
209
  opened.language = definition_for(absolute)
138
210
  apply_editor_settings(opened)
139
211
  @project_tree&.reveal(absolute.delete_prefix(@root + File::SEPARATOR))
212
+ invalidate_hidden_selection_ranges
140
213
  @window&.request_frame
141
214
  opened
142
215
  end
@@ -150,25 +223,46 @@ module Canopus
150
223
  end
151
224
  File.join(File.realpath(parent), *pieces)
152
225
  end
153
- def save_buffer(buffer = editor.buffer, path: buffer.path)
226
+ def save_buffer(buffer = editor.buffer, path: buffer.path, encoding: nil, confirmed: false)
154
227
  if buffer.is_a?(MultiBuffer)
155
- buffer.excerpts.map(&:buffer).uniq.each { |source| save_buffer(source) if source.dirty? }
228
+ buffer.excerpts.map(&:buffer).uniq.each { |source| save_buffer(source, encoding: encoding) if source.dirty? }
156
229
  return buffer
157
230
  end
158
231
  target = path && canonical_path(path)
159
232
  raise Error, "Choose a path before saving" unless target
160
233
  existing = @buffers[target]
161
234
  raise Error, "Destination is already open in another buffer" if existing && !existing.equal?(buffer)
235
+ unless confirmed || buffer.roundtrip?(encoding: encoding)
236
+ self.palette = {kind: :confirm_encoding_save, query: "Encoding cannot represent all characters. Save anyway?",
237
+ index: 1, matches: ["Save anyway", "Cancel"], buffer: buffer, target: target, encoding: encoding}
238
+ return buffer
239
+ end
162
240
  previous = buffer.path
163
- result = buffer.save(target)
241
+ prepare_editorconfig_save(buffer, target)
242
+ @running_save_actions ||= {}.compare_by_identity
243
+ unless @running_save_actions.key?(buffer)
244
+ @running_save_actions[buffer] = true
245
+ begin
246
+ run_save_actions(buffer) if previous
247
+ rescue StandardError => error
248
+ notify("Save actions failed: #{error.message}")
249
+ ensure
250
+ @running_save_actions.delete(buffer)
251
+ end
252
+ end
253
+ result = buffer.save(target, encoding: encoding)
164
254
  if previous != buffer.path
255
+ relocate_breakpoints(buffer)
256
+ invalidate_hierarchy(buffer)
257
+ invalidate_prepare_rename(buffer)
258
+ invalidate_document_links(buffer)
259
+ invalidate_linked_editing_ranges(buffer)
165
260
  invalidate_git
166
261
  # Close the old URI before registering this buffer under its new path.
167
262
  @opened_lsp_documents&.keys&.each do |client, document|
168
263
  next unless document.equal?(buffer)
169
- @opened_lsp_documents.delete([client, document])
170
264
  begin
171
- client.close_document(LSP::Protocol.uri(previous)) if previous
265
+ close_language_document(client, document, uri: Sadr::Protocol.uri(previous)) if previous
172
266
  rescue StandardError => error
173
267
  @message = "File saved; language server close failed: #{error.message}"
174
268
  end
@@ -187,19 +281,48 @@ module Canopus
187
281
  @opened_lsp_documents&.each_key do |client, document|
188
282
  next unless document.equal?(buffer)
189
283
  begin
190
- client.save_document(LSP::Protocol.uri(buffer.path))
284
+ client.save(Sadr::Protocol.uri(buffer.path))
191
285
  rescue StandardError => error
192
286
  self.message = "File saved; language server notification failed: #{error.message}"
193
287
  end
194
288
  end
195
289
  result
196
290
  end
291
+
292
+ def show_encoding_actions(buffer = editor&.buffer)
293
+ return unless buffer&.path && !buffer.read_only
294
+
295
+ choices = [Encoding::UTF_8, Encoding::UTF_16LE, Encoding::UTF_16BE, Encoding::UTF_32LE, Encoding::UTF_32BE,
296
+ Encoding::Windows_31J,
297
+ Encoding::EUC_JP, Encoding::ISO_2022_JP].uniq
298
+ actions = choices.flat_map do |encoding|
299
+ [["Reload as #{encoding.name}", :reload, encoding], ["Save as #{encoding.name}", :save, encoding]]
300
+ end
301
+ self.palette = {kind: :encoding_actions, query: +"", index: 0, matches: actions.map(&:first),
302
+ encoding_actions: actions, buffer: buffer}
303
+ @palette
304
+ end
305
+
306
+ def convert_line_endings(to, confirmed: false)
307
+ buffer = editor&.buffer
308
+ return unless buffer && !buffer.read_only
309
+ if buffer.mixed_line_endings? && !confirmed
310
+ self.palette = {kind: :confirm_line_ending_conversion,
311
+ query: "Mixed line endings will be normalized. Continue?", index: 1,
312
+ matches: ["Convert line endings", "Cancel"], buffer: buffer, to: to}
313
+ return buffer
314
+ end
315
+
316
+ buffer.convert_line_endings(to: to)
317
+ self.message = "Line endings converted to #{to.to_s.upcase}"
318
+ end
197
319
  def new_buffer
198
320
  @vim_states[editor]&.deactivate
199
321
  buffer = Buffer.new
200
322
  @buffers[buffer.object_id] = buffer
201
323
  opened = @active_pane.open(buffer)
202
324
  apply_editor_settings(opened)
325
+ invalidate_hidden_selection_ranges
203
326
  opened
204
327
  end
205
328
  def focus(pane)
@@ -213,6 +336,7 @@ module Canopus
213
336
  @vim_states[editor]&.deactivate unless tab.equal?(editor)
214
337
  focus(pane)
215
338
  pane.activate(pane.editors.index(tab))
339
+ invalidate_hidden_selection_ranges
216
340
  end
217
341
  def split(direction = :horizontal)
218
342
  raise ArgumentError, "invalid split" unless [:horizontal, :vertical].include?(direction)
@@ -245,6 +369,16 @@ module Canopus
245
369
  end
246
370
  sources = current.buffer.is_a?(MultiBuffer) ? current.buffer.excerpts.map(&:buffer).uniq : []
247
371
  @vim_states.delete(current)&.dispose
372
+ @sticky_fallback_cache&.delete(current)
373
+ @sticky_context_cache&.clear
374
+ invalidate_document_highlights(editor: current)
375
+ invalidate_folding_ranges(editor: current)
376
+ invalidate_selection_ranges(editor: current)
377
+ invalidate_hierarchy(editor: current)
378
+ invalidate_prepare_rename(editor: current)
379
+ invalidate_document_links(editor: current)
380
+ invalidate_linked_editing_ranges(editor: current)
381
+ invalidate_brackets(current.buffer)
248
382
  pane.close(current, discard: discard, activate: @settings["tabs"]["activate_on_close"].to_sym)
249
383
  release_buffer(current.buffer, discard: discard)
250
384
  sources.each { |source| release_buffer(source, discard: discard) }
@@ -329,25 +463,63 @@ module Canopus
329
463
  to.active_index = index
330
464
  to.pin(editor) if pinned
331
465
  focus(to)
466
+ invalidate_hidden_selection_ranges
467
+ end
468
+
469
+ def new_terminal(cwd: nil, profile: nil)
470
+ previous = terminal
471
+ created = start_terminal(cwd: cwd, profile: profile)
472
+ @terminal_layout = replace_terminal_leaf(@terminal_layout, previous, created) || {terminal: created}
473
+ invalidate_terminal_layout_resize
474
+ created
475
+ end
476
+
477
+ def split_terminal(direction, profile: nil)
478
+ raise ArgumentError, "invalid terminal split direction" unless %i[horizontal vertical].include?(direction)
479
+ previous = terminal
480
+ return new_terminal(profile: profile) unless previous
481
+ created = start_terminal(cwd: terminal_cwd(previous), profile: profile)
482
+ split = {direction: direction, ratio: 0.5, children: [{terminal: previous}, {terminal: created}]}
483
+ @terminal_layout = replace_terminal_node(@terminal_layout, previous, split) || split
484
+ invalidate_terminal_layout_resize
485
+ created
332
486
  end
333
487
 
334
- def new_terminal(cwd: terminal_working_directory)
488
+ def terminal_profile(current = terminal) = @terminal_profiles[current]
489
+
490
+ def start_terminal(cwd: nil, profile: nil)
335
491
  options = @settings["terminal"]
336
- created = Terminal::PTY.new(command: options["shell"] || ENV.fetch("SHELL", "/bin/sh"), cwd: cwd,
337
- columns: 100, rows: 12, env: options["env"], scrollback: options["scrollback_lines"],
492
+ cwd ||= terminal&.respond_to?(:cwd) && terminal.cwd
493
+ cwd = terminal_working_directory unless cwd && File.directory?(cwd)
494
+ profile ||= options["default_profile"]
495
+ configured = profile && options["profiles"].fetch(profile)
496
+ fallback = Gem.win_platform? ? ENV.fetch("COMSPEC", "cmd.exe") : ENV.fetch("SHELL", "/bin/sh")
497
+ command = configured && terminal_profile_command(configured) || options["shell"] || fallback
498
+ env = options["env"].merge(configured&.fetch("env", {}) || {})
499
+ .merge("CANOPUS" => "1", "EDITOR" => "canopus --wait")
500
+ created = Tarazed::Session.new(command: command, cwd: cwd,
501
+ columns: 100, rows: 12, env: env, scrollback_limit: options["scrollback_lines"],
338
502
  queue_limit_bytes: options["queue_limit_bytes"])
503
+ inject_shell_integration(created) if options["shell_integration"]
339
504
  @terminals << created
505
+ @terminal_profiles[created] = profile if profile
340
506
  @active_terminal_index = @terminals.length - 1
341
507
  self.terminal_visible = true
342
- resize_terminal(*@terminal_dimensions, final: true) if @terminal_dimensions
508
+ resize_terminal(*@terminal_dimensions, final: true, terminal: created) if @terminal_dimensions
343
509
  @window&.request_frame
344
510
  created
345
511
  end
512
+ private :start_terminal
346
513
 
347
514
  def activate_terminal(index)
348
515
  raise IndexError, "terminal tab outside panel" unless index.is_a?(Integer) && index.between?(0, @terminals.length - 1)
516
+ previous = terminal
349
517
  @active_terminal_index = index
350
- resize_terminal(*@terminal_dimensions, final: true) if @terminal_dimensions
518
+ unless terminal_layout_include?(terminal)
519
+ @terminal_layout = replace_terminal_leaf(@terminal_layout, previous, terminal)
520
+ invalidate_terminal_layout_resize
521
+ end
522
+ @terminal_layout ||= {terminal: terminal}
351
523
  @window&.request_frame
352
524
  terminal
353
525
  end
@@ -376,14 +548,26 @@ module Canopus
376
548
  current = @terminals.delete_at(index)
377
549
  return unless current
378
550
  @terminal_names.delete(current)
551
+ @terminal_profiles.delete(current)
379
552
  @terminal_sizes&.delete(current)
553
+ @terminal_dimensions_by_terminal&.delete(current)
380
554
  @terminal_resize_times&.delete(current)
381
555
  current.close if current.respond_to?(:close)
556
+ visible = terminal_layout_include?(current)
557
+ @terminal_layout = remove_terminal_leaf(@terminal_layout, current) if visible
382
558
  @active_terminal_index = if current.equal?(active)
383
559
  [index, @terminals.length - 1].min.clamp(0, @terminals.length)
384
560
  else
385
561
  @terminals.index(active) || 0
386
562
  end
563
+ if @terminals.empty?
564
+ @terminal_layout = nil
565
+ elsif visible
566
+ replacement = terminal_layout_terminals.first || terminal
567
+ @active_terminal_index = @terminals.index(replacement) || @active_terminal_index
568
+ @terminal_layout ||= {terminal: terminal}
569
+ invalidate_terminal_layout_resize
570
+ end
387
571
  self.terminal_visible = false if @terminals.empty? && @settings["terminal"]["hide_when_empty"]
388
572
  @window&.request_frame
389
573
  current
@@ -392,19 +576,30 @@ module Canopus
392
576
  def restart_terminal(index = @active_terminal_index)
393
577
  current = @terminals[index]
394
578
  return unless current
395
- cwd = current.vt.cwd || (current.respond_to?(:initial_cwd) ? current.initial_cwd : @root)
579
+ cwd = terminal_cwd(current)
396
580
  name = @terminal_names[current]
397
- close_terminal(index)
398
- replacement = new_terminal(cwd: File.directory?(cwd) ? cwd : @root)
399
- move_terminal(@terminals.length - 1, index) if index < @terminals.length - 1
581
+ profile = @terminal_profiles[current]
582
+ replacement = start_terminal(cwd: File.directory?(cwd) ? cwd : @root, profile: profile)
583
+ @terminals.delete(replacement)
584
+ @terminals[index] = replacement
585
+ @active_terminal_index = index
586
+ @terminal_layout = replace_terminal_leaf(@terminal_layout, current, replacement)
587
+ invalidate_terminal_layout_resize
588
+ @terminal_names.delete(current)
589
+ @terminal_profiles.delete(current)
590
+ @terminal_sizes&.delete(current)
591
+ @terminal_dimensions_by_terminal&.delete(current)
592
+ @terminal_resize_times&.delete(current)
593
+ current.close if current.respond_to?(:close)
400
594
  rename_terminal(name, replacement) if name
401
595
  replacement
402
596
  end
403
597
 
404
598
  def terminal_title(current = terminal)
405
599
  return "Terminal" unless current
600
+ cwd = current.respond_to?(:cwd) ? current.cwd : current.vt.cwd
406
601
  title = @terminal_names[current] || current.vt.title.to_s.then { |value| value.empty? ? nil : value } ||
407
- current.vt.cwd&.then { |cwd| File.basename(cwd).empty? ? cwd : File.basename(cwd) } ||
602
+ cwd&.then { |path| File.basename(path).empty? ? path : File.basename(path) } ||
408
603
  (current.respond_to?(:foreground_process_name) ? current.foreground_process_name : nil) ||
409
604
  (current.respond_to?(:command_name) ? current.command_name : nil) || "shell"
410
605
  title.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).scrub.slice(0, 200)
@@ -416,6 +611,14 @@ module Canopus
416
611
  value.empty? ? @terminal_names.delete(current) : @terminal_names[current] = value.slice(0, 200)
417
612
  end
418
613
 
614
+ def show_terminal_commands(failed: false)
615
+ items = retained_terminal_commands
616
+ items = items.select { |command| command.exit_status && command.exit_status != 0 } if failed
617
+ self.palette = {kind: :terminal_commands, query: +"", index: 0, items: items,
618
+ matches: items.map { |command| terminal_command_label(command) }}
619
+ update_palette
620
+ end
621
+
419
622
  def drain_terminals(now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
420
623
  return false if @terminals.empty?
421
624
  remaining = @settings["terminal"]["max_bytes_per_frame"]
@@ -436,15 +639,17 @@ module Canopus
436
639
  changed
437
640
  end
438
641
 
439
- def resize_terminal(columns, rows, final: false, now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
642
+ def resize_terminal(columns, rows, final: false, now: Process.clock_gettime(Process::CLOCK_MONOTONIC), terminal: nil)
440
643
  minimum = @settings["terminal"]
441
644
  dimensions = [[columns.to_i, minimum["min_cols"]].max, [rows.to_i, minimum["min_rows"]].max]
442
- @terminal_dimensions = dimensions
645
+ @terminal_dimensions = dimensions if !terminal || terminal.equal?(self.terminal)
443
646
  return if @terminals.empty?
444
647
  final ||= @terminal_resize_final
445
648
  @terminal_resize_final = false
446
649
  wait = minimum["resize_debounce_ms"] / 1000.0
447
- @terminals.each do |current|
650
+ targets = terminal ? [terminal] : @terminals
651
+ targets.each do |current|
652
+ (@terminal_dimensions_by_terminal ||= {})[current] = dimensions
448
653
  previous = (@terminal_resize_times ||= {})[current]
449
654
  next if !final && previous && now - previous < wait
450
655
  next if (@terminal_sizes ||= {})[current] == dimensions
@@ -455,7 +660,13 @@ module Canopus
455
660
  end
456
661
 
457
662
  def flush_terminal_resize
458
- resize_terminal(*@terminal_dimensions, final: true) if @terminal_dimensions
663
+ if @terminal_dimensions_by_terminal&.any?
664
+ @terminal_dimensions_by_terminal.dup.each do |current, dimensions|
665
+ resize_terminal(*dimensions, final: true, terminal: current) if @terminals.include?(current)
666
+ end
667
+ elsif @terminal_dimensions
668
+ resize_terminal(*@terminal_dimensions, final: true)
669
+ end
459
670
  @terminal_resize_final = true
460
671
  end
461
672
 
@@ -484,8 +695,8 @@ module Canopus
484
695
  end
485
696
  end
486
697
  end
487
- def command_context(terminal: false)
488
- {"Terminal" => terminal, "Editor" => !!editor,
698
+ def command_context(terminal: false, task_output: false)
699
+ {"Terminal" => terminal, "TaskOutput" => task_output, "Editor" => !!editor,
489
700
  "vim_mode" => @settings["vim_mode"] && editor ? vim.mode.to_s : false}
490
701
  end
491
702
  def call(name, *args, context: nil)
@@ -514,6 +725,8 @@ module Canopus
514
725
  @palette[:search_options] = {regexp: false, case_sensitive: true, whole_word: false, selection_only: false}
515
726
  @palette[:selection] = editor.primary.range unless editor.primary.empty?
516
727
  @palette[:editor], @palette[:version] = editor, editor.buffer.version
728
+ elsif kind == :regex_matches
729
+ @palette[:editor], @palette[:version] = editor, editor.buffer.version
517
730
  end
518
731
  update_palette
519
732
  end
@@ -538,9 +751,76 @@ module Canopus
538
751
  @palette[:index] = @palette[:index].clamp(0, [@palette[:matches].length - 1, 0].max)
539
752
  return
540
753
  end
541
- if [:completion, :locations, :symbols, :code_actions, :outline, :branches, :settings_keys, :snippet_choices].include?(@palette[:kind])
754
+ if @palette[:kind] == :completion
755
+ labels = @palette[:all_matches] ||= @palette[:matches].dup
756
+ candidates = @palette[:items].map do |item|
757
+ item.is_a?(Provider::Completion) ? item.filter_text || item.label : item["filterText"] || item.fetch("label")
758
+ end
759
+ groups = @palette[:completion_indices] ||= candidates.each_with_index.each_with_object({}) do |(candidate, index), grouped|
760
+ (grouped[candidate] ||= []) << index
761
+ end
762
+ session = @palette[:search] ||= Spica::Index.new(groups.keys, tie_break: :index).session
763
+ session.query = @palette[:query]
764
+ matches = session.matches(12)
765
+ @palette[:indices] = matches.flat_map { |match| groups.fetch(match.candidate) }.first(12)
766
+ @palette[:matches] = @palette[:indices].map { |index| labels.fetch(index) }
767
+ @palette[:index] = 0
768
+ return
769
+ end
770
+ if @palette[:kind] == :workspace_symbol_results
771
+ labels = @palette.fetch(:all_matches)
772
+ if @palette[:query].empty?
773
+ @palette[:indices] = (0...[labels.length, 12].min).to_a
774
+ @palette[:matches] = @palette[:indices].map { |index| labels.fetch(index) }
775
+ else
776
+ session = @palette[:search] ||= @palette.fetch(:workspace_symbol_index).session
777
+ session.query = @palette[:query]
778
+ groups = @palette.fetch(:workspace_symbol_groups)
779
+ @palette[:indices] = session.matches(12).flat_map { |match| groups.fetch(match.candidate) }.first(12)
780
+ @palette[:matches] = @palette[:indices].map { |index| labels.fetch(index) }
781
+ end
782
+ @palette[:index] = 0
783
+ return
784
+ end
785
+ if @palette[:kind] == :git_commit_history
786
+ labels = @palette[:all_matches] ||= @palette[:matches].dup
787
+ groups = @palette[:history_search_groups] ||= @palette.fetch(:search_values).each_with_index.each_with_object({}) do |(value, index), result|
788
+ (result[value] ||= []) << index
789
+ end
790
+ session = @palette[:search] ||= Spica::Index.new(groups.keys, tie_break: :index).session
791
+ session.query = @palette[:query]
792
+ @palette[:indices] = session.matches(12).flat_map { |match| groups.fetch(match.candidate) }.first(12)
793
+ @palette[:matches] = @palette[:indices].map { |index| labels.fetch(index) }
794
+ @palette[:index] = 0
795
+ return
796
+ end
797
+ if @palette[:kind] == :terminal_commands
542
798
  labels = @palette[:all_matches] ||= @palette[:matches].dup
543
- session = @palette[:search] ||= Spica::Index.new(labels).session
799
+ groups = @palette[:terminal_command_groups] ||= labels.each_with_index.each_with_object({}) do |(label, index), result|
800
+ (result[label] ||= []) << index
801
+ end
802
+ session = @palette[:search] ||= Spica::Index.new(groups.keys).session
803
+ session.query = @palette[:query]
804
+ @palette[:indices] = session.matches(12).flat_map { |match| groups.fetch(match.candidate) }.first(12)
805
+ @palette[:matches] = @palette[:indices].map { |index| labels.fetch(index) }
806
+ @palette[:index] = 0
807
+ return
808
+ end
809
+ if @palette[:kind] == :settings_edit
810
+ @palette[:index] = @palette[:index].clamp(0, @palette[:matches].length - 1)
811
+ return
812
+ end
813
+ if @palette[:kind] == :keymap_edit
814
+ @palette[:index] = @palette[:index].clamp(0, @palette[:matches].length - 1)
815
+ return
816
+ end
817
+ if [:locations, :symbols, :code_actions, :outline, :branches, :settings_keys, :settings_gui, :keymap_gui, :keymap_presets, :snippet_choices,
818
+ :breadcrumbs, :hierarchy_roots, :language_servers, :debug_configurations, :debug_watch_remove, :tasks,
819
+ :git_file_history, :git_commit_paths, :git_remotes].include?(@palette[:kind])
820
+ labels = @palette[:all_matches] ||= @palette[:matches].dup
821
+ index = [:language_servers, :git_file_history].include?(@palette[:kind]) ?
822
+ Spica::Index.new(labels, tie_break: :index) : Spica::Index.new(labels)
823
+ session = @palette[:search] ||= index.session
544
824
  session.query = @palette[:query]
545
825
  matches = session.matches(12)
546
826
  @palette[:indices] = matches.map(&:index)
@@ -561,13 +841,51 @@ module Canopus
561
841
  @palette[:index] = @palette[:index].clamp(0, [@palette[:matches].length - 1, 0].max)
562
842
  end
563
843
  def palette_accept
844
+ return accept_task_palette if @palette[:kind] == :tasks
845
+ if @palette[:kind] == :encoding_actions
846
+ current = @palette
847
+ action = current[:encoding_actions][current[:index]]
848
+ self.palette = nil
849
+ return unless action
850
+
851
+ buffer = current[:buffer]
852
+ return unless @buffers.value?(buffer) || editor&.buffer.equal?(buffer)
853
+ if action[1] == :reload
854
+ buffer.reload(force: false, encoding: action[2])
855
+ self.message = "Reloaded as #{action[2].name}"
856
+ else
857
+ save_buffer(buffer, encoding: action[2])
858
+ self.message = "Saved as #{action[2].name}" unless @palette&.fetch(:kind, nil) == :confirm_encoding_save
859
+ end
860
+ return buffer
861
+ end
862
+ if @palette[:kind] == :confirm_line_ending_conversion
863
+ current = @palette
864
+ self.palette = nil
865
+ return if current[:index] != 0
866
+
867
+ buffer = current[:buffer]
868
+ return unless @buffers.value?(buffer) || editor&.buffer.equal?(buffer)
869
+ return convert_line_endings(current[:to], confirmed: true)
870
+ end
871
+ if @palette[:kind] == :confirm_encoding_save
872
+ current = @palette
873
+ self.palette = nil
874
+ return if current[:index] != 0
875
+
876
+ return save_buffer(current[:buffer], path: current[:target], encoding: current[:encoding], confirmed: true)
877
+ end
878
+ return accept_breakpoint_palette if [:breakpoint_actions, :breakpoint_edit].include?(@palette[:kind])
879
+ return accept_debug_watch_palette if [:debug_watch_add, :debug_watch_remove].include?(@palette[:kind])
880
+ return accept_debug_console_palette if @palette[:kind] == :debug_console
881
+ return accept_git_credentials if @palette[:kind] == :git_credentials
564
882
  search_state = @palette.slice(:search_options, :selection, :editor, :version)
565
883
  after_save = @palette[:after_save]
566
884
  if @palette[:kind] == :snippet_choices
567
885
  current = @palette
568
886
  self.palette = nil
569
887
  return @panes.any? { |pane| pane.editors.include?(current[:editor]) } && current[:editor].choose_snippet(current[:matches][current[:index]])
570
- elsif [:completion, :locations, :symbols, :code_actions].include?(@palette[:kind])
888
+ elsif [:completion, :locations, :symbols, :code_actions, :workspace_symbol_results].include?(@palette[:kind])
571
889
  current = @palette
572
890
  self.palette = nil
573
891
  index = current[:indices] ? current[:indices][current[:index]] : current[:index]
@@ -582,6 +900,76 @@ module Canopus
582
900
  editor.reveal_cursor
583
901
  end
584
902
  return
903
+ elsif @palette[:kind] == :breadcrumbs
904
+ current = @palette
905
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
906
+ self.palette = nil
907
+ return accept_breadcrumb_palette(current, index)
908
+ elsif @palette[:kind] == :hierarchy_roots
909
+ current = @palette
910
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
911
+ self.palette = nil
912
+ return accept_hierarchy_root(current, index)
913
+ elsif @palette[:kind] == :language_servers
914
+ current = @palette
915
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
916
+ server = index && current[:items][index]
917
+ self.palette = nil
918
+ return restart_language_server(server[:language], server[:index]) if server
919
+ elsif @palette[:kind] == :debug_configurations
920
+ current = @palette
921
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
922
+ configuration = index && current[:items][index]
923
+ self.palette = nil
924
+ return start_debugging(configuration.fetch("name")) if configuration
925
+ elsif @palette[:kind] == :terminal_commands
926
+ current = @palette
927
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
928
+ self.palette = nil
929
+ return index && current[:items][index]
930
+ elsif @palette[:kind] == :problem_filter
931
+ query = @palette[:query]
932
+ self.palette = nil
933
+ return apply_problem_filter(query)
934
+ elsif @palette[:kind] == :git_file_history
935
+ current = @palette
936
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
937
+ entry = index && current[:items][index]
938
+ self.palette = nil
939
+ return compare_git_revisions(entry.parent, entry.oid, path: entry.path) if entry
940
+ elsif @palette[:kind] == :git_commit_history
941
+ current = @palette
942
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
943
+ entry = index && current[:items][index]
944
+ self.palette = nil
945
+ return open_git_commit(entry) if entry
946
+ elsif @palette[:kind] == :git_commit_paths
947
+ current = @palette
948
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
949
+ path = index && current[:items][index]
950
+ self.palette = nil
951
+ commit = current.fetch(:commit)
952
+ return compare_git_revisions(commit.parents.first, commit.oid, path: path) if path
953
+ elsif @palette[:kind] == :git_remotes
954
+ current = @palette
955
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
956
+ remote = index && current[:items][index]
957
+ self.palette = nil
958
+ return public_send(:"#{current.fetch(:operation)}_git", remote) if remote
959
+ elsif @palette[:kind] == :git_revision_compare
960
+ current = @palette
961
+ revisions = current[:query].split("..", -1)
962
+ self.palette = nil
963
+ raise Error, "Enter revisions as FROM..TO" unless revisions.length == 2 && revisions.all? { |revision| !revision.empty? }
964
+ return compare_git_revisions(revisions.first, revisions.last, path: current[:path])
965
+ elsif @palette[:kind] == :rename && @palette[:rename]
966
+ current = @submitting_rename_palette = @palette
967
+ begin
968
+ self.palette = nil
969
+ ensure
970
+ @submitting_rename_palette = nil
971
+ end
972
+ return rename_prepared(current[:rename], current[:query])
585
973
  end
586
974
  selected = @palette[:matches][@palette[:index]]
587
975
  selected_command = if @palette[:kind] == :commands && selected
@@ -590,6 +978,7 @@ module Canopus
590
978
  end
591
979
  command_context = @palette[:command_context]
592
980
  kind, query, pattern = @palette.values_at(:kind, :query, :pattern)
981
+ current = @palette
593
982
  self.palette = nil
594
983
  if kind == :files && selected
595
984
  open(selected)
@@ -598,8 +987,12 @@ module Canopus
598
987
  elsif kind == :search
599
988
  pattern, options = search_query(query, search_state)
600
989
  matches = editor.search(pattern, **options)
990
+ @minimap.record_search(editor.buffer, editor.buffer.version, matches)
601
991
  editor.select(matches.first.begin, matches.first.end) unless matches.empty?
602
992
  @message = "#{matches.length} matches"
993
+ elsif kind == :regex_matches
994
+ pattern, options = search_query(query, search_state.merge(search_options: {regexp: true}))
995
+ @message = "#{editor.select_matches(pattern, **options).length} matches"
603
996
  elsif kind == :project_search
604
997
  search_project(query, **search_state.fetch(:search_options, {}).reject { |key, _| key == :selection_only })
605
998
  elsif kind == :replace_query
@@ -628,7 +1021,46 @@ module Canopus
628
1021
  elsif kind == :branches && selected
629
1022
  checkout_branch(selected)
630
1023
  elsif kind == :settings_keys && selected
631
- editor.insert_text("#{JSON.generate(selected)}: #{JSON.generate(Settings::DEFAULTS.fetch(selected))}", auto_indent: false)
1024
+ editor.insert_text("#{JSON.generate(selected)}: #{JSON.generate(Settings::DEFAULTS.fetch(selected))}", auto_indent: false, pair: false)
1025
+ elsif kind == :settings_gui && selected
1026
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
1027
+ field = current.fetch(:fields).fetch(index)
1028
+ self.palette = {kind: :settings_edit, query: JSON.generate(field[:value]), index: 0,
1029
+ matches: ["Save", "Reset to default"], field: field, settings_file: current.fetch(:settings_file)}
1030
+ elsif kind == :settings_edit
1031
+ field = current.fetch(:field)
1032
+ value = current[:index] == 1 ? field[:default] : JSON.parse(query)
1033
+ begin
1034
+ @settings.set_file(current.fetch(:settings_file), field.fetch(:path), value)
1035
+ self.palette = nil
1036
+ poll_settings(force: true)
1037
+ @message = "Updated #{field[:path].join(".")}"
1038
+ rescue StandardError => error
1039
+ @message = error.message
1040
+ self.palette = current
1041
+ end
1042
+ elsif kind == :keymap_gui && selected
1043
+ index = current[:indices] ? current[:indices][current[:index]] : current[:index]
1044
+ entry = current.fetch(:entries).fetch(index)
1045
+ self.palette = {kind: :keymap_edit, query: entry[:keys].dup, index: 0, matches: ["Save"],
1046
+ entry: entry, settings_file: current.fetch(:settings_file)}
1047
+ elsif kind == :keymap_edit
1048
+ begin
1049
+ apply_keymap_binding(current.fetch(:entry), query)
1050
+ self.palette = nil
1051
+ @message = "Updated #{current[:entry][:id]} key binding"
1052
+ rescue StandardError => error
1053
+ @message = error.message
1054
+ self.palette = current
1055
+ end
1056
+ elsif kind == :keymap_presets && selected
1057
+ begin
1058
+ load_keymap_preset(selected)
1059
+ self.palette = nil
1060
+ @message = "Loaded #{selected} keymap preset"
1061
+ rescue StandardError => error
1062
+ @message = error.message
1063
+ end
632
1064
  end
633
1065
  end
634
1066
  def search_query(query, state)
@@ -645,7 +1077,7 @@ module Canopus
645
1077
  [pattern, search_options]
646
1078
  end
647
1079
  def connect_server(language, command)
648
- options = normalize_server_options({"command" => command})
1080
+ options = normalize_server_configuration(command)
649
1081
  (@client_lock ||= Mutex.new).synchronize { ensure_language_server(language, options) }
650
1082
  end
651
1083
  def drain
@@ -663,27 +1095,61 @@ module Canopus
663
1095
  nil
664
1096
  end
665
1097
  def close
1098
+ reset_auto_save
666
1099
  @closed = true
1100
+ cancel_workspace_symbol_search
1101
+ cancel_completion_requests
667
1102
  cancel_project_search
1103
+ invalidate_hierarchy
1104
+ invalidate_document_highlights
1105
+ invalidate_folding_ranges
1106
+ invalidate_selection_ranges
1107
+ invalidate_prepare_rename
1108
+ invalidate_document_links
1109
+ invalidate_linked_editing_ranges
1110
+ invalidate_inlay_hints
1111
+ @inlay_hint_requests&.clear
1112
+ invalidate_code_lenses
1113
+ invalidate_sticky_symbols
1114
+ @sticky_symbol_requests&.clear
668
1115
  @palette&.dig(:response)&.fulfill({"applied" => false, "failureReason" => "Workspace closed"})
669
1116
  self.palette = nil
670
1117
  @plugins&.close
671
- @watcher&.close
672
- stop_language_servers
1118
+ failure = nil
1119
+ cleanup = lambda do |&operation|
1120
+ operation.call
1121
+ rescue StandardError => error
1122
+ failure ||= error
1123
+ end
1124
+ cleanup.call { stop_debugging }
1125
+ cleanup.call { @breakpoints.close }
1126
+ cleanup.call { @minimap.close }
1127
+ cleanup.call { @watcher&.close }
1128
+ cleanup.call { close_git }
1129
+ cleanup.call { stop_language_servers }
1130
+ cleanup.call { close_tests }
1131
+ cleanup.call { close_tasks }
673
1132
  terminal_closers = @terminals.filter_map do |current|
674
1133
  Thread.new { current.close } if current.respond_to?(:close)
675
1134
  end
676
- terminal_closers.each(&:join)
1135
+ terminal_closers.each { |thread| cleanup.call { thread.join } }
677
1136
  @terminals.clear
678
- clear_vim_states
679
- @panes.each { |pane| pane.editors.each(&:dispose) }
680
- @buffers.each_value(&:close)
1137
+ cleanup.call { clear_vim_states }
1138
+ @buffers.each_value { |buffer| cleanup.call { save_persistent_undo(buffer) } }
1139
+ @panes.each { |pane| pane.editors.each { |current| cleanup.call { current.dispose } } }
1140
+ @buffers.each_value { |buffer| cleanup.call { buffer.close } }
1141
+ cleanup.call { close_recovery(clean: !@preserve_recovery && !failure) }
1142
+ raise failure if failure
681
1143
  end
682
1144
 
683
1145
  private
684
1146
  def register_actions
685
1147
  register_action("file.new") { new_buffer }
686
1148
  register_action("file.save") { editor.buffer.path || editor.buffer.is_a?(MultiBuffer) ? save_buffer : palette_open(:save_as) }
1149
+ register_action("file.encoding", description: "Encoding and Line Endings") { show_encoding_actions }
1150
+ register_action("file.newline.lf", description: "Convert Line Endings to LF") { convert_line_endings(:lf) }
1151
+ register_action("file.newline.crlf", description: "Convert Line Endings to CRLF") { convert_line_endings(:crlf) }
1152
+ register_action("file.newline.cr", description: "Convert Line Endings to CR") { convert_line_endings(:cr) }
687
1153
  register_action("file.close") { request_close }
688
1154
  register_action("tab.close") { request_close }
689
1155
  register_action("tab.close_others") { request_close(@active_pane.editors.reject { |current| current.equal?(editor) }) }
@@ -693,27 +1159,68 @@ module Canopus
693
1159
  register_action("tab.reopen_closed") { reopen_closed }
694
1160
  register_action("pane.close") { request_close(@active_pane.editors.dup) }
695
1161
  register_action("debug.buffer_refs") { self.message = buffer_refs(editor.buffer).map { |kind, id| "#{kind}:#{id}" }.join(", ") }
1162
+ register_action("debug.start", description: "Start Debugging") { start_debugging }
1163
+ register_action("debug.stop", description: "Stop Debugging") { stop_debugging }
1164
+ register_action("debug.watch.add", description: "Add Debug Watch") { show_debug_watch_add }
1165
+ register_action("debug.watch.remove", description: "Remove Debug Watch") { show_debug_watch_remove }
1166
+ register_action("debug.console.evaluate", description: "Evaluate in Debug Console") { show_debug_console }
1167
+ register_action("task.run", description: "Run Task") { show_tasks }
1168
+ register_action("task.stop", description: "Stop Task", condition: "TaskOutput") { stop_task }
1169
+ register_action("test.refresh", description: "Refresh Tests") { refresh_tests }
696
1170
  register_action("file.find") { palette_open(:files) }
697
1171
  register_action("project.new_file") { project_prompt(:create_file) }
698
1172
  register_action("project.new_folder") { project_prompt(:create_folder) }
699
1173
  register_action("project.rename") { project_prompt(:rename_file) }
700
1174
  register_action("project.trash") { project_prompt(:trash_file) }
701
1175
  register_action("git.diff") { show_git_diff }
1176
+ register_action("git.stage", description: "Stage File") { stage_git_file }
1177
+ register_action("git.unstage", description: "Unstage File") { unstage_git_file }
1178
+ register_action("git.stage_hunk", description: "Stage Hunk") { stage_git_hunk }
1179
+ register_action("git.unstage_hunk", description: "Unstage Hunk") { unstage_git_hunk }
1180
+ register_action("git.stage_line", description: "Stage Line") { stage_git_line }
1181
+ register_action("git.unstage_line", description: "Unstage Line") { unstage_git_line }
1182
+ register_action("git.commit", description: "Commit Staged Changes") { commit_git }
1183
+ register_action("git.diff.toggle_mode", description: "Toggle Inline / Side-by-Side Diff") { toggle_git_diff_mode }
1184
+ register_action("git.compare_revisions", description: "Compare Git Revisions") { show_git_revision_compare }
1185
+ register_action("git.file_history", description: "Show File History") { show_git_file_history }
1186
+ register_action("git.history", description: "Show Commit History") { show_git_commit_history }
1187
+ register_action("git.fetch", description: "Fetch from Remote") { fetch_git }
1188
+ register_action("git.pull", description: "Pull from Remote") { pull_git }
1189
+ register_action("git.push", description: "Push to Remote") { push_git }
1190
+ register_action("git.cancel_transfer", description: "Cancel Git Transfer") { cancel_git_transfer }
1191
+ register_action("git.conflicts", description: "Resolve Merge Conflicts") { show_git_conflicts }
1192
+ register_action("git.conflict.ours", description: "Resolve Conflict with Ours") { resolve_git_conflict(:ours) }
1193
+ register_action("git.conflict.theirs", description: "Resolve Conflict with Theirs") { resolve_git_conflict(:theirs) }
1194
+ register_action("git.conflict.both", description: "Resolve Conflict with Both") { resolve_git_conflict(:both) }
1195
+ register_action("git.conflict.manual", description: "Resolve Conflict with Manual Edit") { resolve_git_conflict(:manual) }
702
1196
  register_action("git.toggle_hunk") { toggle_git_hunk }
703
1197
  register_action("git.blame") { show_git_blame }
704
1198
  register_action("git.revert_hunk") { revert_current_hunk }
705
- register_action("git.branches") { self.palette = {kind: :branches, query: +"", index: 0, matches: git ? git.branches : []} }
1199
+ register_action("git.branches") { self.palette = {kind: :branches, query: +"", index: 0, matches: git_branches} }
706
1200
  register_action("command.palette") { palette_open(:commands) }
707
1201
  register_action("pane.split_right") { split(:horizontal) }
708
1202
  register_action("pane.split_down") { split(:vertical) }
709
1203
  register_action("pane.next") { focus(@panes[(@panes.index(@active_pane) + 1) % @panes.length]) }
710
1204
  register_action("tab.pin") { @active_pane.pin }
711
- register_action("tab.back") { @vim_states[editor]&.deactivate; @active_pane.back }
712
- register_action("tab.forward") { @vim_states[editor]&.deactivate; @active_pane.forward }
1205
+ register_action("tab.back") { @vim_states[editor]&.deactivate; @active_pane.back; invalidate_hidden_selection_ranges }
1206
+ register_action("tab.forward") { @vim_states[editor]&.deactivate; @active_pane.forward; invalidate_hidden_selection_ranges }
713
1207
  register_action("edit.undo") { editor.undo }
714
1208
  register_action("edit.redo") { editor.redo }
715
1209
  register_action("edit.select_all") { editor.select_all }
716
1210
  register_action("edit.select_next") { editor.select_next_occurrence }
1211
+ %i[up down].each do |direction|
1212
+ register_action("edit.add_cursor_#{direction}", description: "Add Cursor #{direction.capitalize}", condition: "Editor && !vim_mode") do
1213
+ editor.add_cursor(direction)
1214
+ end
1215
+ end
1216
+ %i[start end].each do |edge|
1217
+ register_action("edit.add_cursors_to_line_#{edge}s", description: "Add Cursors to Line #{edge.capitalize}s", condition: "Editor && !vim_mode") do
1218
+ editor.add_cursors_to_selected_lines(edge)
1219
+ end
1220
+ end
1221
+ register_action("edit.select_all_regex_matches", description: "Select All Regular Expression Matches", condition: "Editor && !vim_mode") do
1222
+ palette_open(:regex_matches)
1223
+ end
717
1224
  register_action("edit.select_all_occurrences") { editor.select_next_occurrence(all: true) }
718
1225
  register_action("edit.duplicate_line") { editor.duplicate_lines }
719
1226
  %i[up down].each do |direction|
@@ -722,11 +1229,16 @@ module Canopus
722
1229
  end
723
1230
  register_action("edit.toggle_comment") { editor.toggle_comment(prefix: editor.language_document.definition.comment) }
724
1231
  register_action("language.outline") { show_outline }
725
- register_action("language.workspace_symbols") { palette_open(:workspace_symbols) }
1232
+ register_action("language.workspace_symbols", description: "Workspace Symbols") { palette_open(:workspace_symbols) }
726
1233
  %i[completion hover definition typeDefinition implementation references formatting codeAction signatureHelp documentSymbol inlayHint codeLens diagnostic semantic_tokens].each do |kind|
727
1234
  register_action("language.#{kind}") { language_request(kind) }
728
1235
  end
729
- register_action("language.rename") { self.palette = {kind: :rename, query: +"", index: 0, matches: []} }
1236
+ register_action("language.rename") { prepare_rename }
1237
+ register_action("language.linked_editing") { linked_editing_range }
1238
+ register_action("language.call_hierarchy") { show_call_hierarchy }
1239
+ register_action("language.type_hierarchy") { show_type_hierarchy }
1240
+ register_action("language.expand_selection") { expand_selection }
1241
+ register_action("language.shrink_selection") { shrink_selection }
730
1242
  register_action("editor.fold") { fold_current }
731
1243
  register_action("editor.unfold") { editor.display_map.unfold(editor.primary.head) }
732
1244
  register_action("edit.indent") { editor.indent }
@@ -736,11 +1248,26 @@ module Canopus
736
1248
  register_action("search.replace") { palette_open(:replace_query) }
737
1249
  register_action("settings.open") { open_settings }
738
1250
  register_action("settings.complete") { settings_completions }
1251
+ register_action("settings.gui", description: "Settings GUI") { settings_gui }
1252
+ register_action("settings.gui_changed", description: "Changed Settings GUI") { settings_gui(changed_only: true) }
1253
+ register_action("keymap.gui", description: "Keymap GUI") { keymap_gui }
1254
+ register_action("keymap.preset", description: "Keymap Preset") { keymap_presets }
1255
+ register_action("workspace.trust.toggle", description: "Toggle Workspace Trust") { toggle_workspace_trust }
1256
+ register_action("workspace.trust.show", description: "Show Workspace Trust") { show_workspace_trust }
739
1257
  register_action("language.diagnostics") { show_diagnostics }
1258
+ register_action("language.restart_server") { show_language_server_restart }
740
1259
  register_action("view.project") { @panels.toggle("explorer") }
741
1260
  register_action("panel.explorer") { @panels.toggle("explorer") }
1261
+ register_action("panel.scm") { @panels.toggle("scm"); refresh_scm if @panels.visible?("scm") }
742
1262
  register_action("panel.search") { @panels.fetch("search").build.call }
743
1263
  register_action("panel.terminal") { @terminals.empty? ? new_terminal : @panels.toggle("terminal") }
1264
+ register_action("panel.hierarchy") { @panels.toggle("hierarchy") if @hierarchy_state }
1265
+ register_action("panel.problems") { @panels.toggle("problems") }
1266
+ register_action("panel.debug") { @panels.toggle("debug") }
1267
+ register_action("panel.debug_console") { @panels.toggle("debug_console") }
1268
+ register_action("panel.output") { toggle_task_output }
1269
+ register_action("panel.test") { toggle_tests }
1270
+ register_action("problems.filter") { show_problem_filter }
744
1271
  [:left, :right, :bottom].each { |side| register_action("view.dock_#{side}") { toggle_dock(side) } }
745
1272
  register_action("view.wrap") { editor.display_map.wrap_width = editor.display_map.wrap_map.width ? nil : 100 }
746
1273
  register_action("view.theme") { self.theme = @theme.name.include?("Dark") ? "Canopus Light" : "Canopus Dark" }
@@ -753,13 +1280,46 @@ module Canopus
753
1280
  end
754
1281
  register_action("terminal.toggle") { call("view.terminal") }
755
1282
  register_action("terminal.new") { new_terminal }
1283
+ register_action("terminal.split_right", description: "Split Terminal Right", condition: "Terminal") { split_terminal(:horizontal) }
1284
+ register_action("terminal.split_down", description: "Split Terminal Down", condition: "Terminal") { split_terminal(:vertical) }
756
1285
  register_action("terminal.close") { request_terminal_close }
757
1286
  register_action("terminal.restart") { restart_terminal }
758
1287
  register_action("terminal.next") { activate_terminal((@active_terminal_index + 1) % @terminals.length) unless @terminals.empty? }
759
1288
  register_action("terminal.prev") { activate_terminal((@active_terminal_index - 1) % @terminals.length) unless @terminals.empty? }
760
1289
  9.times { |index| register_action("terminal.select_#{index + 1}") { activate_terminal(index) if index < @terminals.length } }
761
1290
  register_action("terminal.rename") { self.palette = {kind: :terminal_rename, query: +"", index: 0, matches: []} if terminal }
762
- register_action("terminal.clear") { terminal&.grid&.reset }
1291
+ register_action("terminal.commands", description: "Show Terminal Commands", condition: "Terminal") { show_terminal_commands }
1292
+ register_action("terminal.commands.failed", description: "Show Failed Terminal Commands", condition: "Terminal") do
1293
+ show_terminal_commands(failed: true)
1294
+ end
1295
+ register_action("terminal.clear") do
1296
+ terminal&.grid&.reset
1297
+ terminal&.clear_commands
1298
+ end
1299
+ end
1300
+
1301
+ def inject_shell_integration(current)
1302
+ shell = current.command_name
1303
+ path = Tarazed::ShellIntegration.path(shell)
1304
+ source = File.basename(shell).downcase.delete_suffix(".exe") == "fish" ? "source" : "."
1305
+ current.input("#{source} #{Shellwords.escape(path)}\n")
1306
+ rescue ArgumentError, IOError, SystemCallError
1307
+ nil
1308
+ end
1309
+
1310
+ def retained_terminal_commands
1311
+ return [] unless terminal&.respond_to?(:commands) && !terminal.grid.alternate?
1312
+ grid = terminal.grid
1313
+ first = grid.scrollback.total - grid.scrollback.length
1314
+ last = grid.scrollback.total + grid.rows - 1
1315
+ terminal.commands.select { |command| command.prompt_row.between?(first, last) }
1316
+ end
1317
+
1318
+ def terminal_command_label(command)
1319
+ input = command.input.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).scrub
1320
+ .gsub(/[\x00-\x1f\x7f]+/, " ").strip
1321
+ input = "(empty command)" if input.empty?
1322
+ "exit #{command.exit_status} #{input}".slice(0, 240)
763
1323
  end
764
1324
  def encode_layout(node)
765
1325
  node[:pane] ? {pane: @panes.index(node[:pane])} : {direction: node[:direction], ratio: node.fetch(:ratio, 0.5), children: node[:children].map { |child| encode_layout(child) }}
@@ -812,7 +1372,11 @@ module Canopus
812
1372
  def release_buffer(buffer, discard: false)
813
1373
  return unless buffer_refs(buffer).empty?
814
1374
  return if buffer.dirty? && !discard
1375
+ @minimap.release(buffer)
1376
+ invalidate_sticky_symbols(buffer)
815
1377
  close_language_documents(buffer)
1378
+ detach_breakpoints(buffer)
1379
+ save_persistent_undo(buffer)
816
1380
  @buffers.delete_if { |_, current| current.equal?(buffer) }
817
1381
  buffer.close
818
1382
  end
@@ -847,6 +1411,52 @@ module Canopus
847
1411
  path
848
1412
  end
849
1413
 
1414
+ def terminal_profile_command(profile)
1415
+ return profile["command"] if profile["command"]
1416
+ [profile["path"], *profile.fetch("args", [])] if profile["path"]
1417
+ end
1418
+
1419
+ def invalidate_terminal_layout_resize
1420
+ @terminal_resize_times&.clear
1421
+ end
1422
+
1423
+ def terminal_cwd(current)
1424
+ cwd = current.respond_to?(:cwd) ? current.cwd : current.vt.cwd
1425
+ cwd || (current.respond_to?(:initial_cwd) ? current.initial_cwd : @root)
1426
+ end
1427
+
1428
+ def terminal_layout_include?(current, node = @terminal_layout)
1429
+ return false unless node
1430
+ return node[:terminal].equal?(current) if node[:terminal]
1431
+ node[:children].any? { |child| terminal_layout_include?(current, child) }
1432
+ end
1433
+
1434
+ def terminal_layout_terminals(node = @terminal_layout)
1435
+ return [] unless node
1436
+ return [node[:terminal]] if node[:terminal]
1437
+ node[:children].flat_map { |child| terminal_layout_terminals(child) }
1438
+ end
1439
+
1440
+ def replace_terminal_leaf(node, current, replacement)
1441
+ replace_terminal_node(node, current, {terminal: replacement})
1442
+ end
1443
+
1444
+ def replace_terminal_node(node, current, replacement)
1445
+ return unless node
1446
+ return replacement if node[:terminal]&.equal?(current)
1447
+ return if node[:terminal]
1448
+ children = node[:children].map { |child| replace_terminal_node(child, current, replacement) || child }
1449
+ node.merge(children: children)
1450
+ end
1451
+
1452
+ def remove_terminal_leaf(node, current)
1453
+ return unless node
1454
+ return if node[:terminal]&.equal?(current)
1455
+ return node if node[:terminal]
1456
+ children = node[:children].filter_map { |child| remove_terminal_leaf(child, current) }
1457
+ children.length == 1 ? children.first : node.merge(children: children)
1458
+ end
1459
+
850
1460
  def reap_exited_terminals
851
1461
  policy = @settings["terminal"]["close_on_exit"]
852
1462
  @terminals.dup.each do |current|
@@ -859,20 +1469,54 @@ module Canopus
859
1469
  end
860
1470
 
861
1471
  require_relative "workspace/session_persistable"
1472
+ require_relative "workspace/recoverable"
862
1473
  require_relative "workspace/project_tree_editable"
863
1474
  require_relative "workspace/file_change_aware"
864
1475
  require_relative "workspace/language_server_configurable"
865
1476
  require_relative "workspace/language_aware"
1477
+ require_relative "workspace/hierarchy_aware"
1478
+ require_relative "workspace/problems_aware"
866
1479
  require_relative "workspace/git_aware"
1480
+ require_relative "workspace/git_staging"
1481
+ require_relative "workspace/git_diff_view"
1482
+ require_relative "workspace/git_conflict_resolution"
1483
+ require_relative "workspace/git_history"
1484
+ require_relative "workspace/git_remote"
1485
+ require_relative "workspace/git_blame"
867
1486
  require_relative "workspace/project_searchable"
1487
+ require_relative "workspace/editor_configurable"
868
1488
  require_relative "workspace/settings_aware"
1489
+ require_relative "workspace/keymap_aware"
1490
+ require_relative "workspace/trust"
1491
+ require_relative "workspace/auto_savable"
1492
+ require_relative "workspace/persistent_undo"
869
1493
  require_relative "workspace/file_previewable"
1494
+ require_relative "workspace/debug_aware"
1495
+ require_relative "workspace/task_aware"
1496
+ require_relative "workspace/test_aware"
870
1497
  Canopus::Workspace.include Canopus::Workspace::SessionPersistable
1498
+ Canopus::Workspace.include Canopus::Workspace::Recoverable
871
1499
  Canopus::Workspace.include Canopus::Workspace::ProjectTreeEditable
872
1500
  Canopus::Workspace.include Canopus::Workspace::FileChangeAware
873
1501
  Canopus::Workspace.include Canopus::Workspace::LanguageServerConfigurable
874
1502
  Canopus::Workspace.include Canopus::Workspace::LanguageAware
1503
+ Canopus::Workspace.include Canopus::Workspace::HierarchyAware
1504
+ Canopus::Workspace.include Canopus::Workspace::ProblemsAware
875
1505
  Canopus::Workspace.include Canopus::Workspace::GitAware
1506
+ Canopus::Workspace.include Canopus::Workspace::GitStaging
1507
+ Canopus::Workspace.include Canopus::Workspace::GitDiffView
1508
+ Canopus::Workspace.include Canopus::Workspace::GitConflictResolution
1509
+ Canopus::Workspace.include Canopus::Workspace::GitHistory
1510
+ Canopus::Workspace.include Canopus::Workspace::GitRemote
1511
+ Canopus::Workspace.include Canopus::Workspace::GitBlame
876
1512
  Canopus::Workspace.include Canopus::Workspace::ProjectSearchable
1513
+ Canopus::Workspace.include Canopus::Workspace::EditorConfigurable
877
1514
  Canopus::Workspace.include Canopus::Workspace::SettingsAware
1515
+ Canopus::Workspace.include Canopus::Workspace::KeymapAware
1516
+ Canopus::Workspace.include Canopus::Workspace::TrustAware
1517
+ Canopus::Workspace.include Canopus::Workspace::AutoSavable
1518
+ Canopus::Workspace.include Canopus::Workspace::PersistentUndo
878
1519
  Canopus::Workspace.include Canopus::Workspace::FilePreviewable
1520
+ Canopus::Workspace.include Canopus::Workspace::DebugAware
1521
+ Canopus::Workspace.include Canopus::Workspace::TaskAware
1522
+ Canopus::Workspace.include Canopus::Workspace::TestAware