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
@@ -8,10 +8,19 @@ module Canopus
8
8
  def initialize(workspace, window)
9
9
  @workspace, @window, @view = workspace, window, Workspace::View.new(workspace)
10
10
  workspace.window = window
11
- workspace.register_action("terminal.copy", condition: "Terminal") { terminal_copy }
12
- workspace.register_action("terminal.paste", condition: "Terminal") do
11
+ workspace.register_action("terminal.copy", condition: "Terminal || TaskOutput") { terminal_copy }
12
+ workspace.register_action("terminal.paste", condition: "Terminal || TaskOutput") do
13
13
  terminal_paste(@window.respond_to?(:clipboard) ? @window.clipboard.to_s : @clipboard)
14
14
  end
15
+ workspace.register_action("terminal.command.previous", description: "Go to Previous Terminal Command", condition: "Terminal") do
16
+ @view.terminal_command(:previous)
17
+ end
18
+ workspace.register_action("terminal.command.next", description: "Go to Next Terminal Command", condition: "Terminal") do
19
+ @view.terminal_command(:next)
20
+ end
21
+ workspace.register_action("terminal.command.toggle_fold", description: "Toggle Terminal Command Output", condition: "Terminal") do
22
+ @view.terminal_toggle_command
23
+ end
15
24
  reload_keymap
16
25
  window.draw { workspace.drain; @view }
17
26
  window.on_input { |event| input(event) }
@@ -19,20 +28,30 @@ module Canopus
19
28
  window.on_appearance { |_| workspace.apply_settings if workspace.settings["theme"] == "auto" } if window.respond_to?(:on_appearance)
20
29
  window.on_tick do
21
30
  workspace.drain_terminals
31
+ workspace.drain_task_outputs
22
32
  workspace.drain
23
33
  workspace.poll_changes
24
34
  workspace.poll_settings
35
+ workspace.poll_auto_save
36
+ workspace.poll_recovery
25
37
  reload_keymap
26
38
  workspace.poll_git_changes
27
39
  poll_display_maps
28
40
  poll_language_documents
29
41
  poll_scroll
30
42
  window.request_frame if @view.tick
43
+ window.close if wait_complete?
31
44
  end
32
- @clipboard, @terminal_focus = "", false
45
+ @clipboard, @terminal_focus = "", nil
33
46
  workspace.new_buffer unless workspace.editor
34
47
  end
48
+ def wait_for(paths)
49
+ @wait_paths = paths.map { |path| @workspace.canonical_path(path) }.uniq
50
+ end
35
51
  def input(event)
52
+ return if focused_ui_widget? &&
53
+ [Zaniah::Input::KeyDown, Zaniah::Input::TextInput, Zaniah::Input::Composition].any? { |type| event.is_a?(type) }
54
+
36
55
  case event
37
56
  when Zaniah::Input::KeyUp, Zaniah::Input::MouseDown, Zaniah::Input::FileDrop
38
57
  @suppress_key_text = false
@@ -55,7 +74,7 @@ module Canopus
55
74
  when Zaniah::Input::TextInput
56
75
  input_text(event.text)
57
76
  when Zaniah::Input::Composition
58
- if @terminal_focus && @workspace.terminal_visible
77
+ if focused_terminal
59
78
  @workspace.terminal_composition = event
60
79
  else
61
80
  @workspace.new_buffer unless @workspace.editor
@@ -67,18 +86,41 @@ module Canopus
67
86
  mouse_down(event)
68
87
  when Zaniah::Input::MouseMove
69
88
  mouse_move(event) if @drag
89
+ if @minimap_drag
90
+ pane, editor = @minimap_drag
91
+ @view.minimap_scroll(editor, event.position) if @workspace.panes.include?(pane) && pane.editors.include?(editor)
92
+ end
70
93
  resize_drag(event.position) if @resize_drag
71
94
  scroll_drag(event.position) if @scroll_drag
72
95
  if @terminal_drag
73
96
  @terminal_drag == :selection ? @view.terminal_select(event.position, extend: true) : terminal_mouse(event, :move)
