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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Rakefile +8 -0
  4. data/exe/sv +6 -0
  5. data/lib/sorbet_view/cli/runner.rb +206 -0
  6. data/lib/sorbet_view/compiler/adapters/erb_adapter.rb +170 -0
  7. data/lib/sorbet_view/compiler/component_compiler.rb +48 -0
  8. data/lib/sorbet_view/compiler/heredoc_extractor.rb +112 -0
  9. data/lib/sorbet_view/compiler/parser_adapter.rb +16 -0
  10. data/lib/sorbet_view/compiler/ruby_generator.rb +211 -0
  11. data/lib/sorbet_view/compiler/ruby_segment.rb +13 -0
  12. data/lib/sorbet_view/compiler/template_compiler.rb +41 -0
  13. data/lib/sorbet_view/compiler/template_context.rb +170 -0
  14. data/lib/sorbet_view/configuration.rb +34 -0
  15. data/lib/sorbet_view/file_system/file_watcher.rb +61 -0
  16. data/lib/sorbet_view/file_system/output_manager.rb +40 -0
  17. data/lib/sorbet_view/file_system/project_scanner.rb +34 -0
  18. data/lib/sorbet_view/lsp/document_store.rb +56 -0
  19. data/lib/sorbet_view/lsp/position_translator.rb +95 -0
  20. data/lib/sorbet_view/lsp/server.rb +607 -0
  21. data/lib/sorbet_view/lsp/sorbet_process.rb +164 -0
  22. data/lib/sorbet_view/lsp/transport.rb +89 -0
  23. data/lib/sorbet_view/lsp/uri_mapper.rb +105 -0
  24. data/lib/sorbet_view/perf.rb +92 -0
  25. data/lib/sorbet_view/source_map/mapping_entry.rb +12 -0
  26. data/lib/sorbet_view/source_map/position.rb +23 -0
  27. data/lib/sorbet_view/source_map/range.rb +35 -0
  28. data/lib/sorbet_view/source_map/source_map.rb +103 -0
  29. data/lib/sorbet_view/version.rb +6 -0
  30. data/lib/sorbet_view.rb +36 -0
  31. data/lib/tapioca/dsl/compilers/sorbet_view.rb +265 -0
  32. data/sorbet_view.gemspec +34 -0
  33. data/vscode/.gitignore +3 -0
  34. data/vscode/.vscode/launch.json +12 -0
  35. data/vscode/.vscodeignore +4 -0
  36. data/vscode/package-lock.json +140 -0
  37. data/vscode/package.json +61 -0
  38. data/vscode/src/extension.ts +88 -0
  39. data/vscode/tsconfig.json +17 -0
  40. 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
@@ -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,3 @@
1
+ node_modules/
2
+ dist/
3
+ *.vsix
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Run Extension",
6
+ "type": "extensionHost",
7
+ "request": "launch",
8
+ "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
9
+ "outFiles": ["${workspaceFolder}/dist/**/*.js"]
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,4 @@
1
+ src/
2
+ node_modules/
3
+ tsconfig.json
4
+ .gitignore
@@ -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
+ }
@@ -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
+ }