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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
data/lib/canopus/project.rb
CHANGED
|
@@ -4,6 +4,10 @@ require "set"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "tempfile"
|
|
6
6
|
require "thuban"
|
|
7
|
+
require_relative "regexp_compat"
|
|
8
|
+
alkaid_path = ENV["ALKAID_PATH"]
|
|
9
|
+
alkaid_root = File.expand_path("../..", __dir__)
|
|
10
|
+
alkaid_path ? require(File.expand_path("lib/alkaid", File.expand_path(alkaid_path, alkaid_root))) : require("alkaid")
|
|
7
11
|
|
|
8
12
|
module Canopus
|
|
9
13
|
class Project
|
|
@@ -89,14 +93,20 @@ module Canopus
|
|
|
89
93
|
false
|
|
90
94
|
end
|
|
91
95
|
|
|
92
|
-
def
|
|
96
|
+
def ignore_matcher
|
|
97
|
+
Thuban::IgnoreMatcher.load(root, global: false).add("/.canopus/trash/\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
93
100
|
def watcher(**options) = Watcher.new(self, **options)
|
|
94
101
|
|
|
95
102
|
# Each file is replaced atomically; a concurrent write aborts that file.
|
|
96
|
-
def replace(pattern, replacement,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
def replace(pattern, replacement, workers: 4, max_size: 10 * 1024 * 1024, extensions: nil, limit: nil,
|
|
104
|
+
case_sensitive: true, cancelled: nil, paths: nil)
|
|
105
|
+
expression = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern), case_sensitive ? 0 : Regexp::IGNORECASE)
|
|
106
|
+
matches = Alkaid::Search.new(root, pattern: expression, ignore: ignore_matcher, workers: workers,
|
|
107
|
+
max_file_size: max_size, max_matches: limit, extensions: extensions, paths: paths, hidden: true,
|
|
108
|
+
cancelled: cancelled).run
|
|
109
|
+
paths = matches.map(&:path).uniq
|
|
100
110
|
paths.to_h do |relative|
|
|
101
111
|
absolute = path(relative)
|
|
102
112
|
before = File.stat(absolute)
|
|
@@ -123,5 +133,4 @@ module Canopus
|
|
|
123
133
|
end
|
|
124
134
|
end
|
|
125
135
|
|
|
126
|
-
require_relative "project/search"
|
|
127
136
|
require_relative "project/watcher"
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
module Provider
|
|
5
|
+
Completion = Data.define(:label, :insert_text, :kind, :detail, :documentation,
|
|
6
|
+
:sort_text, :filter_text, :additional_edits, :source)
|
|
7
|
+
|
|
8
|
+
class Registry
|
|
9
|
+
MAX_PROVIDERS = 64
|
|
10
|
+
MAX_ITEMS = 10_000
|
|
11
|
+
MAX_STRING = 65_536
|
|
12
|
+
PROVIDER_TIMEOUT = 10
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@completion, @inline, @sequence = {}, {}, 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def register_completion(source, priority:, &supplier)
|
|
19
|
+
source = normalize_source(source)
|
|
20
|
+
raise ArgumentError, "completion priority must be finite" unless priority.is_a?(Numeric) && priority.real? && priority.finite?
|
|
21
|
+
raise ArgumentError, "completion supplier required" unless supplier
|
|
22
|
+
raise ArgumentError, "too many completion providers" if !@completion.key?(source) && @completion.length >= MAX_PROVIDERS
|
|
23
|
+
|
|
24
|
+
@sequence += 1
|
|
25
|
+
@completion[source] = [priority, @sequence, supplier]
|
|
26
|
+
source
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def complete(buffer, offset, context)
|
|
30
|
+
validate_position(buffer, offset)
|
|
31
|
+
query = completion_query(context)
|
|
32
|
+
ranked, jobs = [], []
|
|
33
|
+
@completion.to_a.each do |source, details|
|
|
34
|
+
thread = Thread.new { resolve(details.last.call(buffer, offset, context)) }
|
|
35
|
+
thread.report_on_exception = false
|
|
36
|
+
jobs << [source, details, thread]
|
|
37
|
+
end
|
|
38
|
+
jobs.sort_by! { |_source, (priority, registration, _supplier), _thread| [-priority, registration] }
|
|
39
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PROVIDER_TIMEOUT
|
|
40
|
+
jobs.each do |source, (priority, registration, _supplier), thread|
|
|
41
|
+
break if ranked.length == MAX_ITEMS
|
|
42
|
+
begin
|
|
43
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
44
|
+
unless !thread.alive? || remaining.positive? && thread.join(remaining)
|
|
45
|
+
raise RuntimeError, "completion provider timed out"
|
|
46
|
+
end
|
|
47
|
+
supplied = thread.value
|
|
48
|
+
raise TypeError, "completion supplier must return an array" unless supplied.is_a?(Array)
|
|
49
|
+
raise ArgumentError, "completion provider returned too many items" if supplied.length > MAX_ITEMS
|
|
50
|
+
supplied = supplied.first(MAX_ITEMS - ranked.length)
|
|
51
|
+
|
|
52
|
+
ranked.concat(supplied.each_with_index.map do |item, index|
|
|
53
|
+
completion = normalize_completion(item, source, buffer)
|
|
54
|
+
score = Spica.score(query, completion.filter_text || completion.label)
|
|
55
|
+
[completion, score, priority, registration, index]
|
|
56
|
+
end)
|
|
57
|
+
rescue StandardError => error
|
|
58
|
+
provider_error(context, source, error)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
ranked.sort_by! { |completion, score, priority, registration, index| [-score, -priority, completion.sort_text || completion.label, registration, index] }
|
|
63
|
+
ranked.uniq! { |completion, _score, _priority, _registration, _index| [completion.label, completion.insert_text] }
|
|
64
|
+
ranked.map!(&:first)
|
|
65
|
+
ranked.freeze
|
|
66
|
+
ensure
|
|
67
|
+
jobs&.each do |_source, _details, thread|
|
|
68
|
+
thread.kill if thread.alive?
|
|
69
|
+
begin
|
|
70
|
+
thread.join
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_inline_completion(source, &supplier)
|
|
78
|
+
source = normalize_source(source)
|
|
79
|
+
raise ArgumentError, "inline completion supplier required" unless supplier
|
|
80
|
+
raise ArgumentError, "too many inline completion providers" if !@inline.key?(source) && @inline.length >= MAX_PROVIDERS
|
|
81
|
+
|
|
82
|
+
@sequence += 1
|
|
83
|
+
@inline[source] = [@sequence, supplier]
|
|
84
|
+
source
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def inline_completion(buffer, offset)
|
|
88
|
+
validate_position(buffer, offset)
|
|
89
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PROVIDER_TIMEOUT
|
|
90
|
+
@inline.sort_by { |_source, (registration, _supplier)| registration }.each do |_source, (_registration, supplier)|
|
|
91
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
92
|
+
break unless remaining.positive?
|
|
93
|
+
thread = Thread.new { resolve(supplier.call(buffer, offset)) }
|
|
94
|
+
thread.report_on_exception = false
|
|
95
|
+
begin
|
|
96
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
|
+
break unless !thread.alive? || remaining.positive? && thread.join(remaining)
|
|
98
|
+
value = thread.value
|
|
99
|
+
next if value.nil?
|
|
100
|
+
return normalize_string(value, "inline completion")
|
|
101
|
+
rescue StandardError
|
|
102
|
+
next
|
|
103
|
+
ensure
|
|
104
|
+
thread.kill if thread.alive?
|
|
105
|
+
begin
|
|
106
|
+
thread.join
|
|
107
|
+
rescue StandardError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def resolve(value)
|
|
118
|
+
value.respond_to?(:await) ? value.await(timeout: PROVIDER_TIMEOUT) : value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def completion_query(context)
|
|
122
|
+
value = context.respond_to?(:[]) && (context[:query] || context["query"])
|
|
123
|
+
value.nil? ? "" : normalize_string(value, "completion query", maximum: 256)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def normalize_completion(item, source, buffer)
|
|
127
|
+
raise TypeError, "completion provider must return completions" unless item.is_a?(Completion)
|
|
128
|
+
|
|
129
|
+
label = normalize_string(item.label, "completion label")
|
|
130
|
+
raise ArgumentError, "completion label must not be empty" if label.empty?
|
|
131
|
+
insert_text = item.insert_text.nil? ? label : normalize_string(item.insert_text, "completion insertion")
|
|
132
|
+
detail = normalize_optional_string(item.detail, "completion detail")
|
|
133
|
+
documentation = normalize_optional_string(item.documentation, "completion documentation")
|
|
134
|
+
sort_text = normalize_optional_string(item.sort_text, "completion sort text")
|
|
135
|
+
filter_text = normalize_optional_string(item.filter_text, "completion filter text")
|
|
136
|
+
kind = item.kind
|
|
137
|
+
unless kind.nil? || kind.is_a?(Integer) || kind.is_a?(Symbol) || kind.is_a?(String)
|
|
138
|
+
raise TypeError, "invalid completion kind"
|
|
139
|
+
end
|
|
140
|
+
kind = normalize_string(kind, "completion kind") if kind.is_a?(String)
|
|
141
|
+
edits = item.additional_edits || []
|
|
142
|
+
raise TypeError, "completion additional edits must be an array" unless edits.is_a?(Array)
|
|
143
|
+
raise ArgumentError, "too many completion additional edits" if edits.length > 1_000
|
|
144
|
+
edits = edits.map do |entry|
|
|
145
|
+
unless entry.is_a?(Array) && entry.length == 2 && entry[0].is_a?(Range) && entry[1].is_a?(String) &&
|
|
146
|
+
entry[0].begin.is_a?(Integer) && entry[0].end.is_a?(Integer) && entry[0].begin >= 0 && entry[0].end >= entry[0].begin &&
|
|
147
|
+
entry[0].end <= buffer.rope.bytesize
|
|
148
|
+
raise TypeError, "invalid completion additional edit"
|
|
149
|
+
end
|
|
150
|
+
buffer.rope.point_at(entry[0].begin)
|
|
151
|
+
buffer.rope.point_at(entry[0].end)
|
|
152
|
+
[(entry[0].begin...entry[0].end).freeze, normalize_string(entry[1], "completion additional edit")].freeze
|
|
153
|
+
end.freeze
|
|
154
|
+
|
|
155
|
+
Completion.new(label, insert_text, kind, detail, documentation, sort_text, filter_text, edits, source)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def normalize_optional_string(value, name)
|
|
159
|
+
value.nil? ? nil : normalize_string(value, name)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def normalize_string(value, name, maximum: MAX_STRING)
|
|
163
|
+
utf8 = value.is_a?(String) && value.valid_encoding? &&
|
|
164
|
+
(value.encoding == Encoding::UTF_8 || value.encoding.ascii_compatible? && value.ascii_only?)
|
|
165
|
+
unless utf8 && value.bytesize <= maximum && !value.include?("\0")
|
|
166
|
+
raise ArgumentError, "invalid #{name}"
|
|
167
|
+
end
|
|
168
|
+
value.dup.freeze
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def normalize_source(source)
|
|
172
|
+
valid = source.is_a?(Symbol) && !source.to_s.empty? && !source.to_s.include?(":") &&
|
|
173
|
+
!source.to_s.match?(/[\x00-\x1f\x7f]/)
|
|
174
|
+
raise ArgumentError, "invalid provider source" unless valid
|
|
175
|
+
source
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def validate_position(buffer, offset)
|
|
179
|
+
size = buffer.respond_to?(:rope) && buffer.rope.respond_to?(:bytesize) ? buffer.rope.bytesize : nil
|
|
180
|
+
unless offset.is_a?(Integer) && size.is_a?(Integer) && offset.between?(0, size)
|
|
181
|
+
raise ArgumentError, "invalid provider buffer offset"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def provider_error(context, source, error)
|
|
186
|
+
return unless context.is_a?(Hash) && context[:errors].is_a?(Array) && context[:errors].length < MAX_PROVIDERS
|
|
187
|
+
message = error.message.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).slice(0, 4_096).freeze
|
|
188
|
+
context[:errors] << [source, message].freeze
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "timeout"
|
|
3
|
+
require "timeout"
|
|
5
4
|
|
|
5
|
+
unless Regexp.method_defined?(:timeout)
|
|
6
6
|
class Regexp
|
|
7
|
-
TimeoutError = Class.new(
|
|
7
|
+
TimeoutError = Class.new(RegexpError)
|
|
8
8
|
TIMEOUTS = ObjectSpace::WeakMap.new
|
|
9
9
|
|
|
10
10
|
def timeout = TIMEOUTS[self]
|
|
@@ -13,18 +13,50 @@ unless Regexp.method_defined?(:timeout)
|
|
|
13
13
|
Regexp.singleton_class.prepend(Module.new do
|
|
14
14
|
def new(*arguments, timeout: nil)
|
|
15
15
|
expression = super(*arguments)
|
|
16
|
-
Regexp::TIMEOUTS[expression] = timeout
|
|
16
|
+
Regexp::TIMEOUTS[expression] = normalize_timeout(timeout)
|
|
17
|
+
expression
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def compile(*arguments, timeout: nil)
|
|
21
|
+
expression = super(*arguments)
|
|
22
|
+
Regexp::TIMEOUTS[expression] = normalize_timeout(timeout)
|
|
17
23
|
expression
|
|
18
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize_timeout(timeout)
|
|
29
|
+
return if timeout.nil?
|
|
30
|
+
raise TypeError, "no implicit conversion to float from string" if timeout.is_a?(String)
|
|
31
|
+
|
|
32
|
+
seconds = Float(timeout)
|
|
33
|
+
return if seconds.nan?
|
|
34
|
+
raise ArgumentError, "invalid timeout: #{timeout.inspect}" unless seconds.positive?
|
|
35
|
+
return if seconds < 0.000000001
|
|
36
|
+
return 18_446_744_073.709553 if seconds.infinite?
|
|
37
|
+
|
|
38
|
+
(seconds * 1_000_000_000).floor.fdiv(1_000_000_000)
|
|
39
|
+
end
|
|
40
|
+
end)
|
|
41
|
+
|
|
42
|
+
Regexp.prepend(Module.new do
|
|
43
|
+
def initialize_copy(other)
|
|
44
|
+
super
|
|
45
|
+
Regexp::TIMEOUTS[self] = other.timeout
|
|
46
|
+
end
|
|
19
47
|
end)
|
|
20
48
|
|
|
21
49
|
module Canopus
|
|
50
|
+
REGEXP_TIMEOUT_COMPAT = true
|
|
51
|
+
|
|
22
52
|
def self.with_regexp_timeout(expression)
|
|
23
53
|
expression.timeout ? Timeout.timeout(expression.timeout, Regexp::TimeoutError) { yield } : yield
|
|
24
54
|
end
|
|
25
55
|
end
|
|
26
56
|
else
|
|
27
57
|
module Canopus
|
|
58
|
+
REGEXP_TIMEOUT_COMPAT = false
|
|
59
|
+
|
|
28
60
|
def self.with_regexp_timeout(_expression) = yield
|
|
29
61
|
end
|
|
30
62
|
end
|
data/lib/canopus/selection.rb
CHANGED
|
@@ -8,4 +8,16 @@ module Canopus
|
|
|
8
8
|
def empty? = anchor == head
|
|
9
9
|
def reversed? = head < anchor
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
BlockSelection = Data.define(:anchor, :head) do
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
def each
|
|
16
|
+
return enum_for(__method__) unless block_given?
|
|
17
|
+
first, last = [anchor.row, head.row].minmax
|
|
18
|
+
first.upto(last) do |row|
|
|
19
|
+
yield DisplayPoint.new(row, anchor.column), DisplayPoint.new(row, head.column)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
11
23
|
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "kochab"
|
|
4
|
+
|
|
5
|
+
module Canopus
|
|
6
|
+
class Settings
|
|
7
|
+
module Schema
|
|
8
|
+
DEFAULT_AUTO_PAIRS = [["(", ")"], ["[", "]"], ["{", "}"], ['"', '"'], ["'", "'"]].map(&:freeze).freeze
|
|
9
|
+
DEFAULT_VALUES = {
|
|
10
|
+
"font_size" => 14, "tab_size" => 4, "use_tabs" => false, "soft_wrap" => false, "vim_mode" => false, "keymap" => [],
|
|
11
|
+
"auto_pairs" => DEFAULT_AUTO_PAIRS, "scroll_friction" => 12,
|
|
12
|
+
"theme" => "Canopus Dark", "font_family" => nil, "icon_theme" => nil, "languages" => {}, "language_servers" => {},
|
|
13
|
+
"debug_adapters" => {},
|
|
14
|
+
"tabs" => {"activate_on_close" => "history", "close_on_middle_click" => true, "close_empty_pane" => true,
|
|
15
|
+
"reopen_history_limit" => 20, "confirm_on_close_dirty" => true},
|
|
16
|
+
"diagnostics" => {"inline" => true, "inline_max_length" => 80, "severity" => "warning"},
|
|
17
|
+
"inlay_hints" => {"enabled" => true, "parameter_names" => true, "types" => true, "max_length" => 30},
|
|
18
|
+
"code_lens" => {"enabled" => true}, "bracket_colorization" => true,
|
|
19
|
+
"indent_guides" => {"enabled" => true, "active" => true},
|
|
20
|
+
"render_whitespace" => "boundary", "render_ideographic_space" => true,
|
|
21
|
+
"sticky_scroll" => {"enabled" => true, "max_lines" => 5}, "breadcrumbs" => {"enabled" => true},
|
|
22
|
+
"minimap" => {"enabled" => false, "width" => 100, "show_diagnostics" => true},
|
|
23
|
+
"auto_save" => "off", "auto_save_delay" => 1_000,
|
|
24
|
+
"editorconfig" => true, "trim_trailing_whitespace" => nil, "insert_final_newline" => nil, "max_line_length" => nil,
|
|
25
|
+
"persistent_undo" => {"enabled" => true, "max_entries" => 1_000, "expire_days" => 30},
|
|
26
|
+
"format_on_save" => false, "code_actions_on_save" => [], "format_on_save_timeout" => 2_000,
|
|
27
|
+
"git" => {"inline_blame" => "off", "autofetch" => false, "autofetch_interval" => 180},
|
|
28
|
+
"plugins" => {"sandbox" => "auto"},
|
|
29
|
+
"recovery" => {"enabled" => true, "interval" => 5_000},
|
|
30
|
+
"dock" => {"left" => {"size" => 220, "visible" => true}, "right" => {"size" => 260, "visible" => false},
|
|
31
|
+
"bottom" => {"size" => 280, "visible" => false},
|
|
32
|
+
"panels" => {"explorer" => {"size" => 220, "visible" => true}, "search" => {"size" => 220, "visible" => false},
|
|
33
|
+
"terminal" => {"size" => 280, "visible" => false}}},
|
|
34
|
+
"terminal" => {"shell" => nil, "working_directory" => "project", "env" => {}, "scrollback_lines" => 10_000,
|
|
35
|
+
"profiles" => {}, "default_profile" => nil, "font_size" => nil, "line_height" => 1.2, "copy_on_select" => false,
|
|
36
|
+
"blinking" => "terminal_controlled", "cursor_shape" => "block", "close_on_exit" => "clean",
|
|
37
|
+
"confirm_close_running" => true, "confirm_multiline_paste" => true, "restore_on_startup" => false,
|
|
38
|
+
"hide_when_empty" => false, "shell_integration" => true, "max_bytes_per_frame" => 262_144,
|
|
39
|
+
"queue_limit_bytes" => 8_388_608, "resize_debounce_ms" => 100, "min_rows" => 4, "min_cols" => 20}
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
NULL_TYPES = {
|
|
43
|
+
%w[terminal shell] => ["string", "array", "null"],
|
|
44
|
+
%w[terminal default_profile] => ["string", "null"],
|
|
45
|
+
%w[terminal font_size] => ["number", "null"],
|
|
46
|
+
%w[font_family] => ["string", "null"], %w[icon_theme] => ["string", "null"],
|
|
47
|
+
%w[trim_trailing_whitespace] => ["boolean", "null"], %w[insert_final_newline] => ["boolean", "null"],
|
|
48
|
+
%w[max_line_length] => ["integer", "null"]
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
BASE_SCHEMA = {"$schema" => "https://json-schema.org/draft/2020-12/schema", "type" => "object", "properties" => {
|
|
52
|
+
"font_size" => {"type" => "number", "minimum" => 6, "maximum" => 96},
|
|
53
|
+
"tab_size" => {"type" => "integer", "minimum" => 1, "maximum" => 16},
|
|
54
|
+
"scroll_friction" => {"type" => "number", "minimum" => 0, "maximum" => 100},
|
|
55
|
+
"soft_wrap" => {"type" => "boolean"}, "vim_mode" => {"type" => "boolean"}, "use_tabs" => {"type" => "boolean"},
|
|
56
|
+
"auto_pairs" => {"type" => "array", "maxItems" => 64, "items" => {"type" => "array", "minItems" => 2, "maxItems" => 2, "items" => {"type" => "string"}}},
|
|
57
|
+
"keymap" => {"type" => "array", "maxItems" => 128, "items" => {"type" => "object", "required" => ["bindings"], "properties" => {
|
|
58
|
+
"context" => {"type" => "string", "maxLength" => 256},
|
|
59
|
+
"bindings" => {"type" => "object", "maxProperties" => 1024, "additionalProperties" => {"type" => ["string", "null"]}}}}},
|
|
60
|
+
"theme" => {"type" => "string"}, "font_family" => {"type" => ["string", "null"]}, "icon_theme" => {"type" => ["string", "null"]},
|
|
61
|
+
"tabs" => {"type" => "object", "properties" => {
|
|
62
|
+
"activate_on_close" => {"type" => "string", "enum" => %w[history neighbour left right]},
|
|
63
|
+
"close_on_middle_click" => {"type" => "boolean"}, "close_empty_pane" => {"type" => "boolean"},
|
|
64
|
+
"reopen_history_limit" => {"type" => "integer", "minimum" => 0, "maximum" => 1000},
|
|
65
|
+
"confirm_on_close_dirty" => {"type" => "boolean"}}},
|
|
66
|
+
"terminal" => {"type" => "object", "properties" => {
|
|
67
|
+
"shell" => {"type" => ["string", "array", "null"]}, "working_directory" => {"type" => "string"},
|
|
68
|
+
"env" => {"type" => "object", "additionalProperties" => {"type" => ["string", "null"]}},
|
|
69
|
+
"scrollback_lines" => {"type" => "integer", "minimum" => 0, "maximum" => 1_000_000},
|
|
70
|
+
"shell_integration" => {"type" => "boolean"}, "default_profile" => {"type" => ["string", "null"]},
|
|
71
|
+
"profiles" => {"type" => "object", "additionalProperties" => {"$ref" => "#/$defs/terminal_profile"}},
|
|
72
|
+
"font_size" => {"type" => ["number", "null"], "minimum" => 6, "maximum" => 96},
|
|
73
|
+
"line_height" => {"type" => "number", "minimum" => 0.5, "maximum" => 4}, "copy_on_select" => {"type" => "boolean"},
|
|
74
|
+
"blinking" => {"type" => "string", "enum" => %w[off on terminal_controlled]},
|
|
75
|
+
"cursor_shape" => {"type" => "string", "enum" => %w[block bar underline]},
|
|
76
|
+
"close_on_exit" => {"type" => "string", "enum" => %w[never clean always]},
|
|
77
|
+
"confirm_close_running" => {"type" => "boolean"}, "confirm_multiline_paste" => {"type" => "boolean"},
|
|
78
|
+
"restore_on_startup" => {"type" => "boolean"}, "hide_when_empty" => {"type" => "boolean"},
|
|
79
|
+
"max_bytes_per_frame" => {"type" => "integer", "minimum" => 1, "maximum" => 16_777_216},
|
|
80
|
+
"queue_limit_bytes" => {"type" => "integer", "minimum" => 65_536, "maximum" => 268_435_456},
|
|
81
|
+
"resize_debounce_ms" => {"type" => "integer", "minimum" => 0, "maximum" => 10_000},
|
|
82
|
+
"min_rows" => {"type" => "integer", "minimum" => 1, "maximum" => 1000},
|
|
83
|
+
"min_cols" => {"type" => "integer", "minimum" => 1, "maximum" => 1000}}},
|
|
84
|
+
"diagnostics" => {"type" => "object", "required" => %w[inline inline_max_length severity], "properties" => {
|
|
85
|
+
"inline" => {"type" => "boolean"}, "inline_max_length" => {"type" => "integer", "minimum" => 1, "maximum" => 10_000},
|
|
86
|
+
"severity" => {"type" => "string", "enum" => %w[error warning information hint]}}},
|
|
87
|
+
"inlay_hints" => {"type" => "object", "required" => %w[enabled parameter_names types max_length], "properties" => {
|
|
88
|
+
"enabled" => {"type" => "boolean"}, "parameter_names" => {"type" => "boolean"}, "types" => {"type" => "boolean"},
|
|
89
|
+
"max_length" => {"type" => "integer", "minimum" => 1, "maximum" => 10_000}}},
|
|
90
|
+
"code_lens" => {"type" => "object", "required" => ["enabled"], "properties" => {"enabled" => {"type" => "boolean"}}},
|
|
91
|
+
"bracket_colorization" => {"type" => "boolean"},
|
|
92
|
+
"indent_guides" => {"type" => "object", "required" => %w[enabled active], "properties" => {"enabled" => {"type" => "boolean"}, "active" => {"type" => "boolean"}}},
|
|
93
|
+
"render_whitespace" => {"type" => "string", "enum" => %w[none boundary selection all]}, "render_ideographic_space" => {"type" => "boolean"},
|
|
94
|
+
"sticky_scroll" => {"type" => "object", "required" => %w[enabled max_lines], "properties" => {"enabled" => {"type" => "boolean"}, "max_lines" => {"type" => "integer", "minimum" => 1, "maximum" => 20}}},
|
|
95
|
+
"breadcrumbs" => {"type" => "object", "required" => ["enabled"], "properties" => {"enabled" => {"type" => "boolean"}}},
|
|
96
|
+
"minimap" => {"type" => "object", "required" => %w[enabled width show_diagnostics], "properties" => {"enabled" => {"type" => "boolean"}, "width" => {"type" => "integer", "minimum" => 40, "maximum" => 400}, "show_diagnostics" => {"type" => "boolean"}}},
|
|
97
|
+
"auto_save" => {"type" => "string", "enum" => %w[off after_delay on_focus_change]}, "auto_save_delay" => {"type" => "integer", "minimum" => 100, "maximum" => 3_600_000},
|
|
98
|
+
"editorconfig" => {"type" => "boolean"}, "trim_trailing_whitespace" => {"type" => ["boolean", "null"]}, "insert_final_newline" => {"type" => ["boolean", "null"]},
|
|
99
|
+
"max_line_length" => {"type" => ["integer", "null"], "minimum" => 1, "maximum" => 1_000_000},
|
|
100
|
+
"persistent_undo" => {"type" => "object", "additionalProperties" => false, "required" => %w[enabled max_entries expire_days], "properties" => {
|
|
101
|
+
"enabled" => {"type" => "boolean"}, "max_entries" => {"type" => "integer", "minimum" => 1, "maximum" => 10_000}, "expire_days" => {"type" => "integer", "minimum" => 1, "maximum" => 3_650}}},
|
|
102
|
+
"format_on_save" => {"type" => "boolean"}, "code_actions_on_save" => {"type" => "array", "maxItems" => 64, "uniqueItems" => true, "items" => {"type" => "string", "minLength" => 1, "maxLength" => 256}},
|
|
103
|
+
"format_on_save_timeout" => {"type" => "integer", "minimum" => 1, "maximum" => 60_000},
|
|
104
|
+
"git" => {"type" => "object", "additionalProperties" => false, "required" => %w[inline_blame autofetch autofetch_interval], "properties" => {
|
|
105
|
+
"inline_blame" => {"type" => "string", "enum" => %w[off cursor all]}, "autofetch" => {"type" => "boolean"}, "autofetch_interval" => {"type" => "integer", "minimum" => 10, "maximum" => 86_400}}},
|
|
106
|
+
"plugins" => {"type" => "object", "additionalProperties" => false, "required" => ["sandbox"], "properties" => {
|
|
107
|
+
"sandbox" => {"type" => "string", "enum" => %w[off auto required]}}},
|
|
108
|
+
"recovery" => {"type" => "object", "additionalProperties" => false, "required" => %w[enabled interval], "properties" => {"enabled" => {"type" => "boolean"}, "interval" => {"type" => "integer", "minimum" => 100, "maximum" => 3_600_000}}},
|
|
109
|
+
"dock" => {"type" => "object", "properties" => {"left" => {"$ref" => "#/$defs/dock"}, "right" => {"$ref" => "#/$defs/dock"}, "bottom" => {"$ref" => "#/$defs/dock"}, "panels" => {"type" => "object", "maxProperties" => 1000, "additionalProperties" => {"$ref" => "#/$defs/panel"}}}},
|
|
110
|
+
"languages" => {"type" => "object", "additionalProperties" => {"allOf" => [{"$ref" => "#"}, {"properties" => {"languages" => false, "debug_adapters" => false, "recovery" => false, "auto_save" => false, "auto_save_delay" => false, "persistent_undo" => false}}]}},
|
|
111
|
+
"language_servers" => {"type" => "object", "additionalProperties" => {"anyOf" => [{"type" => "null"}, {"$ref" => "#/$defs/language_server"}, {"type" => "array", "minItems" => 1, "items" => {"type" => "string", "minLength" => 1}}, {"type" => "array", "minItems" => 1, "maxItems" => 16, "items" => {"$ref" => "#/$defs/language_server"}}]}},
|
|
112
|
+
"debug_adapters" => {"type" => "object", "maxProperties" => 64, "propertyNames" => {"type" => "string", "minLength" => 1, "maxLength" => 128, "pattern" => "^[A-Za-z0-9_.-]+$"}, "additionalProperties" => {"$ref" => "#/$defs/debug_adapter"}}},
|
|
113
|
+
"$defs" => {
|
|
114
|
+
"language_server" => {"type" => "object", "additionalProperties" => false, "required" => ["command"], "properties" => {"command" => {"type" => "array", "minItems" => 1, "items" => {"type" => "string", "minLength" => 1}}, "env" => {"type" => "object", "additionalProperties" => {"type" => ["string", "null"]}}, "initialization_options" => {}, "configuration" => {"type" => "object"}, "features" => {"type" => "array", "minItems" => 1, "uniqueItems" => true, "items" => {"type" => "string", "enum" => %w[completion diagnostics codeAction formatting definition typeDefinition implementation hover signatureHelp references rename documentSymbol codeLens inlayHint semanticTokens documentHighlight foldingRange selectionRange callHierarchy typeHierarchy documentLink linkedEditingRange workspaceSymbol]}}}},
|
|
115
|
+
"debug_adapter" => {"type" => "object", "additionalProperties" => false, "required" => %w[command transport], "properties" => {"command" => {"type" => "array", "minItems" => 1, "maxItems" => 32, "items" => {"type" => "string", "minLength" => 1, "maxLength" => 4096, "pattern" => "^(?![\\s\\S]*[\\u0000-\\u001f\\u007f])[\\s\\S]+$"}}, "transport" => {"type" => "string", "enum" => %w[stdio tcp]}}},
|
|
116
|
+
"terminal_profile" => {"type" => "object", "additionalProperties" => false, "properties" => {"command" => {"anyOf" => [{"type" => "string", "minLength" => 1}, {"type" => "array", "minItems" => 1, "items" => {"type" => "string"}}]}, "path" => {"type" => "string", "minLength" => 1}, "args" => {"type" => "array", "items" => {"type" => "string"}}, "env" => {"type" => "object", "additionalProperties" => {"type" => ["string", "null"]}}}},
|
|
117
|
+
"dock" => {"type" => "object", "required" => %w[size visible], "properties" => {"size" => {"type" => "number", "exclusiveMinimum" => 0}, "visible" => {"type" => "boolean"}}},
|
|
118
|
+
"panel" => {"type" => "object", "required" => %w[size visible], "properties" => {"size" => {"type" => "number", "exclusiveMinimum" => 0}, "visible" => {"type" => "boolean"}}}
|
|
119
|
+
}
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
def self.copy(value)
|
|
123
|
+
case value
|
|
124
|
+
when Hash then value.to_h { |key, child| [key, copy(child)] }
|
|
125
|
+
when Array then value.map { |child| copy(child) }
|
|
126
|
+
else value
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.inferred(value, path)
|
|
131
|
+
return {"type" => NULL_TYPES.fetch(path, "null")} if value.nil?
|
|
132
|
+
|
|
133
|
+
type = case value
|
|
134
|
+
when true, false then "boolean"
|
|
135
|
+
when Integer then "integer"
|
|
136
|
+
when Numeric then "number"
|
|
137
|
+
when String then "string"
|
|
138
|
+
when Array then "array"
|
|
139
|
+
when Hash then "object"
|
|
140
|
+
else "any"
|
|
141
|
+
end
|
|
142
|
+
result = {"type" => type}
|
|
143
|
+
result["items"] = value.empty? ? {"type" => "any"} : inferred(value.first, path + [0]) if value.is_a?(Array)
|
|
144
|
+
result
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.annotate(node, defaults, path = [])
|
|
148
|
+
result = copy(node)
|
|
149
|
+
unless path.empty?
|
|
150
|
+
result["description"] ||= "#{path.join(".")} setting"
|
|
151
|
+
result["default"] = copy(defaults)
|
|
152
|
+
end
|
|
153
|
+
if defaults.is_a?(Hash)
|
|
154
|
+
properties = result["properties"] ||= {}
|
|
155
|
+
defaults.each do |name, value|
|
|
156
|
+
properties[name] = annotate(properties.fetch(name, inferred(value, path + [name])), value, path + [name])
|
|
157
|
+
end
|
|
158
|
+
elsif defaults.is_a?(Array) && result["items"].is_a?(Hash) && !defaults.empty?
|
|
159
|
+
result["items"] = annotate(result["items"], defaults.first, path + [0])
|
|
160
|
+
end
|
|
161
|
+
result
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.deep_freeze(value)
|
|
165
|
+
case value
|
|
166
|
+
when Hash
|
|
167
|
+
value.each { |key, child| deep_freeze(key); deep_freeze(child) }
|
|
168
|
+
when Array
|
|
169
|
+
value.each { |child| deep_freeze(child) }
|
|
170
|
+
end
|
|
171
|
+
value.freeze
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.describe_nodes(node, path = [])
|
|
175
|
+
result = copy(node)
|
|
176
|
+
result["description"] ||= "#{path.join(".")} setting" unless path.empty?
|
|
177
|
+
if result["properties"].is_a?(Hash)
|
|
178
|
+
result["properties"] = result["properties"].to_h do |name, child|
|
|
179
|
+
[name, describe_nodes(child, path + [name])]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
result["items"] = describe_nodes(result["items"], path + [0]) if result["items"].is_a?(Hash)
|
|
183
|
+
result["additionalProperties"] = describe_nodes(result["additionalProperties"], path + ["*"]) if result["additionalProperties"].is_a?(Hash)
|
|
184
|
+
if result["$defs"].is_a?(Hash)
|
|
185
|
+
result["$defs"] = result["$defs"].to_h { |name, child| [name, describe_nodes(child, path + [name])] }
|
|
186
|
+
end
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.kochab_input(schema)
|
|
191
|
+
result = copy(schema)
|
|
192
|
+
result.dig("properties", "languages", "additionalProperties")["$ref"] = "#"
|
|
193
|
+
result
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
JSON_SCHEMA = describe_nodes(annotate(BASE_SCHEMA, DEFAULT_VALUES)).freeze
|
|
197
|
+
KOCHAB_SCHEMA = Kochab::Schema.from_json_schema(kochab_input(JSON_SCHEMA))
|
|
198
|
+
DEFAULTS = deep_freeze(KOCHAB_SCHEMA.defaults)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|