74
- elsif !(@drag || @resize_drag || @scroll_drag || @drag_file || @drag_tab) &&
75
- @workspace.terminal_visible && @workspace.terminal&.vt&.modes&.[](1003) && @view.hit(event.position)&.first == :terminal
76
- terminal_mouse(event, :move)
97
+ elsif !(@drag || @resize_drag || @scroll_drag || @drag_file || @drag_tab)
98
+ hit = @view.hit(event.position)
99
+ target = hit&.first
100
+ terminal = if target == :terminal
101
+ hit[1] if @workspace.terminal_visible
102
+ elsif target == :terminal_command
103
+ hit[2] if @workspace.terminal_visible
104
+ elsif target == :task_output
105
+ @workspace.task_terminal if @workspace.task_output_visible
106
+ end
107
+ @view.focus_terminal(terminal) if terminal
108
+ terminal_mouse(event, :move, terminal: terminal) if terminal&.vt&.modes&.[](1003)
109
+ end
110
+ unless @drag || @resize_drag || @scroll_drag || @minimap_drag || @drag_file || @drag_tab || @terminal_drag
111
+ target = @view.hit(event.position)
112
+ if target&.first == :editor
113
+ current = target.last
114
+ @workspace.debug_hover(current, @view.offset_at(current, event.position))
115
+ else
116
+ @workspace.debug_hover(nil, nil)
117
+ end
77
118
  end
78
119
  when Zaniah::Input::MouseUp
79
- @workspace.flush_terminal_resize if @resize_drag&.first == :dock_resize
120
+ @workspace.flush_terminal_resize if @resize_drag
80
121
  @resize_drag = nil
81
122
  @scroll_drag = nil
123
+ @minimap_drag = nil
82
124
  if @drag_file
83
125
  path, origin = @drag_file
84
126
  target = @view.hit(event.position)
@@ -122,12 +164,13 @@ module Canopus
122
164
  end
123
165
  def input_text(text)
124
166
  if @workspace.palette
125
- return if [:workspace_edit, :confirm_close, :confirm_tab_close, :confirm_terminal_close, :confirm_terminal_paste, :trash_file].include?(@workspace.palette[:kind])
167
+ return if [:workspace_edit, :confirm_close, :confirm_tab_close, :confirm_terminal_close, :confirm_terminal_paste, :trash_file, :recovery,
168
+ :breakpoint_actions].include?(@workspace.palette[:kind])
126
169
  @workspace.palette[:query] << text
127
170
  @workspace.update_palette
128
- elsif @terminal_focus && @workspace.terminal_visible
171
+ elsif (terminal = focused_terminal)
129
172
  @workspace.terminal_composition = nil
130
- @workspace.terminal.write(text)
173
+ terminal.write(text)
131
174
  else
132
175
  @workspace.new_buffer unless @workspace.editor
133
176
  if @workspace.settings["vim_mode"]
@@ -144,15 +187,16 @@ module Canopus
144
187
  stroke = Zaniah::Input::Keystroke.normalize(stroke)
145
188
  return palette_key(stroke) if @workspace.palette
146
189
  return if @composing && (stroke.split("-") & %w[ctrl cmd]).empty?
147
- if stroke == "cmd-q" || stroke == "ctrl-q" && !(@terminal_focus && @workspace.terminal_visible)
190
+ if stroke == "cmd-q" || stroke == "ctrl-q" && !focused_terminal
148
191
  return @window.close
149
192
  end
150
- if @terminal_focus && @workspace.terminal_visible && @workspace.terminal
151
- if stroke == "enter" && @workspace.terminal.respond_to?(:alive?) && !@workspace.terminal.alive?
193
+ if (terminal = focused_terminal)
194
+ if terminal_focused? && stroke == "enter" && terminal.respond_to?(:alive?) && !terminal.alive?
152
195
  return @workspace.restart_terminal
153
196
  end
154
197
  mode = @workspace.settings["vim_mode"] && @workspace.editor ? @workspace.vim.mode.to_s : false
155
- context = @workspace.command_context(terminal: true).merge("Editor" => false, "vim_mode" => mode)
198
+ context = @workspace.command_context(terminal: terminal_focused?,
199
+ task_output: @terminal_focus == :task_output).merge("Editor" => false, "vim_mode" => mode)
156
200
  action = @keymap.dispatch(stroke, context: context)
157
201
  if action && action != :pending
158
202
  @drag_terminal_tab = nil if action == "terminal.close"
@@ -166,7 +210,7 @@ module Canopus
166
210
  name = parts.pop
167
211
  return if (name.length == 1 || name == "space") && (parts & %w[ctrl alt cmd]).empty?
