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/settings.rb
CHANGED
|
@@ -1,68 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tempfile"
|
|
4
6
|
require "kochab"
|
|
7
|
+
require_relative "settings/schema"
|
|
5
8
|
|
|
6
9
|
module Canopus
|
|
7
10
|
class Settings
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"bottom" => {"size" => 280, "visible" => false}.freeze,
|
|
16
|
-
"panels" => {"explorer" => {"size" => 220, "visible" => true}.freeze,
|
|
17
|
-
"search" => {"size" => 220, "visible" => false}.freeze,
|
|
18
|
-
"terminal" => {"size" => 280, "visible" => false}.freeze}.freeze}.freeze,
|
|
19
|
-
"terminal" => {"shell" => nil, "working_directory" => "project", "env" => {}.freeze, "scrollback_lines" => 10_000,
|
|
20
|
-
"font_size" => nil, "line_height" => 1.2, "copy_on_select" => false, "blinking" => "terminal_controlled", "cursor_shape" => "block",
|
|
21
|
-
"close_on_exit" => "clean", "confirm_close_running" => true, "confirm_multiline_paste" => true,
|
|
22
|
-
"restore_on_startup" => false, "hide_when_empty" => false, "max_bytes_per_frame" => 262_144,
|
|
23
|
-
"queue_limit_bytes" => 8_388_608, "resize_debounce_ms" => 100, "min_rows" => 4, "min_cols" => 20}.freeze}.freeze
|
|
24
|
-
SCHEMA = {"$schema" => "https://json-schema.org/draft/2020-12/schema", "type" => "object", "properties" => {
|
|
25
|
-
"font_size" => {"type" => "number", "minimum" => 6, "maximum" => 96},
|
|
26
|
-
"tab_size" => {"type" => "integer", "minimum" => 1, "maximum" => 16},
|
|
27
|
-
"scroll_friction" => {"type" => "number", "minimum" => 0, "maximum" => 100},
|
|
28
|
-
"soft_wrap" => {"type" => "boolean"}, "vim_mode" => {"type" => "boolean"},
|
|
29
|
-
"use_tabs" => {"type" => "boolean"},
|
|
30
|
-
"keymap" => {"type" => "array", "maxItems" => 128, "items" => {"type" => "object", "required" => ["bindings"], "properties" => {
|
|
31
|
-
"context" => {"type" => "string", "maxLength" => 256},
|
|
32
|
-
"bindings" => {"type" => "object", "maxProperties" => 1024, "additionalProperties" => {"type" => ["string", "null"]}}}}},
|
|
33
|
-
"theme" => {"type" => "string"}, "font_family" => {"type" => ["string", "null"]},
|
|
34
|
-
"icon_theme" => {"type" => ["string", "null"]},
|
|
35
|
-
"tabs" => {"type" => "object"}, "terminal" => {"type" => "object"},
|
|
36
|
-
"dock" => {"type" => "object", "properties" => {
|
|
37
|
-
"left" => {"$ref" => "#/$defs/dock"}, "right" => {"$ref" => "#/$defs/dock"},
|
|
38
|
-
"bottom" => {"$ref" => "#/$defs/dock"}, "panels" => {"type" => "object", "maxProperties" => 1000,
|
|
39
|
-
"additionalProperties" => {"$ref" => "#/$defs/panel"}}}},
|
|
40
|
-
"languages" => {"type" => "object", "additionalProperties" => {"$ref" => "#"}},
|
|
41
|
-
"language_servers" => {"type" => "object"}}, "$defs" => {
|
|
42
|
-
"dock" => {"type" => "object", "required" => %w[size visible], "properties" => {
|
|
43
|
-
"size" => {"type" => "number", "exclusiveMinimum" => 0}, "visible" => {"type" => "boolean"}}},
|
|
44
|
-
"panel" => {"type" => "object", "required" => %w[size visible], "properties" => {
|
|
45
|
-
"size" => {"type" => "number", "exclusiveMinimum" => 0}, "visible" => {"type" => "boolean"}}}}}.freeze
|
|
11
|
+
ReloadFallback = Struct.new(:values, keyword_init: true)
|
|
12
|
+
private_constant :ReloadFallback
|
|
13
|
+
DEFAULT_AUTO_PAIRS = Schema::DEFAULT_AUTO_PAIRS
|
|
14
|
+
DEFAULTS = Schema::DEFAULTS
|
|
15
|
+
SCHEMA = Schema::JSON_SCHEMA
|
|
16
|
+
SCHEMA_MODEL = Schema::KOCHAB_SCHEMA
|
|
17
|
+
SCHEMA_METADATA = SCHEMA_MODEL
|
|
46
18
|
attr_reader :values, :errors, :layers
|
|
47
19
|
def self.schema = SCHEMA
|
|
20
|
+
def self.schema_model = SCHEMA_MODEL
|
|
21
|
+
def self.schema_metadata = SCHEMA_MODEL
|
|
22
|
+
def self.file_diagnostics(path)
|
|
23
|
+
return nil unless File.file?(path)
|
|
24
|
+
|
|
25
|
+
document = Kochab.parse(File.read(path))
|
|
26
|
+
diagnostics = document.errors.dup
|
|
27
|
+
if document.valid?
|
|
28
|
+
diagnostics.concat(SCHEMA_MODEL.validate(document).reject { |diagnostic| diagnostic.message == "Required value is missing" })
|
|
29
|
+
end
|
|
30
|
+
[document, diagnostics.freeze].freeze
|
|
31
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
48
34
|
def self.user_path
|
|
49
35
|
File.join(ENV["XDG_CONFIG_HOME"] || File.expand_path("~/.config"), "canopus", "settings.jsonc")
|
|
50
36
|
end
|
|
51
37
|
def initialize(*layers)
|
|
38
|
+
fallback = layers.pop.values if layers.last.is_a?(ReloadFallback)
|
|
52
39
|
@values, @errors = DEFAULTS.dup, []
|
|
53
40
|
@layers = layers.compact
|
|
41
|
+
@reload_fallback = fallback
|
|
42
|
+
@initializing = true
|
|
54
43
|
layers.compact.each { |layer| merge!(layer.is_a?(String) ? parse_file(layer) : layer) }
|
|
44
|
+
@initializing = false
|
|
45
|
+
@reload_fallback = nil
|
|
55
46
|
validate!
|
|
56
47
|
end
|
|
57
48
|
def [](key) = @values[key.to_s]
|
|
58
49
|
def paths = @layers.grep(String)
|
|
59
|
-
def reload = self.class.new(*@layers)
|
|
50
|
+
def reload = self.class.new(*@layers, ReloadFallback.new(values: @values))
|
|
60
51
|
def for_language(language) = Settings.new(@values, @values.fetch("languages", {}).fetch(language, {}))
|
|
61
52
|
def merge!(layer)
|
|
62
|
-
|
|
53
|
+
document = layer.is_a?(Kochab::Document)
|
|
54
|
+
raise Error, "settings must be an object" unless document || layer.is_a?(Hash)
|
|
63
55
|
previous = @values
|
|
64
|
-
@values =
|
|
56
|
+
@values = document ? merge_document(@values, layer) : merge_hash(@values, layer)
|
|
65
57
|
validate!
|
|
58
|
+
@layers << snapshot_layer(layer) unless @initializing
|
|
66
59
|
self
|
|
67
60
|
rescue StandardError
|
|
68
61
|
@values = previous if previous
|
|
@@ -72,8 +65,22 @@ module Canopus
|
|
|
72
65
|
source = File.file?(path) ? File.read(path) : "{}\n"
|
|
73
66
|
doc = Kochab.parse(source)
|
|
74
67
|
raise Error, "cannot edit invalid settings" unless doc.valid?
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
key_path = key.is_a?(Array) ? key : [key]
|
|
69
|
+
raise Error, "settings key path must contain strings" unless key_path.all? { |part| part.is_a?(String) && !part.empty? }
|
|
70
|
+
editable_source = source
|
|
71
|
+
(1...key_path.length).each do |length|
|
|
72
|
+
current = Kochab.parse(editable_source)
|
|
73
|
+
parent = key_path[0...length]
|
|
74
|
+
next if current.range_of(parent)
|
|
75
|
+
|
|
76
|
+
editable_source = Kochab.apply(editable_source, current.set(parent, {}))
|
|
77
|
+
end
|
|
78
|
+
doc = Kochab.parse(editable_source)
|
|
79
|
+
replacement = Kochab.apply(editable_source, doc.set(key_path, value))
|
|
80
|
+
candidate = Kochab.parse(replacement)
|
|
81
|
+
schema_errors = SCHEMA_MODEL.validate(candidate).reject { |diagnostic| diagnostic.message == "Required value is missing" }
|
|
82
|
+
raise Error, "invalid setting #{key_path.join(".")}: #{schema_errors.first.message}" unless schema_errors.empty?
|
|
83
|
+
Settings.new(candidate)
|
|
77
84
|
FileUtils.mkdir_p(File.dirname(path))
|
|
78
85
|
Tempfile.create([".settings-", ".json"], File.dirname(path)) do |file|
|
|
79
86
|
file.write(replacement)
|
|
@@ -90,10 +97,30 @@ module Canopus
|
|
|
90
97
|
document = Kochab.parse(File.read(path))
|
|
91
98
|
@errors.concat(document.errors)
|
|
92
99
|
raise Error, "invalid settings: #{path}" unless document.valid?
|
|
93
|
-
document.value
|
|
100
|
+
raise Error, "settings must be an object" unless document.value.is_a?(Hash)
|
|
101
|
+
document
|
|
102
|
+
end
|
|
103
|
+
def merge_hash(left, right)
|
|
104
|
+
left.merge(right) { |_, old, new| old.is_a?(Hash) && new.is_a?(Hash) ? merge_hash(old, new) : new }
|
|
94
105
|
end
|
|
95
|
-
def
|
|
96
|
-
|
|
106
|
+
def merge_document(previous, document)
|
|
107
|
+
fallback = previous_layer(document.value, @reload_fallback || previous)
|
|
108
|
+
SCHEMA_MODEL.merge(previous, fallback, document)
|
|
109
|
+
end
|
|
110
|
+
def previous_layer(layer, previous)
|
|
111
|
+
layer.each_with_object({}) do |(key, value), result|
|
|
112
|
+
old = previous[key]
|
|
113
|
+
next unless previous.key?(key)
|
|
114
|
+
|
|
115
|
+
result[key] = value.is_a?(Hash) && old.is_a?(Hash) ? previous_layer(value, old) : snapshot_layer(old)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
def snapshot_layer(value)
|
|
119
|
+
case value
|
|
120
|
+
when Hash then value.to_h { |key, child| [key, snapshot_layer(child)] }
|
|
121
|
+
when Array then value.map { |child| snapshot_layer(child) }
|
|
122
|
+
else value
|
|
123
|
+
end
|
|
97
124
|
end
|
|
98
125
|
def validate!
|
|
99
126
|
{"font_size" => 6..96, "tab_size" => 1..16, "scroll_friction" => 0..100}.each do |key, range|
|
|
@@ -101,20 +128,46 @@ module Canopus
|
|
|
101
128
|
raise Error, "invalid #{key}" unless value.is_a?(Numeric) && range.cover?(value)
|
|
102
129
|
end
|
|
103
130
|
raise Error, "tab_size must be an integer" unless @values["tab_size"].is_a?(Integer)
|
|
104
|
-
%w[soft_wrap vim_mode use_tabs].each { |key| raise Error, "#{key} must be true or false" unless [true, false].include?(@values[key]) }
|
|
131
|
+
%w[soft_wrap vim_mode use_tabs render_ideographic_space format_on_save].each { |key| raise Error, "#{key} must be true or false" unless [true, false].include?(@values[key]) }
|
|
132
|
+
raise Error, "invalid render_whitespace" unless %w[none boundary selection all].include?(@values["render_whitespace"])
|
|
105
133
|
validate_keymap!
|
|
134
|
+
validate_auto_pairs!
|
|
106
135
|
%w[languages language_servers].each { |key| raise Error, "#{key} must be an object" unless @values[key].is_a?(Hash) }
|
|
107
136
|
raise Error, "theme must be a string" unless @values["theme"].is_a?(String)
|
|
108
137
|
raise Error, "font_family must be a string or null" unless @values["font_family"].nil? || @values["font_family"].is_a?(String)
|
|
109
138
|
raise Error, "icon_theme must be a string or null" unless @values["icon_theme"].nil? || @values["icon_theme"].is_a?(String)
|
|
110
139
|
validate_tabs!
|
|
111
140
|
validate_terminal!
|
|
141
|
+
validate_diagnostics!
|
|
142
|
+
validate_inlay_hints!
|
|
143
|
+
validate_code_lens!
|
|
144
|
+
validate_structural_guides!
|
|
145
|
+
validate_sticky_scroll!
|
|
146
|
+
validate_breadcrumbs!
|
|
147
|
+
validate_minimap!
|
|
148
|
+
validate_auto_save!
|
|
149
|
+
validate_editorconfig!
|
|
150
|
+
validate_persistent_undo!
|
|
151
|
+
validate_save_actions!
|
|
152
|
+
validate_git!
|
|
153
|
+
validate_plugins!
|
|
154
|
+
validate_recovery!
|
|
155
|
+
validate_language_server_keys!
|
|
156
|
+
snapshot_language_servers!
|
|
157
|
+
validate_debug_adapters!
|
|
112
158
|
validate_dock!
|
|
113
159
|
@values["languages"] = @values["languages"].to_h do |name, layer|
|
|
114
160
|
raise Error, "language settings must be objects" unless name.is_a?(String) && layer.is_a?(Hash)
|
|
115
161
|
raise Error, "language settings cannot contain nested languages" if layer.key?("languages")
|
|
162
|
+
%w[debug_adapters recovery auto_save auto_save_delay persistent_undo].each do |key|
|
|
163
|
+
raise Error, "#{key} is a global setting" if layer.key?(key) || layer.key?(key.to_sym)
|
|
164
|
+
end
|
|
116
165
|
checked = Settings.new(@values.merge("languages" => {}), layer)
|
|
117
|
-
|
|
166
|
+
layer = layer.merge("keymap" => checked["keymap"]) if layer.key?("keymap")
|
|
167
|
+
layer = layer.merge("auto_pairs" => checked["auto_pairs"]) if layer.key?("auto_pairs")
|
|
168
|
+
layer = layer.merge("code_actions_on_save" => checked["code_actions_on_save"]) if layer.key?("code_actions_on_save")
|
|
169
|
+
layer = layer.merge("language_servers" => checked["language_servers"]) if layer.key?("language_servers")
|
|
170
|
+
[name, layer]
|
|
118
171
|
end
|
|
119
172
|
end
|
|
120
173
|
def validate_keymap!
|
|
@@ -140,6 +193,23 @@ module Canopus
|
|
|
140
193
|
raise Error, "invalid keymap: #{error.message}"
|
|
141
194
|
end
|
|
142
195
|
|
|
196
|
+
def validate_auto_pairs!
|
|
197
|
+
pairs = @values["auto_pairs"]
|
|
198
|
+
raise Error, "auto_pairs must be an array of at most 64 pairs" unless pairs.is_a?(Array) && pairs.length <= 64
|
|
199
|
+
openers = {}
|
|
200
|
+
@values["auto_pairs"] = pairs.map do |pair|
|
|
201
|
+
raise Error, "auto_pairs entries must contain an opener and closer" unless pair.is_a?(Array) && pair.length == 2
|
|
202
|
+
opener, closer = pair
|
|
203
|
+
unless [opener, closer].all? { |value| value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? &&
|
|
204
|
+
value.each_grapheme_cluster.one? && !value.match?(/\p{Cc}/) }
|
|
205
|
+
raise Error, "auto_pairs openers and closers must be single non-control UTF-8 graphemes"
|
|
206
|
+
end
|
|
207
|
+
raise Error, "auto_pairs openers must be unique" if openers[opener]
|
|
208
|
+
openers[opener] = true
|
|
209
|
+
[opener.dup.freeze, closer.dup.freeze].freeze
|
|
210
|
+
end.freeze
|
|
211
|
+
end
|
|
212
|
+
|
|
143
213
|
def validate_tabs!
|
|
144
214
|
tabs = @values["tabs"]
|
|
145
215
|
raise Error, "tabs must be an object" unless tabs.is_a?(Hash)
|
|
@@ -158,9 +228,10 @@ module Canopus
|
|
|
158
228
|
(terminal["shell"].is_a?(Array) && terminal["shell"].all? { |value| value.is_a?(String) })
|
|
159
229
|
raise Error, "invalid terminal.working_directory" unless terminal["working_directory"].is_a?(String)
|
|
160
230
|
raise Error, "invalid terminal.env" unless terminal["env"].is_a?(Hash) && terminal["env"].all? { |key, value| key.is_a?(String) && (value.nil? || value.is_a?(String)) }
|
|
231
|
+
validate_terminal_profiles!(terminal)
|
|
161
232
|
raise Error, "invalid terminal.font_size" unless terminal["font_size"].nil? || terminal["font_size"].is_a?(Numeric) && terminal["font_size"].between?(6, 96)
|
|
162
233
|
raise Error, "invalid terminal.line_height" unless terminal["line_height"].is_a?(Numeric) && terminal["line_height"].between?(0.5, 4)
|
|
163
|
-
%w[confirm_close_running confirm_multiline_paste copy_on_select restore_on_startup hide_when_empty].each do |key|
|
|
234
|
+
%w[confirm_close_running confirm_multiline_paste copy_on_select restore_on_startup hide_when_empty shell_integration].each do |key|
|
|
164
235
|
raise Error, "terminal.#{key} must be true or false" unless [true, false].include?(terminal[key])
|
|
165
236
|
end
|
|
166
237
|
raise Error, "invalid terminal.close_on_exit" unless %w[never clean always].include?(terminal["close_on_exit"])
|
|
@@ -174,6 +245,214 @@ module Canopus
|
|
|
174
245
|
end
|
|
175
246
|
end
|
|
176
247
|
|
|
248
|
+
def validate_terminal_profiles!(terminal)
|
|
249
|
+
profiles = terminal["profiles"]
|
|
250
|
+
raise Error, "terminal.profiles must be an object" unless profiles.is_a?(Hash) && profiles.length <= 100
|
|
251
|
+
profiles.each do |name, profile|
|
|
252
|
+
valid_name = name.is_a?(String) && name.valid_encoding? && name.bytesize.between?(1, 128) && name == name.strip
|
|
253
|
+
raise Error, "invalid terminal profile name" unless valid_name
|
|
254
|
+
unless profile.is_a?(Hash) && profile.keys.all? { |key| %w[command path args env].include?(key) }
|
|
255
|
+
raise Error, "invalid terminal profile #{name}"
|
|
256
|
+
end
|
|
257
|
+
command = profile["command"]
|
|
258
|
+
path, args, env = profile.values_at("path", "args", "env")
|
|
259
|
+
valid_command = command.nil? || command.is_a?(String) && !command.empty? ||
|
|
260
|
+
command.is_a?(Array) && command.first.is_a?(String) && !command.first.empty? &&
|
|
261
|
+
command.all? { |value| value.is_a?(String) }
|
|
262
|
+
raise Error, "invalid terminal profile command #{name}" unless valid_command
|
|
263
|
+
raise Error, "terminal profile #{name} cannot use command with path or args" if command && (path || args)
|
|
264
|
+
raise Error, "terminal profile #{name} args require path" if args && !path
|
|
265
|
+
raise Error, "invalid terminal profile path #{name}" unless path.nil? || path.is_a?(String) && !path.empty?
|
|
266
|
+
raise Error, "invalid terminal profile args #{name}" unless args.nil? || args.is_a?(Array) && args.all? { |value| value.is_a?(String) }
|
|
267
|
+
valid_env = env.nil? || env.is_a?(Hash) && env.all? { |key, value| key.is_a?(String) && (value.nil? || value.is_a?(String)) }
|
|
268
|
+
raise Error, "invalid terminal profile env #{name}" unless valid_env
|
|
269
|
+
end
|
|
270
|
+
default = terminal["default_profile"]
|
|
271
|
+
unless default.nil? || default.is_a?(String) && profiles.key?(default)
|
|
272
|
+
raise Error, "terminal.default_profile must name a configured profile"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def validate_git!
|
|
277
|
+
git = @values["git"]
|
|
278
|
+
raise Error, "git must be an object" unless git.is_a?(Hash)
|
|
279
|
+
raise Error, "invalid git.inline_blame" unless %w[off cursor all].include?(git["inline_blame"])
|
|
280
|
+
raise Error, "git.autofetch must be true or false" unless [true, false].include?(git["autofetch"])
|
|
281
|
+
interval = git["autofetch_interval"]
|
|
282
|
+
raise Error, "invalid git.autofetch_interval" unless interval.is_a?(Integer) && interval.between?(10, 86_400)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def validate_recovery!
|
|
286
|
+
recovery = @values["recovery"]
|
|
287
|
+
raise Error, "recovery must be an object" unless recovery.is_a?(Hash)
|
|
288
|
+
raise Error, "recovery.enabled must be true or false" unless [true, false].include?(recovery["enabled"])
|
|
289
|
+
interval = recovery["interval"]
|
|
290
|
+
raise Error, "invalid recovery.interval" unless interval.is_a?(Integer) && interval.between?(100, 3_600_000)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def validate_plugins!
|
|
294
|
+
plugins = @values["plugins"]
|
|
295
|
+
raise Error, "plugins must be an object" unless plugins.is_a?(Hash)
|
|
296
|
+
raise Error, "invalid plugins.sandbox" unless %w[off auto required].include?(plugins["sandbox"])
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def validate_diagnostics!
|
|
300
|
+
diagnostics = @values["diagnostics"]
|
|
301
|
+
raise Error, "diagnostics must be an object" unless diagnostics.is_a?(Hash)
|
|
302
|
+
raise Error, "diagnostics.inline must be true or false" unless [true, false].include?(diagnostics["inline"])
|
|
303
|
+
length = diagnostics["inline_max_length"]
|
|
304
|
+
raise Error, "invalid diagnostics.inline_max_length" unless length.is_a?(Integer) && length.between?(1, 10_000)
|
|
305
|
+
unless %w[error warning information hint].include?(diagnostics["severity"])
|
|
306
|
+
raise Error, "invalid diagnostics.severity"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def validate_inlay_hints!
|
|
311
|
+
hints = @values["inlay_hints"]
|
|
312
|
+
raise Error, "inlay_hints must be an object" unless hints.is_a?(Hash)
|
|
313
|
+
%w[enabled parameter_names types].each do |key|
|
|
314
|
+
raise Error, "inlay_hints.#{key} must be true or false" unless [true, false].include?(hints[key])
|
|
315
|
+
end
|
|
316
|
+
length = hints["max_length"]
|
|
317
|
+
raise Error, "invalid inlay_hints.max_length" unless length.is_a?(Integer) && length.between?(1, 10_000)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def validate_code_lens!
|
|
321
|
+
lens = @values["code_lens"]
|
|
322
|
+
raise Error, "code_lens must be an object" unless lens.is_a?(Hash)
|
|
323
|
+
raise Error, "code_lens.enabled must be true or false" unless [true, false].include?(lens["enabled"])
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def validate_structural_guides!
|
|
327
|
+
unless [true, false].include?(@values["bracket_colorization"])
|
|
328
|
+
raise Error, "bracket_colorization must be true or false"
|
|
329
|
+
end
|
|
330
|
+
guides = @values["indent_guides"]
|
|
331
|
+
raise Error, "indent_guides must be an object" unless guides.is_a?(Hash)
|
|
332
|
+
%w[enabled active].each do |key|
|
|
333
|
+
raise Error, "indent_guides.#{key} must be true or false" unless [true, false].include?(guides[key])
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def validate_sticky_scroll!
|
|
338
|
+
sticky = @values["sticky_scroll"]
|
|
339
|
+
raise Error, "sticky_scroll must be an object" unless sticky.is_a?(Hash)
|
|
340
|
+
raise Error, "sticky_scroll.enabled must be true or false" unless [true, false].include?(sticky["enabled"])
|
|
341
|
+
unless sticky["max_lines"].is_a?(Integer) && sticky["max_lines"].between?(1, 20)
|
|
342
|
+
raise Error, "invalid sticky_scroll.max_lines"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def validate_breadcrumbs!
|
|
347
|
+
breadcrumbs = @values["breadcrumbs"]
|
|
348
|
+
unless breadcrumbs.is_a?(Hash) && [true, false].include?(breadcrumbs["enabled"])
|
|
349
|
+
raise Error, "breadcrumbs.enabled must be true or false"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def validate_minimap!
|
|
354
|
+
minimap = @values["minimap"]
|
|
355
|
+
raise Error, "minimap must be an object" unless minimap.is_a?(Hash)
|
|
356
|
+
%w[enabled show_diagnostics].each do |key|
|
|
357
|
+
raise Error, "minimap.#{key} must be true or false" unless [true, false].include?(minimap[key])
|
|
358
|
+
end
|
|
359
|
+
raise Error, "invalid minimap.width" unless minimap["width"].is_a?(Integer) && minimap["width"].between?(40, 400)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def validate_auto_save!
|
|
363
|
+
raise Error, "invalid auto_save" unless %w[off after_delay on_focus_change].include?(@values["auto_save"])
|
|
364
|
+
delay = @values["auto_save_delay"]
|
|
365
|
+
raise Error, "invalid auto_save_delay" unless delay.is_a?(Integer) && delay.between?(100, 3_600_000)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def validate_editorconfig!
|
|
369
|
+
raise Error, "editorconfig must be true or false" unless [true, false].include?(@values["editorconfig"])
|
|
370
|
+
%w[trim_trailing_whitespace insert_final_newline].each do |key|
|
|
371
|
+
value = @values[key]
|
|
372
|
+
raise Error, "#{key} must be true, false, or null" unless value.nil? || [true, false].include?(value)
|
|
373
|
+
end
|
|
374
|
+
length = @values["max_line_length"]
|
|
375
|
+
raise Error, "invalid max_line_length" unless length.nil? || length.is_a?(Integer) && length.between?(1, 1_000_000)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def validate_persistent_undo!
|
|
379
|
+
persistent = @values["persistent_undo"]
|
|
380
|
+
raise Error, "persistent_undo must be an object" unless persistent.is_a?(Hash)
|
|
381
|
+
raise Error, "persistent_undo.enabled must be true or false" unless [true, false].include?(persistent["enabled"])
|
|
382
|
+
max_entries = persistent["max_entries"]
|
|
383
|
+
raise Error, "invalid persistent_undo.max_entries" unless max_entries.is_a?(Integer) && max_entries.between?(1, 10_000)
|
|
384
|
+
expire_days = persistent["expire_days"]
|
|
385
|
+
raise Error, "invalid persistent_undo.expire_days" unless expire_days.is_a?(Integer) && expire_days.between?(1, 3_650)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def validate_save_actions!
|
|
389
|
+
actions = @values["code_actions_on_save"]
|
|
390
|
+
valid = actions.is_a?(Array) && actions.length <= 64 && actions.uniq.length == actions.length && actions.all? do |action|
|
|
391
|
+
action.is_a?(String) && action.valid_encoding? && action.bytesize.between?(1, 256) &&
|
|
392
|
+
action == action.strip && !action.match?(/[\x00-\x1f\x7f]/)
|
|
393
|
+
end
|
|
394
|
+
raise Error, "invalid code_actions_on_save" unless valid
|
|
395
|
+
@values["code_actions_on_save"] = actions.map { |action| action.dup.freeze }.freeze
|
|
396
|
+
timeout = @values["format_on_save_timeout"]
|
|
397
|
+
raise Error, "invalid format_on_save_timeout" unless timeout.is_a?(Integer) && timeout.between?(1, 60_000)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def snapshot_language_servers!
|
|
401
|
+
snapshot = JSON.parse(JSON.generate(@values["language_servers"]))
|
|
402
|
+
freeze_value = lambda do |value|
|
|
403
|
+
value.each { |key, child| key.freeze; freeze_value.call(child) } if value.is_a?(Hash)
|
|
404
|
+
value.each { |child| freeze_value.call(child) } if value.is_a?(Array)
|
|
405
|
+
value.freeze
|
|
406
|
+
end
|
|
407
|
+
@values["language_servers"] = freeze_value.call(snapshot)
|
|
408
|
+
rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError
|
|
409
|
+
raise Error, "invalid language_servers"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def validate_language_server_keys!
|
|
413
|
+
@values["language_servers"].each do |name, value|
|
|
414
|
+
raise Error, "language server names must be strings" unless name.is_a?(String)
|
|
415
|
+
options = value.is_a?(Array) ? value.select { |item| item.is_a?(Hash) } : value.is_a?(Hash) ? [value] : []
|
|
416
|
+
options.each do |option|
|
|
417
|
+
raise Error, "language server option keys must be strings" unless option.keys.all? { |key| key.is_a?(String) }
|
|
418
|
+
env = option["env"]
|
|
419
|
+
raise Error, "language server env keys must be strings" if env.is_a?(Hash) && !env.keys.all? { |key| key.is_a?(String) }
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def validate_debug_adapters!
|
|
425
|
+
adapters = @values["debug_adapters"]
|
|
426
|
+
raise Error, "debug_adapters must be an object of at most 64 adapters" unless adapters.is_a?(Hash) && adapters.length <= 64
|
|
427
|
+
adapters.each do |type, options|
|
|
428
|
+
valid_type = type.is_a?(String) && type.valid_encoding? && type.bytesize.between?(1, 128) &&
|
|
429
|
+
type.match?(/\A[A-Za-z0-9_.-]+\z/)
|
|
430
|
+
raise Error, "invalid debug adapter type" unless valid_type
|
|
431
|
+
unless options.is_a?(Hash) && options.keys.all? { |key| key.is_a?(String) } &&
|
|
432
|
+
(options.keys - %w[command transport]).empty?
|
|
433
|
+
raise Error, "invalid debug adapter options for #{type}"
|
|
434
|
+
end
|
|
435
|
+
command = options["command"]
|
|
436
|
+
valid_command = command.is_a?(Array) && command.length.between?(1, 32) && command.all? do |argument|
|
|
437
|
+
argument.is_a?(String) && argument.valid_encoding? && argument.bytesize.between?(1, 4096) &&
|
|
438
|
+
!argument.match?(/[\x00-\x1f\x7f]/)
|
|
439
|
+
end
|
|
440
|
+
raise Error, "invalid debug adapter command for #{type}" unless valid_command
|
|
441
|
+
raise Error, "invalid debug adapter transport for #{type}" unless %w[stdio tcp].include?(options["transport"])
|
|
442
|
+
end
|
|
443
|
+
serialized = JSON.generate(adapters)
|
|
444
|
+
raise Error, "debug_adapters exceeds 1 MiB" if serialized.bytesize > 1_048_576
|
|
445
|
+
snapshot = JSON.parse(serialized)
|
|
446
|
+
freeze_value = lambda do |value|
|
|
447
|
+
value.each { |key, child| key.freeze; freeze_value.call(child) } if value.is_a?(Hash)
|
|
448
|
+
value.each { |child| freeze_value.call(child) } if value.is_a?(Array)
|
|
449
|
+
value.freeze
|
|
450
|
+
end
|
|
451
|
+
@values["debug_adapters"] = freeze_value.call(snapshot)
|
|
452
|
+
rescue JSON::GeneratorError, JSON::ParserError, JSON::NestingError
|
|
453
|
+
raise Error, "invalid debug_adapters"
|
|
454
|
+
end
|
|
455
|
+
|
|
177
456
|
def validate_dock!
|
|
178
457
|
dock = @values["dock"]
|
|
179
458
|
raise Error, "dock must be an object" unless dock.is_a?(Hash)
|