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
@@ -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 search(pattern, **options, &block) = Search.new(self).call(pattern, **options, &block)
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, **options)
97
- expression = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern))
98
- searcher = Search.new(self)
99
- paths = searcher.call(expression, **options).map(&:path).uniq
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
- unless Regexp.method_defined?(:timeout)
4
- require "timeout"
3
+ require "timeout"
5
4
 
5
+ unless Regexp.method_defined?(:timeout)
6
6
  class Regexp
7
- TimeoutError = Class.new(StandardError)
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
@@ -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