168
212
  name = {"esc" => "escape", "pageup" => "page_up", "pagedown" => "page_down", "space" => " "}.fetch(name, name)
169
- return @workspace.terminal.key(name, control: parts.include?("ctrl"), alt: parts.include?("alt"), shift: parts.include?("shift"))
213
+ return terminal.key(name, control: parts.include?("ctrl"), alt: parts.include?("alt"), shift: parts.include?("shift"))
170
214
  end
171
215
  return @workspace.settings_completions if stroke == "ctrl-space" && @workspace.editor && @workspace.settings_document?(@workspace.editor.buffer)
172
216
  mode = @workspace.settings["vim_mode"] && @workspace.editor ? @workspace.vim.mode.to_s : false
@@ -225,20 +269,37 @@ module Canopus
225
269
  candidates = visible + (inactive.empty? ? [] : [inactive[@language_poll_index % inactive.length]])
226
270
  candidates.each do |editor|
227
271
  document = editor.language_document
272
+ sticky = false
228
273
  if visible.include?(editor)
274
+ @workspace.request_document_highlights(editor)
275
+ @workspace.request_document_links(editor)
229
276
  map = editor.display_map
230
277
  first = editor.scroll_y.floor.clamp(0, map.row_count - 1)
231
278
  last = [first + editor.viewport_rows, first + 255, map.row_count - 1].min
279
+ sticky = @workspace.sticky_scroll_enabled?(editor)
280
+ symbols = sticky || @workspace.breadcrumbs_enabled?(editor)
232
281
  cached = @language_viewports[editor]
233
282
  # The persistent display tree changes on edits, folds, blocks and
234
283
  # completed wrap batches. Idle polls need not walk every visible row.
235
284
  unless cached && cached[0].equal?(document) && cached[1].equal?(map.tree) &&
236
- cached[2] == editor.buffer.version && cached[3] == first && cached[4] == last
237
- document.request(rows: (first..last).map { |row| map.source_row(row) }.uniq)
238
- @language_viewports[editor] = [document, map.tree, editor.buffer.version, first, last]
285
+ cached[2] == editor.buffer.version && cached[3] == first && cached[4] == last && cached[7] == symbols
286
+ rows = (first..last).map { |row| map.source_row(row) }.uniq.sort
287
+ document.request(rows: rows, syntax: symbols)
288
+ inlay_ranges = @workspace.visible_inlay_hint_ranges(editor, first...(last + 1))
289
+ lens_ranges = @workspace.visible_code_lens_ranges(editor, first...(last + 1))
290
+ cached = @language_viewports[editor] = [document, map.tree, editor.buffer.version, first, last, inlay_ranges, lens_ranges, symbols]
291
+ end
292
+ cached[5].each do |rows|
293
+ @workspace.request_inlay_hints(editor, rows, start: true)
239
294
  end
295
+ cached[6].each do |rows|
296
+ @workspace.request_code_lenses(editor, rows, start: true)
297
+ end
298
+ @workspace.request_sticky_symbols(editor) if symbols
240
299
  end
241
300
  changed = document.poll
301
+ @workspace.refresh_sticky_fallback(editor, document) if visible.include?(editor) && symbols && document.syntax_ready?
302
+ @workspace.invalidate_brackets(editor.buffer) if changed
242
303
  @workspace.language_ready(editor, document)
243
304
  @window.request_frame if changed
244
305
  rescue StandardError => error
@@ -300,6 +361,17 @@ module Canopus
300
361
  end
301
362
 
302
363
  private
364
+ def wait_complete?
365
+ return false unless @wait_paths
366
+ return false if @wait_paths.any? do |path|
367
+ @workspace.panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.path == path } }
368
+ end
369
+ @wait_paths = nil
370
+ true
371
+ end
372
+
373
+ def focused_ui_widget? = !@window.dispatcher.focused.nil?
374
+
303
375
  def reload_keymap
304
376
  language = @workspace.editor&.language_document&.definition&.name
305
377
  groups = @workspace.settings["languages"].dig(language, "keymap") || @workspace.settings["keymap"]
@@ -324,7 +396,7 @@ module Canopus
324
396
  end
325
397
  def palette_input(event)
326
398
  return false unless @workspace.palette
327
- @drag = @resize_drag = @scroll_drag = @terminal_drag = @drag_file = @drag_tab = @drag_terminal_tab = nil
399
+ @drag = @resize_drag = @scroll_drag = @minimap_drag = @terminal_drag = @drag_file = @drag_tab = @drag_terminal_tab = nil
328
400
  case event
