sorbet_view 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/Rakefile +8 -0
- data/exe/sv +6 -0
- data/lib/sorbet_view/cli/runner.rb +206 -0
- data/lib/sorbet_view/compiler/adapters/erb_adapter.rb +170 -0
- data/lib/sorbet_view/compiler/component_compiler.rb +48 -0
- data/lib/sorbet_view/compiler/heredoc_extractor.rb +112 -0
- data/lib/sorbet_view/compiler/parser_adapter.rb +16 -0
- data/lib/sorbet_view/compiler/ruby_generator.rb +211 -0
- data/lib/sorbet_view/compiler/ruby_segment.rb +13 -0
- data/lib/sorbet_view/compiler/template_compiler.rb +41 -0
- data/lib/sorbet_view/compiler/template_context.rb +170 -0
- data/lib/sorbet_view/configuration.rb +34 -0
- data/lib/sorbet_view/file_system/file_watcher.rb +61 -0
- data/lib/sorbet_view/file_system/output_manager.rb +40 -0
- data/lib/sorbet_view/file_system/project_scanner.rb +34 -0
- data/lib/sorbet_view/lsp/document_store.rb +56 -0
- data/lib/sorbet_view/lsp/position_translator.rb +95 -0
- data/lib/sorbet_view/lsp/server.rb +607 -0
- data/lib/sorbet_view/lsp/sorbet_process.rb +164 -0
- data/lib/sorbet_view/lsp/transport.rb +89 -0
- data/lib/sorbet_view/lsp/uri_mapper.rb +105 -0
- data/lib/sorbet_view/perf.rb +92 -0
- data/lib/sorbet_view/source_map/mapping_entry.rb +12 -0
- data/lib/sorbet_view/source_map/position.rb +23 -0
- data/lib/sorbet_view/source_map/range.rb +35 -0
- data/lib/sorbet_view/source_map/source_map.rb +103 -0
- data/lib/sorbet_view/version.rb +6 -0
- data/lib/sorbet_view.rb +36 -0
- data/lib/tapioca/dsl/compilers/sorbet_view.rb +265 -0
- data/sorbet_view.gemspec +34 -0
- data/vscode/.gitignore +3 -0
- data/vscode/.vscode/launch.json +12 -0
- data/vscode/.vscodeignore +4 -0
- data/vscode/package-lock.json +140 -0
- data/vscode/package.json +61 -0
- data/vscode/src/extension.ts +88 -0
- data/vscode/tsconfig.json +17 -0
- metadata +151 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(Tapioca::Dsl::Compiler)
|
|
5
|
+
|
|
6
|
+
require 'srb_lens'
|
|
7
|
+
require 'json'
|
|
8
|
+
|
|
9
|
+
module Tapioca
|
|
10
|
+
module Dsl
|
|
11
|
+
module Compilers
|
|
12
|
+
class SorbetView < Compiler
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
ConstantType = type_member { { fixed: T.class_of(::SorbetView) } }
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
sig { override.returns(T::Enumerable[Module]) }
|
|
21
|
+
def gather_constants
|
|
22
|
+
[::SorbetView]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.void }
|
|
27
|
+
def decorate
|
|
28
|
+
@module_cache = T.let({}, T::Hash[String, RBI::Scope])
|
|
29
|
+
@project = T.let(SrbLens::Project.load_or_index(Dir.pwd), T.untyped)
|
|
30
|
+
|
|
31
|
+
controllers = ObjectSpace.each_object(Class).select do |klass|
|
|
32
|
+
klass < ::ActionController::Base && !klass.abstract?
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
controllers.each do |controller|
|
|
38
|
+
process_controller(controller)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
generate_ivar_mapping(controllers)
|
|
42
|
+
process_components
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Generate a mapping of controller_path -> defined instance variables
|
|
48
|
+
# Used by the compiler to declare undefined ivars as NilClass
|
|
49
|
+
sig { params(controllers: T::Array[T.untyped]).void }
|
|
50
|
+
def generate_ivar_mapping(controllers)
|
|
51
|
+
config = ::SorbetView::Configuration.load
|
|
52
|
+
mapping = T.let({}, T::Hash[String, T::Array[String]])
|
|
53
|
+
|
|
54
|
+
controllers.each do |controller|
|
|
55
|
+
path = controller.controller_path
|
|
56
|
+
ivars = extract_defined_ivars(controller)
|
|
57
|
+
mapping[path] = ivars unless ivars.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
mapping_path = File.join(config.output_dir, '.defined_ivars.json')
|
|
61
|
+
FileUtils.mkdir_p(File.dirname(mapping_path))
|
|
62
|
+
File.write(mapping_path, JSON.pretty_generate(mapping))
|
|
63
|
+
rescue StandardError
|
|
64
|
+
# Non-critical: if mapping fails, all ivars default to NilClass
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Extract instance variables assigned in the controller source
|
|
68
|
+
sig { params(controller: T.untyped).returns(T::Array[String]) }
|
|
69
|
+
def extract_defined_ivars(controller)
|
|
70
|
+
source_file = "app/controllers/#{controller.controller_path}_controller.rb"
|
|
71
|
+
return [] unless File.exist?(source_file)
|
|
72
|
+
|
|
73
|
+
source = File.read(source_file)
|
|
74
|
+
source.scan(/(?<!@)@([a-zA-Z_]\w*)\s*(?:=|\|\|=)/).map { |m| "@#{m[0]}" }.uniq.sort
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { void }
|
|
78
|
+
def process_components
|
|
79
|
+
config = ::SorbetView::Configuration.load
|
|
80
|
+
return if config.component_dirs.empty?
|
|
81
|
+
|
|
82
|
+
component_files = ::SorbetView::FileSystem::ProjectScanner.scan_components(config)
|
|
83
|
+
|
|
84
|
+
component_files.each do |path|
|
|
85
|
+
source = File.read(path)
|
|
86
|
+
extractions = ::SorbetView::Compiler::HeredocExtractor.extract(source, path)
|
|
87
|
+
next if extractions.empty?
|
|
88
|
+
|
|
89
|
+
class_name = T.must(extractions.first).class_name
|
|
90
|
+
next if class_name.empty?
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
klass = Object.const_get(class_name)
|
|
94
|
+
rescue NameError
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
generate_component_rbi(klass, class_name)
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError
|
|
101
|
+
# Configuration may not be available in all environments
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig { params(klass: T.untyped, class_name: String).void }
|
|
105
|
+
def generate_component_rbi(klass, class_name)
|
|
106
|
+
methods = klass.instance_methods(false)
|
|
107
|
+
|
|
108
|
+
method_sigs = methods.filter_map do |method_name|
|
|
109
|
+
method_name_s = method_name.to_s
|
|
110
|
+
next unless method_name_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
|
|
111
|
+
|
|
112
|
+
method_info = find_method_info(klass, method_name_s)
|
|
113
|
+
next unless method_info
|
|
114
|
+
|
|
115
|
+
return_type = method_info&.return_type || ''
|
|
116
|
+
next if return_type.empty? || return_type == 'T.untyped'
|
|
117
|
+
|
|
118
|
+
params = build_params_from_srb_lens(method_info)
|
|
119
|
+
[method_name_s, params, return_type]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return if method_sigs.empty?
|
|
123
|
+
|
|
124
|
+
create_class_from_path(class_name) do |klass_rbi|
|
|
125
|
+
method_sigs.each do |method_name, params, return_type|
|
|
126
|
+
klass_rbi.create_method(method_name, parameters: params, return_type: return_type)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
sig { params(controller: T.untyped).void }
|
|
132
|
+
def process_controller(controller)
|
|
133
|
+
helper_methods = extract_helper_methods(controller)
|
|
134
|
+
return if helper_methods.empty?
|
|
135
|
+
|
|
136
|
+
path = controller.controller_path
|
|
137
|
+
parts = path.split('/').map { |p| camelize(p) }
|
|
138
|
+
|
|
139
|
+
# 1) module SorbetView::Helpers::<controller_path> with helper methods
|
|
140
|
+
helper_module_name = "SorbetView::Helpers::#{parts.join('::')}"
|
|
141
|
+
create_module_from_path(helper_module_name) do |mod|
|
|
142
|
+
helper_methods.each do |method_name, params, return_type|
|
|
143
|
+
mod.create_method(method_name, parameters: params, return_type: return_type)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# 2) Each action's template class includes the helper module
|
|
148
|
+
actions = controller.action_methods.to_a
|
|
149
|
+
actions.each do |action_name|
|
|
150
|
+
next unless action_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
151
|
+
|
|
152
|
+
class_name = "SorbetView::Generated::#{parts.join('::')}::#{camelize(action_name)}"
|
|
153
|
+
create_class_from_path(class_name) do |klass|
|
|
154
|
+
klass.create_include(helper_module_name)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Returns [[method_name, params, return_type], ...]
|
|
160
|
+
sig { params(controller: T.untyped).returns(T::Array[T.untyped]) }
|
|
161
|
+
def extract_helper_methods(controller)
|
|
162
|
+
return [] unless controller.respond_to?(:_helper_methods)
|
|
163
|
+
|
|
164
|
+
controller._helper_methods.filter_map do |name|
|
|
165
|
+
name_s = name.to_s
|
|
166
|
+
next unless name_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
|
|
167
|
+
|
|
168
|
+
method_info = find_method_info(controller, name_s)
|
|
169
|
+
params = if method_info
|
|
170
|
+
build_params_from_srb_lens(method_info)
|
|
171
|
+
else
|
|
172
|
+
build_params_from_reflection(controller, name_s)
|
|
173
|
+
end
|
|
174
|
+
return_type = method_info&.return_type || 'T.untyped'
|
|
175
|
+
return_type = 'T.untyped' if return_type.empty?
|
|
176
|
+
|
|
177
|
+
[name_s, params, return_type]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sig { params(controller: T.untyped, method_name: String).returns(T.untyped) }
|
|
182
|
+
def find_method_info(controller, method_name)
|
|
183
|
+
methods = @project.find_methods("#{controller.name}##{method_name}")
|
|
184
|
+
methods&.first
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
sig { params(method_info: T.untyped).returns(T::Array[RBI::Param]) }
|
|
188
|
+
def build_params_from_srb_lens(method_info)
|
|
189
|
+
method_info.arguments.filter_map do |arg|
|
|
190
|
+
name = arg.name.gsub(/\A[&*]+/, '') # strip *, **, & prefixes
|
|
191
|
+
type = arg.type || 'T.untyped'
|
|
192
|
+
type = 'T.untyped' if type.empty?
|
|
193
|
+
|
|
194
|
+
case arg.kind
|
|
195
|
+
when 'req' then create_param(name, type: type)
|
|
196
|
+
when 'opt' then create_opt_param(name, type: type, default: 'T.unsafe(nil)')
|
|
197
|
+
when 'rest' then create_rest_param(name.empty? ? 'args' : name, type: type)
|
|
198
|
+
when 'key_req', 'keyreq' then create_kw_param(name, type: type)
|
|
199
|
+
when 'key' then create_kw_opt_param(name, type: type, default: 'T.unsafe(nil)')
|
|
200
|
+
when 'key_rest', 'keyrest' then create_kw_rest_param(name.empty? ? 'kwargs' : name, type: type)
|
|
201
|
+
when 'block' then create_block_param(name.empty? ? 'blk' : name, type: type)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
sig { params(controller: T.untyped, method_name: String).returns(T::Array[RBI::Param]) }
|
|
207
|
+
def build_params_from_reflection(controller, method_name)
|
|
208
|
+
method_obj = controller.instance_method(method_name)
|
|
209
|
+
method_obj.parameters.filter_map do |type, name|
|
|
210
|
+
param_name = name ? name.to_s.gsub(/\A[&*]+/, '') : nil
|
|
211
|
+
case type
|
|
212
|
+
when :req then create_param(param_name || 'arg', type: 'T.untyped')
|
|
213
|
+
when :opt then create_opt_param(param_name || 'arg', type: 'T.untyped', default: 'T.unsafe(nil)')
|
|
214
|
+
when :rest then create_rest_param(param_name.nil? || param_name.empty? ? 'args' : param_name, type: 'T.untyped')
|
|
215
|
+
when :keyreq then create_kw_param(param_name || 'arg', type: 'T.untyped')
|
|
216
|
+
when :key then create_kw_opt_param(param_name || 'arg', type: 'T.untyped', default: 'T.unsafe(nil)')
|
|
217
|
+
when :keyrest then create_kw_rest_param(param_name.nil? || param_name.empty? ? 'kwargs' : param_name, type: 'T.untyped')
|
|
218
|
+
when :block then create_block_param(param_name.nil? || param_name.empty? ? 'blk' : param_name, type: 'T.untyped')
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
rescue NameError
|
|
222
|
+
[]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
sig { params(str: String).returns(String) }
|
|
226
|
+
def camelize(str)
|
|
227
|
+
str.split('_').map(&:capitalize).join
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
sig { params(class_name: String, block: T.proc.params(klass: T.untyped).void).void }
|
|
231
|
+
def create_class_from_path(class_name, &block)
|
|
232
|
+
parts = class_name.split('::')
|
|
233
|
+
current = resolve_module_path(parts[0..-2])
|
|
234
|
+
current.create_class(T.must(parts.last), &block)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
sig { params(module_name: String, block: T.proc.params(mod: T.untyped).void).void }
|
|
238
|
+
def create_module_from_path(module_name, &block)
|
|
239
|
+
parts = module_name.split('::')
|
|
240
|
+
current = resolve_module_path(parts[0..-2])
|
|
241
|
+
mod = current.create_module(T.must(parts.last))
|
|
242
|
+
block.call(mod)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
sig { params(parts: T::Array[String]).returns(RBI::Scope) }
|
|
246
|
+
def resolve_module_path(parts)
|
|
247
|
+
current = root
|
|
248
|
+
path_so_far = +''
|
|
249
|
+
|
|
250
|
+
parts.each do |part|
|
|
251
|
+
path_so_far = path_so_far.empty? ? part : "#{path_so_far}::#{part}"
|
|
252
|
+
if T.must(@module_cache).key?(path_so_far)
|
|
253
|
+
current = T.must(T.must(@module_cache)[path_so_far])
|
|
254
|
+
else
|
|
255
|
+
current = current.create_module(part)
|
|
256
|
+
T.must(@module_cache)[path_so_far] = current
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
current
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
data/sorbet_view.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/sorbet_view/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'sorbet_view'
|
|
7
|
+
spec.version = SorbetView::VERSION
|
|
8
|
+
spec.authors = ['kazuma']
|
|
9
|
+
spec.summary = 'Sorbet type-checking for Rails view templates'
|
|
10
|
+
spec.description = 'Extracts Ruby code from view templates (ERB, etc.) for Sorbet type-checking, with LSP support'
|
|
11
|
+
spec.homepage = 'https://github.com/user/sorbet_view'
|
|
12
|
+
spec.license = 'MIT'
|
|
13
|
+
spec.required_ruby_version = '>= 3.2'
|
|
14
|
+
|
|
15
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
17
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
|
20
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
21
|
+
(File.expand_path(f) == __FILE__) ||
|
|
22
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github Gemfile])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
spec.bindir = 'exe'
|
|
26
|
+
spec.executables = ['sv']
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
|
|
29
|
+
spec.add_dependency 'herb'
|
|
30
|
+
spec.add_dependency 'listen', '~> 3.0'
|
|
31
|
+
spec.add_dependency 'psych'
|
|
32
|
+
spec.add_dependency 'sorbet-runtime'
|
|
33
|
+
spec.add_dependency 'srb_lens'
|
|
34
|
+
end
|
data/vscode/.gitignore
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sorbet-view",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "sorbet-view",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"vscode-languageclient": "^9.0.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^20.0.0",
|
|
16
|
+
"@types/vscode": "^1.91.0",
|
|
17
|
+
"typescript": "^5.3.0"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"vscode": "^1.91.0"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"node_modules/@types/node": {
|
|
24
|
+
"version": "20.19.37",
|
|
25
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
|
26
|
+
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
|
27
|
+
"dev": true,
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"undici-types": "~6.21.0"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"node_modules/@types/vscode": {
|
|
34
|
+
"version": "1.109.0",
|
|
35
|
+
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz",
|
|
36
|
+
"integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==",
|
|
37
|
+
"dev": true,
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
},
|
|
40
|
+
"node_modules/balanced-match": {
|
|
41
|
+
"version": "1.0.2",
|
|
42
|
+
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
|
43
|
+
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
},
|
|
46
|
+
"node_modules/brace-expansion": {
|
|
47
|
+
"version": "2.0.2",
|
|
48
|
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
49
|
+
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"balanced-match": "^1.0.0"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"node_modules/minimatch": {
|
|
56
|
+
"version": "5.1.9",
|
|
57
|
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
|
58
|
+
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
|
59
|
+
"license": "ISC",
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"brace-expansion": "^2.0.1"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=10"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"node_modules/semver": {
|
|
68
|
+
"version": "7.7.4",
|
|
69
|
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
|
70
|
+
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
|
71
|
+
"license": "ISC",
|
|
72
|
+
"bin": {
|
|
73
|
+
"semver": "bin/semver.js"
|
|
74
|
+
},
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=10"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"node_modules/typescript": {
|
|
80
|
+
"version": "5.9.3",
|
|
81
|
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
82
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
83
|
+
"dev": true,
|
|
84
|
+
"license": "Apache-2.0",
|
|
85
|
+
"bin": {
|
|
86
|
+
"tsc": "bin/tsc",
|
|
87
|
+
"tsserver": "bin/tsserver"
|
|
88
|
+
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=14.17"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"node_modules/undici-types": {
|
|
94
|
+
"version": "6.21.0",
|
|
95
|
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
96
|
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
97
|
+
"dev": true,
|
|
98
|
+
"license": "MIT"
|
|
99
|
+
},
|
|
100
|
+
"node_modules/vscode-jsonrpc": {
|
|
101
|
+
"version": "8.2.0",
|
|
102
|
+
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
|
103
|
+
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
|
104
|
+
"license": "MIT",
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": ">=14.0.0"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"node_modules/vscode-languageclient": {
|
|
110
|
+
"version": "9.0.1",
|
|
111
|
+
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
|
|
112
|
+
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
|
|
113
|
+
"license": "MIT",
|
|
114
|
+
"dependencies": {
|
|
115
|
+
"minimatch": "^5.1.0",
|
|
116
|
+
"semver": "^7.3.7",
|
|
117
|
+
"vscode-languageserver-protocol": "3.17.5"
|
|
118
|
+
},
|
|
119
|
+
"engines": {
|
|
120
|
+
"vscode": "^1.82.0"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"node_modules/vscode-languageserver-protocol": {
|
|
124
|
+
"version": "3.17.5",
|
|
125
|
+
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
|
126
|
+
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
|
127
|
+
"license": "MIT",
|
|
128
|
+
"dependencies": {
|
|
129
|
+
"vscode-jsonrpc": "8.2.0",
|
|
130
|
+
"vscode-languageserver-types": "3.17.5"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"node_modules/vscode-languageserver-types": {
|
|
134
|
+
"version": "3.17.5",
|
|
135
|
+
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
|
136
|
+
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
|
137
|
+
"license": "MIT"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
data/vscode/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sorbet-view",
|
|
3
|
+
"displayName": "Sorbet View",
|
|
4
|
+
"description": "ERB template type-checking and LSP support via Sorbet",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"publisher": "sorbet-view",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"vscode": "^1.91.0"
|
|
10
|
+
},
|
|
11
|
+
"categories": [
|
|
12
|
+
"Programming Languages",
|
|
13
|
+
"Linters"
|
|
14
|
+
],
|
|
15
|
+
"activationEvents": [
|
|
16
|
+
"onLanguage:erb",
|
|
17
|
+
"onLanguage:ruby"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/extension.js",
|
|
20
|
+
"contributes": {
|
|
21
|
+
"configuration": {
|
|
22
|
+
"title": "Sorbet View",
|
|
23
|
+
"properties": {
|
|
24
|
+
"sorbetView.command": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"default": "bundle exec sv lsp",
|
|
27
|
+
"description": "Command to start the LSP server"
|
|
28
|
+
},
|
|
29
|
+
"sorbetView.trace.server": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": [
|
|
32
|
+
"off",
|
|
33
|
+
"messages",
|
|
34
|
+
"verbose"
|
|
35
|
+
],
|
|
36
|
+
"default": "off",
|
|
37
|
+
"description": "Trace LSP communication (for debugging)"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"commands": [
|
|
42
|
+
{
|
|
43
|
+
"command": "sorbetView.restart",
|
|
44
|
+
"title": "Sorbet View: Restart Server"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p ./",
|
|
50
|
+
"watch": "tsc -watch -p ./",
|
|
51
|
+
"vscode:prepublish": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"vscode-languageclient": "^9.0.1"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/vscode": "^1.91.0",
|
|
58
|
+
"@types/node": "^20.0.0",
|
|
59
|
+
"typescript": "^5.3.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as vscode from "vscode";
|
|
2
|
+
import {
|
|
3
|
+
LanguageClient,
|
|
4
|
+
LanguageClientOptions,
|
|
5
|
+
ServerOptions,
|
|
6
|
+
} from "vscode-languageclient/node";
|
|
7
|
+
|
|
8
|
+
let client: LanguageClient | undefined;
|
|
9
|
+
let outputChannel: vscode.OutputChannel;
|
|
10
|
+
|
|
11
|
+
export async function activate(
|
|
12
|
+
context: vscode.ExtensionContext
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
outputChannel = vscode.window.createOutputChannel("Sorbet View");
|
|
15
|
+
|
|
16
|
+
await startClient();
|
|
17
|
+
|
|
18
|
+
context.subscriptions.push(
|
|
19
|
+
vscode.commands.registerCommand("sorbetView.restart", async () => {
|
|
20
|
+
outputChannel.appendLine("Restarting Sorbet View server...");
|
|
21
|
+
if (client) {
|
|
22
|
+
await client.restart();
|
|
23
|
+
} else {
|
|
24
|
+
await startClient();
|
|
25
|
+
}
|
|
26
|
+
outputChannel.appendLine("Sorbet View server restarted.");
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
context.subscriptions.push(
|
|
31
|
+
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
32
|
+
if (e.affectsConfiguration("sorbetView.command")) {
|
|
33
|
+
outputChannel.appendLine("Configuration changed, restarting server...");
|
|
34
|
+
if (client) {
|
|
35
|
+
await client.stop();
|
|
36
|
+
client = undefined;
|
|
37
|
+
}
|
|
38
|
+
await startClient();
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function startClient(): Promise<void> {
|
|
45
|
+
const config = vscode.workspace.getConfiguration("sorbetView");
|
|
46
|
+
const commandLine = config.get<string>("command") || "bundle exec sv lsp";
|
|
47
|
+
const parts = commandLine.split(/\s+/);
|
|
48
|
+
const command = parts[0];
|
|
49
|
+
const args = parts.slice(1);
|
|
50
|
+
|
|
51
|
+
outputChannel.appendLine(`Starting: ${commandLine}`);
|
|
52
|
+
|
|
53
|
+
const serverOptions: ServerOptions = {
|
|
54
|
+
command,
|
|
55
|
+
args,
|
|
56
|
+
options: { env: { ...process.env }, shell: true },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const clientOptions: LanguageClientOptions = {
|
|
60
|
+
documentSelector: [
|
|
61
|
+
{ scheme: "file", language: "erb" },
|
|
62
|
+
{ scheme: "file", language: "ruby" },
|
|
63
|
+
],
|
|
64
|
+
outputChannel,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
client = new LanguageClient(
|
|
68
|
+
"sorbetView",
|
|
69
|
+
"Sorbet View",
|
|
70
|
+
serverOptions,
|
|
71
|
+
clientOptions
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await client.start();
|
|
76
|
+
outputChannel.appendLine("Sorbet View server started.");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
outputChannel.appendLine(`Failed to start server: ${error}`);
|
|
79
|
+
client = undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function deactivate(): Promise<void> {
|
|
84
|
+
if (client) {
|
|
85
|
+
await client.stop();
|
|
86
|
+
client = undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "node"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|