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
@@ -18,6 +18,13 @@ module Canopus
18
18
  raise Error, "Open a settings document first" unless settings_document?(editor.buffer)
19
19
  self.palette = {kind: :settings_keys, query: +"", index: 0, matches: Settings::DEFAULTS.keys}
20
20
  end
21
+ def settings_browse(changed_only: false)
22
+ fields = settings_gui_fields(changed_only: changed_only)
23
+ labels = fields.map { |field| "#{field[:path].join(".")} = #{JSON.generate(field[:value])}" }
24
+ self.palette = {kind: :settings_gui, query: +"", index: 0, matches: labels, fields: fields,
25
+ settings_file: settings_path, changed_only: changed_only}
26
+ end
27
+ def settings_gui(changed_only: false) = settings_browse(changed_only: changed_only)
21
28
  def settings_document?(buffer)
22
29
  buffer.path && (buffer.path == settings_path || @settings.paths.any? { |path| File.expand_path(path) == buffer.path })
23
30
  end
@@ -31,6 +38,9 @@ module Canopus
31
38
  system
32
39
  end
33
40
  def apply_settings
41
+ reset_auto_save
42
+ cancel_workspace_symbol_search
43
+ self.palette = nil if @palette&.dig(:kind) == :workspace_symbol_results
34
44
  servers = language_server_settings_plan
35
45
  clear_vim_states unless @settings["vim_mode"]
36
46
  name = @settings["theme"]
@@ -53,6 +63,18 @@ module Canopus
53
63
  end
54
64
  @applied_font_family = @settings["font_family"]
55
65
  @applied_icon_theme = @settings["icon_theme"]
66
+ invalidate_diagnostics
67
+ invalidate_document_highlights
68
+ invalidate_document_links
69
+ invalidate_folding_ranges
70
+ invalidate_selection_ranges
71
+ invalidate_hierarchy
72
+ invalidate_prepare_rename
73
+ invalidate_linked_editing_ranges
74
+ invalidate_inlay_hints
75
+ invalidate_code_lenses
76
+ invalidate_brackets
77
+ @decorations.invalidate(:blame)
56
78
  reload_language_servers(servers)
57
79
  @window&.request_frame
58
80
  end
@@ -70,20 +92,72 @@ module Canopus
70
92
  initial = !defined?(@settings_state)
71
93
  @settings_state = state
72
94
  return if initial && !force
95
+ previous_paths = @settings.paths
73
96
  replacement = @settings.reload
74
97
  old, @settings = @settings, replacement
98
+ publish_settings_diagnostics(previous_paths | @settings.paths)
75
99
  apply_settings
76
- @message = "Settings reloaded"
100
+ errors = @settings.paths.filter_map { |path| Settings.file_diagnostics(path)&.last }.flatten
101
+ @message = errors.empty? ? "Settings reloaded" : "Settings unchanged: #{errors.first.message}"
77
102
  rescue StandardError => error
78
103
  @settings = old if old
104
+ publish_settings_diagnostics(@settings.paths) if @settings
79
105
  @message = "Settings unchanged: #{error.message}"
80
106
  end
81
107
 
82
108
  private
109
+
110
+ def settings_gui_fields(changed_only: false)
111
+ defaults = Settings::SCHEMA_MODEL.defaults
112
+ Settings::SCHEMA_MODEL.fields.filter_map do |field|
113
+ path = field.path
114
+ next unless path.all? { |part| part.is_a?(String) && part != "*" }
115
+
116
+ value = path.reduce(@settings.values) { |current, key| current.is_a?(Hash) ? current[key] : nil }
117
+ default = path.reduce(defaults) { |current, key| current.is_a?(Hash) ? current[key] : nil }
118
+ next if changed_only && value == default
119
+
120
+ {path: path, value: value, default: default, description: field.description}.freeze
121
+ end
122
+ end
123
+
124
+ def publish_settings_diagnostics(paths)
125
+ paths.uniq.each do |path|
126
+ uri = Sadr::Protocol.uri(File.expand_path(path))
127
+ @diagnostics.publish(:settings, uri, settings_diagnostics(path))
128
+ rescue StandardError
129
+ @diagnostics.publish(:settings, uri, [], notify: false) rescue nil
130
+ end
131
+ end
132
+
133
+ def settings_diagnostics(path)
134
+ parsed = Settings.file_diagnostics(path)
135
+ return [] unless parsed
136
+
137
+ document, diagnostics = parsed
138
+ diagnostics.filter_map do |diagnostic|
139
+ range = diagnostic.range
140
+ start_position = document.utf16_position_at(range&.begin || 0)
141
+ end_position = document.utf16_position_at(range&.end || range&.begin || 0)
142
+ {
143
+ "range" => {
144
+ "start" => {"line" => start_position[0], "character" => start_position[1]},
145
+ "end" => {"line" => end_position[0], "character" => end_position[1]}
146
+ },
147
+ "severity" => Diagnostics::SEVERITIES.fetch(diagnostic.severity, 2),
148
+ "message" => diagnostic.message.to_s,
149
+ "source" => "settings"
150
+ }
151
+ rescue EncodingError, RangeError
152
+ nil
153
+ end
154
+ end
155
+
83
156
  def apply_editor_settings(current)