329
401
  when Zaniah::Input::KeyDown then key(event.keystroke)
330
402
  when Zaniah::Input::TextInput then input_text(event.text)
@@ -337,7 +409,7 @@ module Canopus
337
409
  editor = @workspace.editor
338
410
  if operation == "v"
339
411
  value = @window.respond_to?(:clipboard) ? @window.clipboard : @clipboard
340
- editor.insert_text(value.to_s, auto_indent: false)
412
+ editor.insert_text(value.to_s, auto_indent: false, pair: false)
341
413
  else
342
414
  value = editor.selections.map { |selection| editor.buffer.rope.byteslice(selection.range).to_s }.join("\n")
343
415
  @clipboard = value
@@ -370,7 +442,8 @@ module Canopus
370
442
  palette[:details_scroll] = (palette.fetch(:details_scroll, 0) + (stroke == "pageup" ? -step : step)).clamp(0, maximum)
371
443
  end
372
444
  when "backspace"
373
- return if [:workspace_edit, :confirm_close, :confirm_tab_close, :confirm_terminal_close, :confirm_terminal_paste, :trash_file].include?(palette[:kind])
445
+ return if [:workspace_edit, :confirm_close, :confirm_tab_close, :confirm_terminal_close, :confirm_terminal_paste, :trash_file, :recovery,
446
+ :breakpoint_actions].include?(palette[:kind])
374
447
  palette[:query] = palette[:query].grapheme_clusters[0...-1].join
375
448
  @workspace.update_palette
376
449
  when "enter"
@@ -391,6 +464,8 @@ module Canopus
391
464
  elsif result["failureReason"] != "User cancelled"
392
465
  @workspace.message = result["failureReason"].to_s
393
466
  end
467
+ elsif palette[:kind] == :recovery
468
+ @workspace.resolve_recovery(palette[:index].zero? ? :restore : :discard)
394
469
  elsif palette[:kind] == :confirm_close
395
470
  case palette[:index]
396
471
  when 0
@@ -410,11 +485,15 @@ module Canopus
410
485
  @workspace.close_terminal(index) if palette[:index].zero? && index
411
486
  elsif palette[:kind] == :confirm_terminal_paste
412
487
  value = palette[:text]
488
+ terminal = palette[:terminal]
413
489
  @workspace.palette = nil
414
- @workspace.terminal&.paste(value) if palette[:index].zero?
490
+ terminal&.paste(value) if palette[:index].zero?
415
491
  elsif palette[:kind] == :terminal_rename
416
492
  @workspace.rename_terminal(palette[:query])
417
493
  @workspace.palette = nil
494
+ elsif palette[:kind] == :terminal_commands
495
+ command = @workspace.palette_accept
496
+ @workspace.message = "Command output is no longer in scrollback" if command && !@view.terminal_command_jump(command)
418
497
  else
419
498
  @workspace.palette_accept
420
499
  end
@@ -423,6 +502,7 @@ module Canopus
423
502
  def mouse_down(event)
424
503
  action = @view.hit(event.position)
425
504
  return unless action
505
+ @window.dispatcher.focus(nil)
426
506
  kind, *args = action
427
507
  return if @workspace.palette && (kind != :palette || event.button != :left)
428
508
  if [:file, :directory].include?(kind)
@@ -434,16 +514,34 @@ module Canopus
434
514
  return
435
515
  end
436
516
  end
437
- @terminal_focus = [:terminal, :terminal_tab, :terminal_close].include?(kind)
517
+ @terminal_focus = if [:terminal, :terminal_command, :terminal_tab, :terminal_close].include?(kind)
518
+ :terminal
519
+ elsif [:task_output, :task_output_tab, :task_output_close].include?(kind)
520
+ :task_output
521
+ end
522
+ if kind == :terminal || kind == :terminal_command
523
+ current = kind == :terminal ? args.first : args[1]
524
+ if current && (index = @workspace.terminals.index(current))
525
+ @workspace.activate_terminal(index)
526
+ @view.focus_terminal(current)
527
+ end
528
+ elsif kind == :task_output && args.first
529
+ @view.focus_terminal(args.first)
530
+ end
438
531
  case kind
