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,164 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetView
|
|
5
|
+
module Lsp
|
|
6
|
+
# Manages Sorbet LSP as a child process
|
|
7
|
+
class SorbetProcess
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(T.nilable(Transport)) }
|
|
11
|
+
attr_reader :transport
|
|
12
|
+
|
|
13
|
+
sig { params(config: Configuration, logger: Logger).void }
|
|
14
|
+
def initialize(config:, logger:)
|
|
15
|
+
@config = config
|
|
16
|
+
@logger = logger
|
|
17
|
+
@process = T.let(nil, T.nilable(IO))
|
|
18
|
+
@pid = T.let(nil, T.nilable(Integer))
|
|
19
|
+
@transport = T.let(nil, T.nilable(Transport))
|
|
20
|
+
@read_thread = T.let(nil, T.nilable(Thread))
|
|
21
|
+
@notification_handlers = T.let({}, T::Hash[String, T.proc.params(msg: T::Hash[String, T.untyped]).void])
|
|
22
|
+
@pending_requests = T.let({}, T::Hash[T.untyped, Thread::Queue])
|
|
23
|
+
@pending_mutex = T.let(Mutex.new, Mutex)
|
|
24
|
+
@next_id = T.let(1, Integer)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { void }
|
|
28
|
+
def start
|
|
29
|
+
cmd = [
|
|
30
|
+
@config.sorbet_path,
|
|
31
|
+
'tc',
|
|
32
|
+
'--lsp',
|
|
33
|
+
'--enable-all-experimental-lsp-features'
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
@logger.info("Starting Sorbet: #{cmd.join(' ')}")
|
|
37
|
+
|
|
38
|
+
stdin_r, stdin_w = IO.pipe
|
|
39
|
+
stdout_r, stdout_w = IO.pipe
|
|
40
|
+
err_r, err_w = IO.pipe
|
|
41
|
+
|
|
42
|
+
@pid = Process.spawn(
|
|
43
|
+
*cmd,
|
|
44
|
+
in: stdin_r,
|
|
45
|
+
out: stdout_w,
|
|
46
|
+
err: err_w
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
stdin_r.close
|
|
50
|
+
stdout_w.close
|
|
51
|
+
err_w.close
|
|
52
|
+
|
|
53
|
+
@process = stdin_w
|
|
54
|
+
@transport = Transport.new(input: stdout_r, output: stdin_w)
|
|
55
|
+
|
|
56
|
+
@read_thread = Thread.new { read_loop(stdout_r) }
|
|
57
|
+
|
|
58
|
+
# Log stderr from Sorbet in background
|
|
59
|
+
Thread.new do
|
|
60
|
+
err_r.each_line { |line| @logger.info("Sorbet stderr: #{line.chomp}") }
|
|
61
|
+
err_r.close
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { params(method_name: String, params: T.untyped).returns(T.untyped) }
|
|
66
|
+
def send_request(method_name, params)
|
|
67
|
+
transport = @transport
|
|
68
|
+
raise 'Sorbet process not started' unless transport
|
|
69
|
+
|
|
70
|
+
id = @pending_mutex.synchronize do
|
|
71
|
+
current = @next_id
|
|
72
|
+
@next_id += 1
|
|
73
|
+
current
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
queue = Thread::Queue.new
|
|
77
|
+
@pending_mutex.synchronize { @pending_requests[id] = queue }
|
|
78
|
+
|
|
79
|
+
transport.send_request(id, method_name, params)
|
|
80
|
+
|
|
81
|
+
# Wait for response with timeout
|
|
82
|
+
result = nil
|
|
83
|
+
deadline = Time.now + 30
|
|
84
|
+
loop do
|
|
85
|
+
break unless result.nil?
|
|
86
|
+
break if Time.now > deadline
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
result = queue.pop(true) # non-blocking
|
|
90
|
+
rescue ThreadError
|
|
91
|
+
sleep 0.05
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@pending_mutex.synchronize { @pending_requests.delete(id) }
|
|
96
|
+
|
|
97
|
+
if result.nil?
|
|
98
|
+
@logger.error("Sorbet request '#{method_name}' timed out (30s)")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig { params(method_name: String, params: T.untyped).void }
|
|
105
|
+
def send_notification(method_name, params)
|
|
106
|
+
transport = @transport
|
|
107
|
+
raise 'Sorbet process not started' unless transport
|
|
108
|
+
|
|
109
|
+
transport.send_notification(method_name, params)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Forward a raw message (request or notification) to Sorbet
|
|
113
|
+
sig { params(message: T::Hash[String, T.untyped]).void }
|
|
114
|
+
def forward(message)
|
|
115
|
+
transport = @transport
|
|
116
|
+
raise 'Sorbet process not started' unless transport
|
|
117
|
+
|
|
118
|
+
transport.write_message(message)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig { params(method_name: String, block: T.proc.params(msg: T::Hash[String, T.untyped]).void).void }
|
|
122
|
+
def on_notification(method_name, &block)
|
|
123
|
+
@notification_handlers[method_name] = block
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { void }
|
|
127
|
+
def stop
|
|
128
|
+
if @pid
|
|
129
|
+
Process.kill('TERM', @pid)
|
|
130
|
+
Process.wait(@pid)
|
|
131
|
+
@pid = nil
|
|
132
|
+
end
|
|
133
|
+
@read_thread&.kill
|
|
134
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
135
|
+
# Process already gone
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
sig { params(stdout: IO).void }
|
|
141
|
+
def read_loop(stdout)
|
|
142
|
+
reader = Transport.new(input: stdout, output: File.open(File::NULL, 'w'))
|
|
143
|
+
loop do
|
|
144
|
+
message = reader.read_message
|
|
145
|
+
break if message.nil?
|
|
146
|
+
|
|
147
|
+
if message['id'] && !message.key?('method')
|
|
148
|
+
# Response to our request
|
|
149
|
+
@pending_mutex.synchronize do
|
|
150
|
+
queue = @pending_requests[message['id']]
|
|
151
|
+
queue&.push(message)
|
|
152
|
+
end
|
|
153
|
+
elsif message.key?('method')
|
|
154
|
+
# Notification from Sorbet
|
|
155
|
+
handler = @notification_handlers[message['method']]
|
|
156
|
+
handler&.call(message)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
rescue IOError
|
|
160
|
+
@logger.info('Sorbet process stdout closed')
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
module SorbetView
|
|
8
|
+
module Lsp
|
|
9
|
+
# JSON-RPC over stdio transport for LSP
|
|
10
|
+
class Transport
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(input: T.any(IO, StringIO), output: T.any(IO, StringIO)).void }
|
|
14
|
+
def initialize(input: $stdin, output: $stdout)
|
|
15
|
+
@input = T.let(input, T.any(IO, StringIO))
|
|
16
|
+
@output = T.let(output, T.any(IO, StringIO))
|
|
17
|
+
@mutex = T.let(Mutex.new, Mutex)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Read a single JSON-RPC message from the input stream.
|
|
21
|
+
# Returns nil on EOF.
|
|
22
|
+
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
23
|
+
def read_message
|
|
24
|
+
# Read headers
|
|
25
|
+
content_length = nil
|
|
26
|
+
loop do
|
|
27
|
+
line = @input.gets
|
|
28
|
+
return nil if line.nil? # EOF
|
|
29
|
+
|
|
30
|
+
line = line.strip
|
|
31
|
+
break if line.empty? # End of headers
|
|
32
|
+
|
|
33
|
+
if line.start_with?('Content-Length:')
|
|
34
|
+
content_length = line.split(':').last&.strip&.to_i
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return nil unless content_length
|
|
39
|
+
|
|
40
|
+
# Read body
|
|
41
|
+
body = @input.read(content_length)
|
|
42
|
+
return nil if body.nil?
|
|
43
|
+
|
|
44
|
+
JSON.parse(body)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Write a JSON-RPC message to the output stream (thread-safe).
|
|
48
|
+
sig { params(message: T::Hash[String, T.untyped]).void }
|
|
49
|
+
def write_message(message)
|
|
50
|
+
body = JSON.generate(message)
|
|
51
|
+
header = "Content-Length: #{body.bytesize}\r\n\r\n"
|
|
52
|
+
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
@output.write(header)
|
|
55
|
+
@output.write(body)
|
|
56
|
+
@output.flush
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convenience: send a JSON-RPC response
|
|
61
|
+
sig { params(id: T.untyped, result: T.untyped).void }
|
|
62
|
+
def send_response(id, result)
|
|
63
|
+
write_message({ 'jsonrpc' => '2.0', 'id' => id, 'result' => result })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Convenience: send a JSON-RPC error response
|
|
67
|
+
sig { params(id: T.untyped, code: Integer, message: String).void }
|
|
68
|
+
def send_error(id, code, message)
|
|
69
|
+
write_message({
|
|
70
|
+
'jsonrpc' => '2.0',
|
|
71
|
+
'id' => id,
|
|
72
|
+
'error' => { 'code' => code, 'message' => message }
|
|
73
|
+
})
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Convenience: send a JSON-RPC notification (no id)
|
|
77
|
+
sig { params(method_name: String, params: T.untyped).void }
|
|
78
|
+
def send_notification(method_name, params)
|
|
79
|
+
write_message({ 'jsonrpc' => '2.0', 'method' => method_name, 'params' => params })
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Convenience: send a JSON-RPC request
|
|
83
|
+
sig { params(id: T.untyped, method_name: String, params: T.untyped).void }
|
|
84
|
+
def send_request(id, method_name, params)
|
|
85
|
+
write_message({ 'jsonrpc' => '2.0', 'id' => id, 'method' => method_name, 'params' => params })
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module SorbetView
|
|
7
|
+
module Lsp
|
|
8
|
+
# Maps between template file URIs and generated Ruby file URIs
|
|
9
|
+
class UriMapper
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
TEMPLATE_EXTENSIONS = T.let(%w[.erb .haml .slim].freeze, T::Array[String])
|
|
13
|
+
|
|
14
|
+
sig { params(config: Configuration).void }
|
|
15
|
+
def initialize(config:)
|
|
16
|
+
@output_dir = config.output_dir
|
|
17
|
+
# path_mapping: { "/host/path" => "/container/path" }
|
|
18
|
+
@path_mapping = T.let(config.path_mapping, T::Hash[String, String])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { params(uri: String).returns(T::Boolean) }
|
|
22
|
+
def template_uri?(uri)
|
|
23
|
+
path = uri_to_path(uri)
|
|
24
|
+
TEMPLATE_EXTENSIONS.any? { |ext| path.end_with?(ext) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(uri: String).returns(T::Boolean) }
|
|
28
|
+
def generated_ruby_uri?(uri)
|
|
29
|
+
path = uri_to_path(uri)
|
|
30
|
+
path.start_with?(@output_dir) || path.include?("/#{@output_dir}/")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# template URI -> generated ruby URI
|
|
34
|
+
sig { params(template_uri: String).returns(String) }
|
|
35
|
+
def template_to_ruby_uri(template_uri)
|
|
36
|
+
path = uri_to_path(template_uri)
|
|
37
|
+
ruby_path = File.join(@output_dir, "#{path}.rb")
|
|
38
|
+
path_to_uri(ruby_path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# component .rb URI -> generated __erb_template.rb URI
|
|
42
|
+
sig { params(component_uri: String).returns(String) }
|
|
43
|
+
def component_to_ruby_uri(component_uri)
|
|
44
|
+
path = uri_to_path(component_uri)
|
|
45
|
+
ruby_path = File.join(@output_dir, "#{path}__erb_template.rb")
|
|
46
|
+
path_to_uri(ruby_path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# generated ruby URI -> template URI
|
|
50
|
+
sig { params(ruby_uri: String).returns(T.nilable(String)) }
|
|
51
|
+
def ruby_to_template_uri(ruby_uri)
|
|
52
|
+
path = uri_to_path(ruby_uri)
|
|
53
|
+
|
|
54
|
+
# Remove output_dir prefix and .rb suffix
|
|
55
|
+
relative = path.sub(%r{^.*/#{Regexp.escape(@output_dir)}/}, '')
|
|
56
|
+
.sub(%r{^#{Regexp.escape(@output_dir)}/}, '')
|
|
57
|
+
return nil unless relative.end_with?('.rb')
|
|
58
|
+
|
|
59
|
+
# Component heredoc: output is foo.rb__erb_template.rb → source is foo.rb
|
|
60
|
+
if relative.end_with?('__erb_template.rb')
|
|
61
|
+
template_path = relative.chomp('__erb_template.rb')
|
|
62
|
+
return path_to_uri(template_path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
template_path = relative.chomp('.rb')
|
|
66
|
+
path_to_uri(template_path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(uri: String).returns(String) }
|
|
70
|
+
def uri_to_path(uri)
|
|
71
|
+
path = if uri.start_with?('file://')
|
|
72
|
+
URI.decode_www_form_component(URI.parse(uri).path || '')
|
|
73
|
+
else
|
|
74
|
+
uri
|
|
75
|
+
end
|
|
76
|
+
# Map host path -> local (container) path
|
|
77
|
+
@path_mapping.each do |from, to|
|
|
78
|
+
if path.start_with?(from)
|
|
79
|
+
path = path.sub(from, to)
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
# Make relative to CWD
|
|
84
|
+
cwd = Dir.pwd
|
|
85
|
+
if path.start_with?("#{cwd}/")
|
|
86
|
+
path = path.delete_prefix("#{cwd}/")
|
|
87
|
+
end
|
|
88
|
+
path
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { params(path: String).returns(String) }
|
|
92
|
+
def path_to_uri(path)
|
|
93
|
+
absolute = File.expand_path(path)
|
|
94
|
+
# Map local (container) path -> host path
|
|
95
|
+
@path_mapping.each do |from, to|
|
|
96
|
+
if absolute.start_with?(to)
|
|
97
|
+
absolute = absolute.sub(to, from)
|
|
98
|
+
break
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
"file://#{URI.encode_www_form_component(absolute).gsub('%2F', '/')}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetView
|
|
5
|
+
module Perf
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
class Metric < T::Struct
|
|
9
|
+
prop :count, Integer, default: 0
|
|
10
|
+
prop :total_ms, Float, default: 0.0
|
|
11
|
+
prop :min_ms, Float, default: Float::INFINITY
|
|
12
|
+
prop :max_ms, Float, default: 0.0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@metrics = T.let({}, T::Hash[String, Metric])
|
|
16
|
+
@enabled = T.let(true, T::Boolean)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
extend T::Sig
|
|
20
|
+
|
|
21
|
+
sig { returns(T::Boolean) }
|
|
22
|
+
attr_accessor :enabled
|
|
23
|
+
|
|
24
|
+
sig { returns(T::Hash[String, Metric]) }
|
|
25
|
+
attr_reader :metrics
|
|
26
|
+
|
|
27
|
+
sig do
|
|
28
|
+
type_parameters(:R)
|
|
29
|
+
.params(label: String, blk: T.proc.returns(T.type_parameter(:R)))
|
|
30
|
+
.returns(T.type_parameter(:R))
|
|
31
|
+
end
|
|
32
|
+
def measure(label, &blk)
|
|
33
|
+
unless @enabled
|
|
34
|
+
return yield
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
38
|
+
result = yield
|
|
39
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
40
|
+
|
|
41
|
+
metric = @metrics[label] ||= Metric.new
|
|
42
|
+
metric.count += 1
|
|
43
|
+
metric.total_ms += elapsed_ms
|
|
44
|
+
metric.min_ms = elapsed_ms if elapsed_ms < metric.min_ms
|
|
45
|
+
metric.max_ms = elapsed_ms if elapsed_ms > metric.max_ms
|
|
46
|
+
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { void }
|
|
51
|
+
def reset!
|
|
52
|
+
@metrics = {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(io: IO).void }
|
|
56
|
+
def report(io: $stderr)
|
|
57
|
+
return if @metrics.empty?
|
|
58
|
+
|
|
59
|
+
io.puts "\n=== SorbetView Performance Report ==="
|
|
60
|
+
io.puts format('%-35s %8s %10s %10s %10s %10s', 'Label', 'Count', 'Total(ms)', 'Avg(ms)', 'Min(ms)', 'Max(ms)')
|
|
61
|
+
io.puts '-' * 90
|
|
62
|
+
|
|
63
|
+
@metrics.sort_by { |_, m| -m.total_ms }.each do |label, m|
|
|
64
|
+
avg = m.count > 0 ? m.total_ms / m.count : 0.0
|
|
65
|
+
min_val = m.min_ms == Float::INFINITY ? 0.0 : m.min_ms
|
|
66
|
+
io.puts format('%-35s %8d %10.2f %10.2f %10.2f %10.2f', label, m.count, m.total_ms, avg, min_val, m.max_ms)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
io.puts '=' * 90
|
|
70
|
+
io.puts ''
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(logger: Logger).void }
|
|
74
|
+
def report_to_logger(logger)
|
|
75
|
+
return if @metrics.empty?
|
|
76
|
+
|
|
77
|
+
lines = ["\n=== SorbetView Performance Report ==="]
|
|
78
|
+
lines << format('%-35s %8s %10s %10s %10s %10s', 'Label', 'Count', 'Total(ms)', 'Avg(ms)', 'Min(ms)', 'Max(ms)')
|
|
79
|
+
lines << '-' * 90
|
|
80
|
+
|
|
81
|
+
@metrics.sort_by { |_, m| -m.total_ms }.each do |label, m|
|
|
82
|
+
avg = m.count > 0 ? m.total_ms / m.count : 0.0
|
|
83
|
+
min_val = m.min_ms == Float::INFINITY ? 0.0 : m.min_ms
|
|
84
|
+
lines << format('%-35s %8d %10.2f %10.2f %10.2f %10.2f', label, m.count, m.total_ms, avg, min_val, m.max_ms)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
lines << '=' * 90
|
|
88
|
+
logger.info(lines.join("\n"))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetView
|
|
5
|
+
module SourceMap
|
|
6
|
+
class MappingEntry < T::Struct
|
|
7
|
+
const :template_range, Range # position in the template file
|
|
8
|
+
const :ruby_range, Range # position in the generated .rb file
|
|
9
|
+
const :type, Symbol # :code, :expression, :boilerplate
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetView
|
|
5
|
+
module SourceMap
|
|
6
|
+
class Position < T::Struct
|
|
7
|
+
const :line, Integer # 0-based
|
|
8
|
+
const :column, Integer # 0-based
|
|
9
|
+
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(T::Hash[Symbol, Integer]) }
|
|
13
|
+
def to_lsp
|
|
14
|
+
{ line: line, character: column }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(lsp_hash: T::Hash[String, Integer]).returns(Position) }
|
|
18
|
+
def self.from_lsp(lsp_hash)
|
|
19
|
+
new(line: lsp_hash['line'] || lsp_hash[:line], column: lsp_hash['character'] || lsp_hash[:character])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetView
|
|
5
|
+
module SourceMap
|
|
6
|
+
class Range < T::Struct
|
|
7
|
+
const :start, Position
|
|
8
|
+
const :end_, Position
|
|
9
|
+
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(T::Hash[Symbol, T::Hash[Symbol, Integer]]) }
|
|
13
|
+
def to_lsp
|
|
14
|
+
{ start: start.to_lsp, end: end_.to_lsp }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(lsp_hash: T::Hash[String, T.untyped]).returns(Range) }
|
|
18
|
+
def self.from_lsp(lsp_hash)
|
|
19
|
+
new(
|
|
20
|
+
start: Position.from_lsp(lsp_hash['start'] || lsp_hash[:start]),
|
|
21
|
+
end_: Position.from_lsp(lsp_hash['end'] || lsp_hash[:end])
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(position: Position).returns(T::Boolean) }
|
|
26
|
+
def contains?(position)
|
|
27
|
+
return false if position.line < start.line || position.line > end_.line
|
|
28
|
+
return false if position.line == start.line && position.column < start.column
|
|
29
|
+
return false if position.line == end_.line && position.column > end_.column
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetView
|
|
5
|
+
module SourceMap
|
|
6
|
+
class SourceMap
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { returns(String) }
|
|
10
|
+
attr_reader :template_path
|
|
11
|
+
|
|
12
|
+
sig { returns(String) }
|
|
13
|
+
attr_reader :ruby_path
|
|
14
|
+
|
|
15
|
+
sig { returns(T::Array[MappingEntry]) }
|
|
16
|
+
attr_reader :entries
|
|
17
|
+
|
|
18
|
+
sig do
|
|
19
|
+
params(
|
|
20
|
+
template_path: String,
|
|
21
|
+
ruby_path: String,
|
|
22
|
+
entries: T::Array[MappingEntry]
|
|
23
|
+
).void
|
|
24
|
+
end
|
|
25
|
+
def initialize(template_path:, ruby_path:, entries:)
|
|
26
|
+
@template_path = template_path
|
|
27
|
+
@ruby_path = ruby_path
|
|
28
|
+
@entries = entries
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { params(position: Position).returns(T.nilable(Position)) }
|
|
32
|
+
def template_to_ruby(position)
|
|
33
|
+
Perf.measure('sourcemap.template_to_ruby') do
|
|
34
|
+
entry = find_entry_by_template(position)
|
|
35
|
+
next nil unless entry
|
|
36
|
+
next nil if entry.type == :boilerplate
|
|
37
|
+
|
|
38
|
+
translate(position, entry.template_range, entry.ruby_range)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(position: Position).returns(T.nilable(Position)) }
|
|
43
|
+
def ruby_to_template(position)
|
|
44
|
+
Perf.measure('sourcemap.ruby_to_template') do
|
|
45
|
+
entry = find_entry_by_ruby(position)
|
|
46
|
+
next nil unless entry
|
|
47
|
+
next nil if entry.type == :boilerplate
|
|
48
|
+
|
|
49
|
+
translate(position, entry.ruby_range, entry.template_range)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(ruby_range: Range).returns(T.nilable(Range)) }
|
|
54
|
+
def ruby_range_to_template(ruby_range)
|
|
55
|
+
start_pos = ruby_to_template(ruby_range.start)
|
|
56
|
+
end_pos = ruby_to_template(ruby_range.end_)
|
|
57
|
+
return nil unless start_pos && end_pos
|
|
58
|
+
|
|
59
|
+
Range.new(start: start_pos, end_: end_pos)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(template_path: String, ruby_path: String).returns(SourceMap) }
|
|
63
|
+
def self.empty(template_path, ruby_path)
|
|
64
|
+
new(template_path: template_path, ruby_path: ruby_path, entries: [])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
sig { params(position: Position).returns(T.nilable(MappingEntry)) }
|
|
70
|
+
def find_entry_by_template(position)
|
|
71
|
+
@entries.find { |e| e.template_range.contains?(position) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(position: Position).returns(T.nilable(MappingEntry)) }
|
|
75
|
+
def find_entry_by_ruby(position)
|
|
76
|
+
# First try exact match, then fall back to line-only match
|
|
77
|
+
@entries.find { |e| e.ruby_range.contains?(position) } ||
|
|
78
|
+
@entries.find { |e| position.line >= e.ruby_range.start.line && position.line <= e.ruby_range.end_.line }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig do
|
|
82
|
+
params(
|
|
83
|
+
position: Position,
|
|
84
|
+
from_range: Range,
|
|
85
|
+
to_range: Range
|
|
86
|
+
).returns(Position)
|
|
87
|
+
end
|
|
88
|
+
def translate(position, from_range, to_range)
|
|
89
|
+
delta_line = position.line - from_range.start.line
|
|
90
|
+
delta_col = if delta_line == 0
|
|
91
|
+
position.column - from_range.start.column
|
|
92
|
+
else
|
|
93
|
+
position.column
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Position.new(
|
|
97
|
+
line: to_range.start.line + delta_line,
|
|
98
|
+
column: (delta_line == 0 ? to_range.start.column : 0) + delta_col
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/sorbet_view.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
require_relative 'sorbet_view/version'
|
|
7
|
+
require_relative 'sorbet_view/perf'
|
|
8
|
+
require_relative 'sorbet_view/configuration'
|
|
9
|
+
require_relative 'sorbet_view/source_map/position'
|
|
10
|
+
require_relative 'sorbet_view/source_map/range'
|
|
11
|
+
require_relative 'sorbet_view/source_map/mapping_entry'
|
|
12
|
+
require_relative 'sorbet_view/source_map/source_map'
|
|
13
|
+
require_relative 'sorbet_view/compiler/ruby_segment'
|
|
14
|
+
require_relative 'sorbet_view/compiler/parser_adapter'
|
|
15
|
+
require_relative 'sorbet_view/compiler/adapters/erb_adapter'
|
|
16
|
+
require_relative 'sorbet_view/compiler/template_context'
|
|
17
|
+
require_relative 'sorbet_view/compiler/ruby_generator'
|
|
18
|
+
require_relative 'sorbet_view/compiler/template_compiler'
|
|
19
|
+
require_relative 'sorbet_view/compiler/heredoc_extractor'
|
|
20
|
+
require_relative 'sorbet_view/compiler/component_compiler'
|
|
21
|
+
require_relative 'sorbet_view/file_system/output_manager'
|
|
22
|
+
require_relative 'sorbet_view/file_system/project_scanner'
|
|
23
|
+
require_relative 'sorbet_view/file_system/file_watcher'
|
|
24
|
+
require_relative 'sorbet_view/lsp/transport'
|
|
25
|
+
require_relative 'sorbet_view/lsp/uri_mapper'
|
|
26
|
+
require_relative 'sorbet_view/lsp/document_store'
|
|
27
|
+
require_relative 'sorbet_view/lsp/position_translator'
|
|
28
|
+
require_relative 'sorbet_view/lsp/sorbet_process'
|
|
29
|
+
require_relative 'sorbet_view/lsp/server'
|
|
30
|
+
require_relative 'sorbet_view/cli/runner'
|
|
31
|
+
|
|
32
|
+
module SorbetView
|
|
33
|
+
extend T::Sig
|
|
34
|
+
|
|
35
|
+
CONFIG_FILE_NAME = '.sorbet_view.yml'
|
|
36
|
+
end
|