84
- values = @settings.for_language(current.language_document.definition.name)
157
+ values = settings_for_editor(current)
85
158
  current.tab_size = values["tab_size"]
86
159
  current.use_tabs = values["use_tabs"]
160
+ current.closing_pairs = values["auto_pairs"].to_h.freeze
87
161
  current.display_map.tab_size = current.tab_size
88
162
  current.display_map.wrap_width = values["soft_wrap"] ? 100 : nil unless current.buffer.read_only
89
163
  end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ class Workspace
5
+ module TaskAware
6
+ attr_reader :task_runner
7
+
8
+ def initialize_tasks
9
+ options = @settings["terminal"]
10
+ @task_runner = Task::Runner.new(scrollback: options["scrollback_lines"],
11
+ queue_limit_bytes: options["queue_limit_bytes"], report: ->(error) { post { notify("Cannot close task: #{error.message}") } })
12
+ @task_matchers, @task_finished, @task_diagnostic_snapshot = {}, {}, {}.freeze
13
+ end
14
+
15
+ def task_outputs = @task_runner.entries
16
+ def task_output = @task_runner.active
17
+ def task_terminal = @task_runner.terminal
18
+ def task_output_visible = @panels.visible?("output") && @docks[:bottom][:visible]
19
+
20
+ def show_tasks
21
+ configuration = Task::Configuration.new(root: @root)
22
+ if configuration.tasks.empty?
23
+ self.message = "No tasks configured in .canopus/tasks.jsonc"
24
+ return false
25
+ end
26
+ self.palette = {kind: :tasks, query: +"", index: 0,
27
+ matches: configuration.tasks.map { |task| task.fetch("label") },
28
+ items: configuration.tasks, task_configuration: configuration}
29
+ update_palette
30
+ end
31
+
32
+ def run_task(task)
33
+ configuration = Task::Configuration.new(root: @root)
34
+ source = task.is_a?(String) ? task : configuration.tasks.find { |candidate| candidate == task }
35
+ resolved = configuration.resolve(source, **task_context)
36
+ matcher = resolved["problem_matcher"] && configuration.problem_matcher(resolved["problem_matcher"])
37
+ raise Error, "unknown problem matcher: #{resolved['problem_matcher']}" if resolved["problem_matcher"] && !matcher
38
+ start_task(resolved, matcher)
39
+ end
40
+
41
+ def accept_task_palette
42
+ state = @palette
43
+ index = state[:indices] ? state[:indices][state[:index]] : state[:index]
44
+ source = index && state[:items][index]
45
+ configuration = state[:task_configuration]
46
+ self.palette = nil
47
+ return unless source && configuration
48
+
49
+ resolved = configuration.resolve(source, **task_context)
50
+ matcher = resolved["problem_matcher"] && configuration.problem_matcher(resolved["problem_matcher"])
51
+ raise Error, "unknown problem matcher: #{resolved['problem_matcher']}" if resolved["problem_matcher"] && !matcher
52
+ start_task(resolved, matcher)
53
+ end
54
+
55
+ def show_task_output(output = task_output)
56
+ if output && (index = task_outputs.index { |entry| entry.equal?(output) })
57
+ @task_runner.activate(index)
58
+ end
59
+ @panels.hide("terminal") if @panels.visible?("terminal")
60
+ @panels.hide("debug_console") if @panels.visible?("debug_console")
61
+ @panels.show("output")
62
+ @window&.request_frame
63
+ output
64
+ end
65
+
66
+ def toggle_task_output
67
+ task_output_visible ? @panels.hide("output") : show_task_output
68
+ end
69
+
70
+ def activate_task_output(index)
71
+ output = @task_runner.activate(index)
72
+ @window&.request_frame
73
+ output
74
+ end
75
+
76
+ def close_task_output(index = @task_runner.active_index)
77
+ output = @task_runner.remove(index)
78
+ forget_task_output(output) if output
79
+ @panels.hide("output") if task_outputs.empty?
80
+ @window&.request_frame
81
+ output
82
+ end
83
+
84
+ def stop_task(output = task_output)
85
+ stopped = @task_runner.stop(output)
86
+ cancel_test_output(output) if stopped
87
+ self.message = "No running task selected" unless stopped
88
+ @window&.request_frame
89
+ stopped
90
+ end
91
+
92
+ def drain_task_outputs
93
+ changed = @task_runner.drain(max_bytes: @settings["terminal"]["max_bytes_per_frame"]) do |output, data, seconds|
94
+ capture_test_output(output, data)
95
+ matcher = @task_matchers[output.id]
96
+ matcher&.feed(data.to_s, max_seconds: seconds)
97
+ !matcher || !matcher.pending?
98
+ end
99
+ @task_runner.completed.each do |output|
100
+ unless @task_finished[output.id]
101
+ @task_finished[output.id] = true
102
+ @task_matchers[output.id]&.finish
103
+ complete_test_output(output)
104
+ end
105
+ next unless output.presentation.fetch("reveal") == "silent"
106
+ status = output.terminal.status if output.terminal.respond_to?(:status)
107
+ show_task_output(output) if status && !status.success?
108
+ end
109
+ @window&.request_frame if changed || @task_runner.pending?
110
+ changed
111
+ end
112
+
113
+ def resize_task_output(columns, rows) = @task_runner.resize(columns, rows)
114
+
115
+ def task_output_title(output)
116
+ suffix = @task_runner.running?(output) ? " ●" : ""
117
+ "#{output.label}#{suffix}"
118
+ end
119
+
120
+ def close_tasks
121
+ @task_runner.close
122
+ ensure
123
+ @task_matchers.clear
124
+ @task_finished.clear
125
+ publish_task_diagnostics
126
+ end
127
+
128
+ private
129
+
130
+ def start_task(task, matcher)
131
+ output = @task_runner.run(task)
132
+ cleanup_stale_task_outputs
133
+ if matcher
134
+ @task_matchers[output.id] = Task::ProblemMatcher.new(matcher, root: @root) { publish_task_diagnostics }
135
+ end
136
+ show_task_output(output) if output.presentation.fetch("reveal") == "always"
137
+ @window&.request_frame
138
+ output
139
+ end
140
+
141
+ def cleanup_stale_task_outputs
142
+ ids = task_outputs.map(&:id)
143
+ (@task_matchers.keys | @task_finished.keys).each do |id|
144
+ next if ids.include?(id)
145
+ @task_matchers.delete(id)
146
+ @task_finished.delete(id)
147
+ end
148
+ publish_task_diagnostics
149
+ end
150
+
151
+ def forget_task_output(output)
152
+ forget_test_output(output)
153
+ @task_matchers.delete(output.id)
154
+ @task_finished.delete(output.id)
155
+ publish_task_diagnostics
156
+ end
157
+
158
+ def publish_task_diagnostics
159
+ grouped = @task_matchers.values.each_with_object(Hash.new { |hash, uri| hash[uri] = [] }) do |matcher, result|
160
+ matcher.diagnostics.each { |uri, values| result[uri].concat(values) }
161
+ end
162
+ snapshot = grouped.to_h do |uri, values|
163
+ [uri, values.first(Diagnostics::PUBLICATION_LIMIT).freeze]
164
+ end
165
+ snapshot.freeze
166
+ changed = (@task_diagnostic_snapshot.keys | snapshot.keys).select do |uri|
167
+ @task_diagnostic_snapshot[uri] != snapshot[uri]
168
+ end
169
+ return false if changed.empty?
170
+
171
+ changed.each do |uri|
172
+ @diagnostics.publish(:task, uri, snapshot.fetch(uri, []), notify: false)
173
+ end
174
+ @task_diagnostic_snapshot = snapshot
175
+ changed.freeze
176
+ @window ? post { diagnostics_changed(changed) } : diagnostics_changed(changed)
177
+ true
178
+ end
179
+
180
+ def task_context
181
+ current = editor
182
+ file = current&.buffer&.path
183
+ line_number = current && current.buffer.rope.point_at(current.primary.head).row + 1
184
+ selected_text = nil
185
+ if current && !current.primary.empty?
186
+ range = current.primary.range
187
+ raise Error, "selectedText exceeds 1 MiB" if range.size > Task::Configuration::MAX_EXPANDED_BYTES
188
+ selected_text = current.buffer.rope.byteslice(range).to_s
189
+ end
190
+ {file: file, line_number: line_number, selected_text: selected_text}
191
+ end
192
+ end
193
+ end
194
+ end