439
- when :decoration
440
- args[0].call(args[1], args[2]) if event.button == :left
532
+ when :decoration, :context_decoration
533
+ if event.button == :left
534
+ args[0].call(args[1], args[2])
535
+ elsif event.button == :right && args[0].respond_to?(:right_click)
536
+ args[0].right_click(args[1], args[2])
537
+ end
441
538
  when :git_hunk then @workspace.toggle_git_hunk(args[0], row: args[1]) if event.button == :left
442
539
  when :hover_link
443
540
  return unless event.button == :left
444
541
  uri = URI.parse(args.first)
445
542
  @window.open_url(uri.to_s) if %w[http https].include?(uri.scheme) && uri.host && @window.respond_to?(:open_url)
446
543
  when :dismiss_notification then @workspace.dismiss_notification(args.first)
544
+ when :buffer_format then @workspace.show_encoding_actions(args.first) if event.button == :left
447
545
  when :pane then @workspace.focus(args.first)
448
546
  when :split_resize, :dock_resize then @resize_drag = action
449
547
  when :scrollbar
@@ -454,9 +552,15 @@ module Canopus
454
552
  grab = coordinate.between?(start, start + length) ? coordinate - start : length / 2.0
455
553
  @scroll_drag = [editor, direction, track, thumb, maximum, grab]
456
554
  scroll_drag(event.position)
457
- when :terminal
555
+ when :minimap
556
+ pane, editor = args
557
+ if event.button == :left && pane.active.equal?(editor) && @view.minimap_scroll(editor, event.position)
558
+ @workspace.focus(pane)
559
+ @minimap_drag = [pane, editor]
560
+ end
561
+ when :terminal, :task_output
458
562
  return if event.button == :left && @view.terminal_open_link(event.position, modifiers: event.modifiers)
459
- tracking = [1000, 1002, 1003].any? { |mode| @workspace.terminal.vt.modes[mode] }
563
+ tracking = [1000, 1002, 1003].any? { |mode| focused_terminal.vt.modes[mode] }
460
564
  if tracking && !event.modifiers.map(&:to_s).include?("shift")
461
565
  @terminal_drag = event.button
462
566
  terminal_mouse(event, :press)
@@ -464,11 +568,20 @@ module Canopus
464
568
  @terminal_drag = :selection
465
569
  @view.terminal_select(event.position)
466
570
  end
571
+ when :terminal_command
572
+ @view.terminal_toggle_command(args.first) if event.button == :left
467
573
  when :terminal_tab
468
574
  @workspace.activate_terminal(args.first)
469
575
  @drag_terminal_tab = [args.first, event.position] if event.button == :left
470
576
  when :terminal_close
471
577
  @workspace.request_terminal_close(args.first) if event.button == :left
578
+ when :task_output_tab
579
+ @workspace.activate_task_output(args.first)
580
+ when :task_output_close
581
+ output = @workspace.task_outputs[args.first]
582
+ if event.button == :left && output
583
+ @workspace.task_runner.running?(output) ? @workspace.stop_task(output) : @workspace.close_task_output(args.first)
584
+ end
472
585
  when :file then @workspace.open(args.first)
473
586
  when :directory then @workspace.project_tree.toggle(args.first)
474
587
  when :tab
@@ -495,7 +608,19 @@ module Canopus
495
608
  editor.move(:line_start)
496
609
  editor.move(:down, extend: true)
497
610
  end
498
- @drag = [editor, editor.primary.anchor, event.position, modifiers.include?("alt") && modifiers.include?("shift")]
611
+ rectangle = modifiers.include?("alt") && event.click_count == 1
612
+ @drag = [editor, rectangle ? anchor : editor.primary.anchor, event.position, rectangle]
613
+ when :sticky
614
+ pane, editor, offset, version, generation = args
615
+ if event.button == :left && editor.buffer.version == version &&
616
+ @workspace.document_symbol_generation(editor) == generation && pane.editors.include?(editor)
617
+ @workspace.activate_tab(pane, editor)
618
+ editor.select(offset)
619
+ editor.reveal_cursor
620
+ end
621
+ when :breadcrumb
622
+ pane, editor, item, context = args
623
+ @workspace.show_breadcrumb_menu(pane, editor, item, context) if event.button == :left
499
624
  when :palette
500
625
  @workspace.palette[:index] = args.first
501
626
  palette_key("enter")
@@ -510,8 +635,13 @@ module Canopus
510
635
  cancel_scroll
511
636
  raise ArgumentError, "scroll deltas must be finite" unless [event.delta.x, event.delta.y].all? { |value| value.is_a?(Numeric) && value.finite? }
512
637
  action = @view.hit(event.position)
513
- if [:editor, :scrollbar].include?(action&.first)
514
- editor = action.first == :editor ? action.last : action[1]
638
+ if [:editor, :scrollbar, :sticky, :breadcrumb, :minimap].include?(action&.first)
639
+ editor = case action.first
640
+ when :editor then action.last
641
+ when :sticky, :breadcrumb then action[2]
642
+ when :minimap then action[2]
643
+ else action[1]
644
+ end
515
645
  editor.scroll(dx: event.delta.x, dy: event.delta.y / 20.0)
516
646
  # Cocoa already supplies native momentum deltas (numeric NSEvent phase).
517
647
  # Other backends get a bounded decaying tail; zero friction disables it.
@@ -520,10 +650,14 @@ module Canopus
520
650
  end
521
651
  elsif [:file, :directory].include?(action&.first)
522
652
  @view.project_scroll(event.delta.y / 24.0)
523
- elsif action&.first == :terminal
524
- if [1000, 1002, 1003].any? { |mode| @workspace.terminal.vt.modes[mode] }
525
- column, row = @view.terminal_point(event.position)
526
- @workspace.terminal.mouse(button: event.delta.y.positive? ? :wheel_down : :wheel_up, column: column, row: row)
653
+ elsif [:terminal, :terminal_command, :task_output].include?(action&.first)
654
+ terminal = action.first == :terminal_command ? action[2] : action[1]
655
+ terminal ||= action.first == :task_output ? @workspace.task_terminal : @workspace.terminal
656
+ @view.focus_terminal(terminal)
657
+ if [1000, 1002, 1003].any? { |mode| terminal.vt.modes[mode] }
658
+ if (position = @view.terminal_point(event.position))
659
+ terminal.mouse(button: event.delta.y.positive? ? :wheel_down : :wheel_up, column: position[0], row: position[1])
660
+ end
527
661
  else
528
662
  @view.terminal_scroll(event.delta.y / 20.0)
529
663
  end
@@ -539,25 +673,34 @@ module Canopus
539
673
  editor.scroll(dx: fraction.clamp(0, 1) * maximum - editor.scroll_x)
540
674
  end
541
675
  end
542
- def terminal_mouse(event, action)
543
- column, row = @view.terminal_point(event.position)
676
+ def terminal_mouse(event, action, terminal: focused_terminal)
677
+ return unless terminal
678
+ position = @view.terminal_point(event.position)
679
+ return unless position
680
+ column, row = position
544
681
  modifiers = event.modifiers.map(&:to_s)
545
- @workspace.terminal.mouse(button: @terminal_drag, column: column, row: row, action: action,
682
+ terminal.mouse(button: @terminal_drag, column: column, row: row, action: action,
546
683
  shift: modifiers.include?("shift"), alt: modifiers.include?("alt"), control: modifiers.include?("ctrl"))
547
684
  end
548
685
 
549
686
  def terminal_paste(text)
550
687
  if @workspace.settings["terminal"]["confirm_multiline_paste"] && text.match?(/[\r\n].*[\r\n]?/m)
551
688
  @workspace.palette = {kind: :confirm_terminal_paste, query: "Paste multiple lines into the terminal?", index: 1,
552
- matches: ["Paste", "Cancel"], text: text}
689
+ matches: ["Paste", "Cancel"], text: text, terminal: focused_terminal}
553
690
  else
554
- @workspace.terminal.paste(text)
691
+ focused_terminal&.paste(text)
555
692
  end
556
693
  end
557
694
  def terminal_copy
558
695
  @clipboard = @view.terminal_selected_text
559
696
  @window.clipboard = @clipboard if @window.respond_to?(:clipboard=)
560
697
  end
698
+ def focused_terminal
699
+ return @workspace.terminal if terminal_focused? && @workspace.terminal_visible
700
+ return @workspace.task_terminal if @terminal_focus == :task_output && @workspace.task_output_visible
701
+ nil
702
+ end
703
+ def terminal_focused? = @terminal_focus == :terminal || @terminal_focus == true
561
704
  def resize_drag(point)
562
705
  kind, target, bounds = @resize_drag
563
706
  if kind == :split_resize