steep 0.40.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/Gemfile +1 -0
- data/bin/output_rebaseline.rb +15 -30
- data/bin/output_test.rb +23 -57
- data/lib/steep.rb +89 -15
- data/lib/steep/annotation_parser.rb +10 -2
- data/lib/steep/ast/types/class.rb +4 -0
- data/lib/steep/cli.rb +31 -6
- data/lib/steep/diagnostic/ruby.rb +13 -8
- data/lib/steep/diagnostic/signature.rb +152 -2
- data/lib/steep/drivers/annotations.rb +18 -36
- data/lib/steep/drivers/check.rb +140 -31
- data/lib/steep/drivers/diagnostic_printer.rb +20 -11
- data/lib/steep/drivers/langserver.rb +4 -8
- data/lib/steep/drivers/print_project.rb +10 -9
- data/lib/steep/drivers/stats.rb +135 -119
- data/lib/steep/drivers/utils/driver_helper.rb +35 -0
- data/lib/steep/drivers/utils/jobs_count.rb +9 -0
- data/lib/steep/drivers/validate.rb +29 -18
- data/lib/steep/drivers/watch.rb +55 -49
- data/lib/steep/drivers/worker.rb +11 -8
- data/lib/steep/expectations.rb +159 -0
- data/lib/steep/index/signature_symbol_provider.rb +23 -1
- data/lib/steep/index/source_index.rb +55 -5
- data/lib/steep/interface/block.rb +4 -0
- data/lib/steep/project.rb +0 -30
- data/lib/steep/project/dsl.rb +5 -3
- data/lib/steep/project/pattern.rb +56 -0
- data/lib/steep/project/target.rb +11 -227
- data/lib/steep/server/base_worker.rb +1 -3
- data/lib/steep/server/change_buffer.rb +63 -0
- data/lib/steep/server/interaction_worker.rb +72 -57
- data/lib/steep/server/master.rb +652 -234
- data/lib/steep/server/type_check_worker.rb +304 -0
- data/lib/steep/server/worker_process.rb +16 -11
- data/lib/steep/{project → services}/completion_provider.rb +5 -5
- data/lib/steep/services/content_change.rb +61 -0
- data/lib/steep/services/file_loader.rb +48 -0
- data/lib/steep/services/goto_service.rb +321 -0
- data/lib/steep/{project → services}/hover_content.rb +19 -20
- data/lib/steep/services/path_assignment.rb +27 -0
- data/lib/steep/services/signature_service.rb +403 -0
- data/lib/steep/services/stats_calculator.rb +69 -0
- data/lib/steep/services/type_check_service.rb +413 -0
- data/lib/steep/signature/validator.rb +187 -85
- data/lib/steep/source.rb +21 -18
- data/lib/steep/subtyping/check.rb +246 -45
- data/lib/steep/subtyping/constraints.rb +4 -4
- data/lib/steep/type_construction.rb +428 -193
- data/lib/steep/type_inference/block_params.rb +1 -1
- data/lib/steep/type_inference/context.rb +22 -0
- data/lib/steep/type_inference/local_variable_type_env.rb +26 -12
- data/lib/steep/type_inference/logic.rb +1 -1
- data/lib/steep/type_inference/logic_type_interpreter.rb +4 -4
- data/lib/steep/type_inference/type_env.rb +43 -17
- data/lib/steep/version.rb +1 -1
- data/smoke/alias/test_expectations.yml +96 -0
- data/smoke/and/test_expectations.yml +31 -0
- data/smoke/array/test_expectations.yml +103 -0
- data/smoke/block/test_expectations.yml +125 -0
- data/smoke/case/test_expectations.yml +47 -0
- data/smoke/class/test_expectations.yml +120 -0
- data/smoke/const/test_expectations.yml +129 -0
- data/smoke/diagnostics-rbs-duplicated/test_expectations.yml +13 -0
- data/smoke/diagnostics-rbs/Steepfile +7 -4
- data/smoke/diagnostics-rbs/test_expectations.yml +231 -0
- data/smoke/diagnostics-rbs/unknown-type-name-2.rbs +5 -0
- data/smoke/{broken → diagnostics-ruby-unsat}/Steepfile +0 -0
- data/smoke/diagnostics-ruby-unsat/a.rbs +3 -0
- data/smoke/diagnostics-ruby-unsat/test_expectations.yml +27 -0
- data/smoke/{diagnostics → diagnostics-ruby-unsat}/unsatisfiable_constraint.rb +0 -1
- data/smoke/diagnostics/a.rbs +0 -4
- data/smoke/diagnostics/test_expectations.yml +451 -0
- data/smoke/dstr/test_expectations.yml +13 -0
- data/smoke/ensure/test_expectations.yml +62 -0
- data/smoke/enumerator/test_expectations.yml +135 -0
- data/smoke/extension/f.rb +2 -0
- data/smoke/extension/f.rbs +3 -0
- data/smoke/extension/test_expectations.yml +73 -0
- data/smoke/hash/test_expectations.yml +81 -0
- data/smoke/hello/test_expectations.yml +25 -0
- data/smoke/if/test_expectations.yml +34 -0
- data/smoke/implements/b.rb +13 -0
- data/smoke/implements/b.rbs +12 -0
- data/smoke/implements/test_expectations.yml +23 -0
- data/smoke/initialize/test_expectations.yml +1 -0
- data/smoke/integer/test_expectations.yml +101 -0
- data/smoke/interface/test_expectations.yml +23 -0
- data/smoke/kwbegin/test_expectations.yml +17 -0
- data/smoke/lambda/test_expectations.yml +39 -0
- data/smoke/literal/test_expectations.yml +106 -0
- data/smoke/map/test_expectations.yml +1 -0
- data/smoke/method/test_expectations.yml +90 -0
- data/smoke/module/test_expectations.yml +75 -0
- data/smoke/regexp/test_expectations.yml +615 -0
- data/smoke/regression/issue_328.rb +1 -0
- data/smoke/regression/issue_328.rbs +0 -0
- data/smoke/regression/issue_332.rb +11 -0
- data/smoke/regression/issue_332.rbs +19 -0
- data/smoke/regression/issue_372.rb +8 -0
- data/smoke/regression/issue_372.rbs +4 -0
- data/smoke/regression/masgn.rb +4 -0
- data/smoke/regression/test_expectations.yml +60 -0
- data/smoke/regression/thread.rb +7 -0
- data/smoke/rescue/test_expectations.yml +79 -0
- data/smoke/self/test_expectations.yml +23 -0
- data/smoke/skip/test_expectations.yml +23 -0
- data/smoke/stdout/test_expectations.yml +1 -0
- data/smoke/super/test_expectations.yml +69 -0
- data/smoke/toplevel/test_expectations.yml +15 -0
- data/smoke/tsort/Steepfile +2 -0
- data/smoke/tsort/test_expectations.yml +63 -0
- data/smoke/type_case/test_expectations.yml +48 -0
- data/smoke/unexpected/Steepfile +5 -0
- data/smoke/unexpected/test_expectations.yml +25 -0
- data/smoke/unexpected/unexpected.rb +1 -0
- data/smoke/unexpected/unexpected.rbs +3 -0
- data/smoke/yield/test_expectations.yml +68 -0
- data/steep.gemspec +4 -3
- metadata +127 -80
- data/lib/steep/project/file_loader.rb +0 -68
- data/lib/steep/project/signature_file.rb +0 -39
- data/lib/steep/project/source_file.rb +0 -129
- data/lib/steep/project/stats_calculator.rb +0 -80
- data/lib/steep/server/code_worker.rb +0 -150
- data/lib/steep/server/signature_worker.rb +0 -157
- data/lib/steep/server/utils.rb +0 -69
- data/smoke/alias/test.yaml +0 -73
- data/smoke/and/test.yaml +0 -24
- data/smoke/array/test.yaml +0 -80
- data/smoke/block/test.yaml +0 -96
- data/smoke/broken/broken.rb +0 -0
- data/smoke/broken/broken.rbs +0 -0
- data/smoke/broken/test.yaml +0 -6
- data/smoke/case/test.yaml +0 -36
- data/smoke/class/test.yaml +0 -89
- data/smoke/const/test.yaml +0 -96
- data/smoke/diagnostics-rbs-duplicated/test.yaml +0 -10
- data/smoke/diagnostics-rbs/test.yaml +0 -142
- data/smoke/diagnostics/test.yaml +0 -333
- data/smoke/dstr/test.yaml +0 -10
- data/smoke/ensure/test.yaml +0 -47
- data/smoke/enumerator/test.yaml +0 -100
- data/smoke/extension/test.yaml +0 -50
- data/smoke/hash/test.yaml +0 -62
- data/smoke/hello/test.yaml +0 -18
- data/smoke/if/test.yaml +0 -27
- data/smoke/implements/test.yaml +0 -16
- data/smoke/initialize/test.yaml +0 -4
- data/smoke/integer/test.yaml +0 -66
- data/smoke/interface/test.yaml +0 -16
- data/smoke/kwbegin/test.yaml +0 -14
- data/smoke/lambda/test.yaml +0 -28
- data/smoke/literal/test.yaml +0 -79
- data/smoke/map/test.yaml +0 -4
- data/smoke/method/test.yaml +0 -71
- data/smoke/module/test.yaml +0 -51
- data/smoke/regexp/test.yaml +0 -372
- data/smoke/regression/test.yaml +0 -38
- data/smoke/rescue/test.yaml +0 -60
- data/smoke/self/test.yaml +0 -16
- data/smoke/skip/test.yaml +0 -16
- data/smoke/stdout/test.yaml +0 -4
- data/smoke/super/test.yaml +0 -52
- data/smoke/toplevel/test.yaml +0 -12
- data/smoke/tsort/test.yaml +0 -32
- data/smoke/type_case/test.yaml +0 -33
- data/smoke/yield/test.yaml +0 -49
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Steep
|
|
2
|
+
module Server
|
|
3
|
+
module ChangeBuffer
|
|
4
|
+
attr_reader :mutex
|
|
5
|
+
attr_reader :buffered_changes
|
|
6
|
+
|
|
7
|
+
def push_buffer
|
|
8
|
+
@mutex.synchronize do
|
|
9
|
+
yield buffered_changes
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def pop_buffer
|
|
14
|
+
changes = {}
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
changes.merge!(buffered_changes)
|
|
17
|
+
buffered_changes.clear
|
|
18
|
+
end
|
|
19
|
+
if block_given?
|
|
20
|
+
yield changes
|
|
21
|
+
else
|
|
22
|
+
changes
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_files(project:, commandline_args:)
|
|
27
|
+
Steep.logger.tagged "#load_files" do
|
|
28
|
+
push_buffer do |changes|
|
|
29
|
+
loader = Services::FileLoader.new(base_dir: project.base_dir)
|
|
30
|
+
|
|
31
|
+
Steep.measure "load changes from disk" do
|
|
32
|
+
project.targets.each do |target|
|
|
33
|
+
loader.load_changes(target.source_pattern, commandline_args, changes: changes)
|
|
34
|
+
loader.load_changes(target.signature_pattern, changes: changes)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def collect_changes(request)
|
|
42
|
+
push_buffer do |changes|
|
|
43
|
+
path = project.relative_path(Pathname(URI.parse(request[:params][:textDocument][:uri]).path))
|
|
44
|
+
version = request[:params][:textDocument][:version]
|
|
45
|
+
Steep.logger.info { "Updating source: path=#{path}, version=#{version}..." }
|
|
46
|
+
|
|
47
|
+
changes[path] ||= []
|
|
48
|
+
request[:params][:contentChanges].each do |change|
|
|
49
|
+
changes[path] << Services::ContentChange.new(
|
|
50
|
+
range: change[:range]&.yield_self {|range|
|
|
51
|
+
[
|
|
52
|
+
range[:start].yield_self {|pos| Services::ContentChange::Position.new(line: pos[:line] + 1, column: pos[:character]) },
|
|
53
|
+
range[:end].yield_self {|pos| Services::ContentChange::Position.new(line: pos[:line] + 1, column: pos[:character]) }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
text: change[:text]
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,39 +1,62 @@
|
|
|
1
1
|
module Steep
|
|
2
2
|
module Server
|
|
3
3
|
class InteractionWorker < BaseWorker
|
|
4
|
-
|
|
4
|
+
include ChangeBuffer
|
|
5
|
+
|
|
6
|
+
ApplyChangeJob = Class.new()
|
|
7
|
+
HoverJob = Struct.new(:id, :path, :line, :column, keyword_init: true)
|
|
8
|
+
CompletionJob = Struct.new(:id, :path, :line, :column, :trigger, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
attr_reader :service
|
|
5
11
|
|
|
6
12
|
def initialize(project:, reader:, writer:, queue: Queue.new)
|
|
7
13
|
super(project: project, reader: reader, writer: writer)
|
|
8
14
|
@queue = queue
|
|
15
|
+
@service = Services::TypeCheckService.new(project: project)
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@buffered_changes = {}
|
|
9
18
|
end
|
|
10
19
|
|
|
11
20
|
def handle_job(job)
|
|
12
|
-
Steep.logger.
|
|
13
|
-
|
|
21
|
+
Steep.logger.tagged "#handle_job" do
|
|
22
|
+
changes = pop_buffer()
|
|
23
|
+
|
|
24
|
+
unless changes.empty?
|
|
25
|
+
Steep.logger.debug { "Applying changes for #{changes.size} files..." }
|
|
26
|
+
service.update(changes: changes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
case job
|
|
30
|
+
when ApplyChangeJob
|
|
31
|
+
# nop
|
|
32
|
+
when HoverJob
|
|
33
|
+
writer.write({ id: job.id, result: process_hover(job) })
|
|
34
|
+
when CompletionJob
|
|
35
|
+
writer.write({ id: job.id, result: process_completion(job) })
|
|
36
|
+
end
|
|
37
|
+
end
|
|
14
38
|
end
|
|
15
39
|
|
|
16
40
|
def handle_request(request)
|
|
17
41
|
case request[:method]
|
|
18
42
|
when "initialize"
|
|
19
|
-
|
|
43
|
+
load_files(project: project, commandline_args: [])
|
|
44
|
+
queue << ApplyChangeJob.new
|
|
20
45
|
writer.write({ id: request[:id], result: nil })
|
|
21
46
|
|
|
22
47
|
when "textDocument/didChange"
|
|
23
|
-
|
|
48
|
+
collect_changes(request)
|
|
49
|
+
queue << ApplyChangeJob.new
|
|
24
50
|
|
|
25
51
|
when "textDocument/hover"
|
|
26
52
|
id = request[:id]
|
|
27
53
|
|
|
28
54
|
uri = URI.parse(request[:params][:textDocument][:uri])
|
|
29
55
|
path = project.relative_path(Pathname(uri.path))
|
|
30
|
-
line = request[:params][:position][:line]
|
|
56
|
+
line = request[:params][:position][:line]+1
|
|
31
57
|
column = request[:params][:position][:character]
|
|
32
58
|
|
|
33
|
-
queue <<
|
|
34
|
-
id: id,
|
|
35
|
-
result: response_to_hover(path: path, line: line, column: column)
|
|
36
|
-
}
|
|
59
|
+
queue << HoverJob.new(id: id, path: path, line: line, column: column)
|
|
37
60
|
|
|
38
61
|
when "textDocument/completion"
|
|
39
62
|
id = request[:id]
|
|
@@ -42,22 +65,19 @@ module Steep
|
|
|
42
65
|
uri = URI.parse(params[:textDocument][:uri])
|
|
43
66
|
path = project.relative_path(Pathname(uri.path))
|
|
44
67
|
line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
|
|
45
|
-
trigger = params
|
|
68
|
+
trigger = params.dig(:context, :triggerCharacter)
|
|
46
69
|
|
|
47
|
-
queue <<
|
|
48
|
-
id: id,
|
|
49
|
-
result: response_to_completion(path: path, line: line, column: column, trigger: trigger)
|
|
50
|
-
}
|
|
70
|
+
queue << CompletionJob.new(id: id, path: path, line: line, column: column, trigger: trigger)
|
|
51
71
|
end
|
|
52
72
|
end
|
|
53
73
|
|
|
54
|
-
def
|
|
55
|
-
Steep.logger.tagged "#
|
|
56
|
-
Steep.measure "Generating response" do
|
|
57
|
-
Steep.logger.info { "path=#{path}, line=#{line}, column=#{column}" }
|
|
74
|
+
def process_hover(job)
|
|
75
|
+
Steep.logger.tagged "#process_hover" do
|
|
76
|
+
Steep.measure "Generating hover response" do
|
|
77
|
+
Steep.logger.info { "path=#{job.path}, line=#{job.line}, column=#{job.column}" }
|
|
58
78
|
|
|
59
|
-
hover =
|
|
60
|
-
content = hover.content_for(path: path, line: line
|
|
79
|
+
hover = Services::HoverContent.new(service: service)
|
|
80
|
+
content = hover.content_for(path: job.path, line: job.line, column: job.column+1)
|
|
61
81
|
if content
|
|
62
82
|
range = content.location.yield_self do |location|
|
|
63
83
|
start_position = { line: location.line - 1, character: location.column }
|
|
@@ -70,22 +90,22 @@ module Steep
|
|
|
70
90
|
range: range
|
|
71
91
|
)
|
|
72
92
|
end
|
|
93
|
+
rescue Typing::UnknownNodeError => exn
|
|
94
|
+
Steep.log_error exn, message: "Failed to compute hover: #{exn.inspect}"
|
|
95
|
+
nil
|
|
73
96
|
end
|
|
74
|
-
rescue Typing::UnknownNodeError => exn
|
|
75
|
-
Steep.log_error exn, message: "Failed to compute hover: #{exn.inspect}"
|
|
76
|
-
nil
|
|
77
97
|
end
|
|
78
98
|
end
|
|
79
99
|
|
|
80
100
|
def format_hover(content)
|
|
81
101
|
case content
|
|
82
|
-
when
|
|
102
|
+
when Services::HoverContent::VariableContent
|
|
83
103
|
"`#{content.name}`: `#{content.type.to_s}`"
|
|
84
|
-
when
|
|
104
|
+
when Services::HoverContent::MethodCallContent
|
|
85
105
|
method_name = case content.method_name
|
|
86
|
-
when
|
|
106
|
+
when Services::HoverContent::InstanceMethodName
|
|
87
107
|
"#{content.method_name.class_name}##{content.method_name.method_name}"
|
|
88
|
-
when
|
|
108
|
+
when Services::HoverContent::SingletonMethodName
|
|
89
109
|
"#{content.method_name.class_name}.#{content.method_name.method_name}"
|
|
90
110
|
else
|
|
91
111
|
nil
|
|
@@ -107,7 +127,7 @@ HOVER
|
|
|
107
127
|
else
|
|
108
128
|
"`#{content.type}`"
|
|
109
129
|
end
|
|
110
|
-
when
|
|
130
|
+
when Services::HoverContent::DefinitionContent
|
|
111
131
|
string = <<HOVER
|
|
112
132
|
```
|
|
113
133
|
def #{content.method_name}: #{content.method_type}
|
|
@@ -122,42 +142,37 @@ HOVER
|
|
|
122
142
|
end
|
|
123
143
|
|
|
124
144
|
string
|
|
125
|
-
when
|
|
145
|
+
when Services::HoverContent::TypeContent
|
|
126
146
|
"`#{content.type}`"
|
|
127
147
|
end
|
|
128
148
|
end
|
|
129
149
|
|
|
130
|
-
def
|
|
150
|
+
def process_completion(job)
|
|
131
151
|
Steep.logger.tagged("#response_to_completion") do
|
|
132
152
|
Steep.measure "Generating response" do
|
|
133
|
-
Steep.logger.info "path: #{path}, line: #{line}, column: #{column}, trigger: #{trigger}"
|
|
134
|
-
|
|
135
|
-
target = project.target_for_source_path(path) or return
|
|
136
|
-
target.type_check(target_sources: [], validate_signatures: false)
|
|
153
|
+
Steep.logger.info "path: #{job.path}, line: #{job.line}, column: #{job.column}, trigger: #{job.trigger}"
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
source = target.source_files[path]
|
|
155
|
+
target = project.target_for_source_path(job.path) or return
|
|
156
|
+
file = service.source_files[job.path] or return
|
|
157
|
+
subtyping = service.signature_services[target.name].current_subtyping or return
|
|
142
158
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
provider = Services::CompletionProvider.new(source_text: file.content, path: job.path, subtyping: subtyping)
|
|
160
|
+
items = begin
|
|
161
|
+
provider.run(line: job.line, column: job.column)
|
|
162
|
+
rescue Parser::SyntaxError
|
|
163
|
+
[]
|
|
164
|
+
end
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
completion_items = items.map do |item|
|
|
167
|
+
format_completion_item(item)
|
|
168
|
+
end
|
|
153
169
|
|
|
154
|
-
|
|
170
|
+
Steep.logger.debug "items = #{completion_items.inspect}"
|
|
155
171
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
end
|
|
172
|
+
LSP::Interface::CompletionList.new(
|
|
173
|
+
is_incomplete: false,
|
|
174
|
+
items: completion_items
|
|
175
|
+
)
|
|
161
176
|
end
|
|
162
177
|
end
|
|
163
178
|
end
|
|
@@ -175,7 +190,7 @@ HOVER
|
|
|
175
190
|
)
|
|
176
191
|
|
|
177
192
|
case item
|
|
178
|
-
when
|
|
193
|
+
when Services::CompletionProvider::LocalVariableItem
|
|
179
194
|
LanguageServer::Protocol::Interface::CompletionItem.new(
|
|
180
195
|
label: item.identifier,
|
|
181
196
|
kind: LanguageServer::Protocol::Constant::CompletionItemKind::VARIABLE,
|
|
@@ -185,7 +200,7 @@ HOVER
|
|
|
185
200
|
new_text: "#{item.identifier}"
|
|
186
201
|
)
|
|
187
202
|
)
|
|
188
|
-
when
|
|
203
|
+
when Services::CompletionProvider::MethodNameItem
|
|
189
204
|
label = "def #{item.identifier}: #{item.method_type}"
|
|
190
205
|
method_type_snippet = method_type_to_snippet(item.method_type)
|
|
191
206
|
LanguageServer::Protocol::Interface::CompletionItem.new(
|
|
@@ -199,7 +214,7 @@ HOVER
|
|
|
199
214
|
insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET,
|
|
200
215
|
sort_text: item.inherited? ? 'z' : 'a' # Ensure language server puts non-inherited methods before inherited methods
|
|
201
216
|
)
|
|
202
|
-
when
|
|
217
|
+
when Services::CompletionProvider::InstanceVariableItem
|
|
203
218
|
label = "#{item.identifier}: #{item.type}"
|
|
204
219
|
LanguageServer::Protocol::Interface::CompletionItem.new(
|
|
205
220
|
label: label,
|
data/lib/steep/server/master.rb
CHANGED
|
@@ -3,185 +3,444 @@ module Steep
|
|
|
3
3
|
class Master
|
|
4
4
|
LSP = LanguageServer::Protocol
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
class TypeCheckRequest
|
|
7
|
+
attr_reader :guid
|
|
8
|
+
attr_reader :library_paths
|
|
9
|
+
attr_reader :signature_paths
|
|
10
|
+
attr_reader :code_paths
|
|
11
|
+
attr_reader :priority_paths
|
|
12
|
+
attr_reader :checked_paths
|
|
13
|
+
|
|
14
|
+
def initialize(guid:)
|
|
15
|
+
@guid = guid
|
|
16
|
+
@library_paths = Set[]
|
|
17
|
+
@signature_paths = Set[]
|
|
18
|
+
@code_paths = Set[]
|
|
19
|
+
@priority_paths = Set[]
|
|
20
|
+
@checked_paths = Set[]
|
|
21
|
+
end
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# There are four types of threads:
|
|
19
|
-
#
|
|
20
|
-
# 1. Main thread -- Reads messages from client
|
|
21
|
-
# 2. Worker threads -- Reads messages from associated worker
|
|
22
|
-
# 3. Reconciliation thread -- Receives message from worker threads, reconciles, processes, and forwards to write thread
|
|
23
|
-
# 4. Write thread -- Writes messages to client
|
|
24
|
-
#
|
|
25
|
-
# We have two queues:
|
|
26
|
-
#
|
|
27
|
-
# 1. `recon_queue` is to pass messages from worker threads to reconciliation thread
|
|
28
|
-
# 2. `write` thread is to pass messages to write thread
|
|
29
|
-
#
|
|
30
|
-
# Message passing: Client -> Server (Master) -> Worker
|
|
31
|
-
#
|
|
32
|
-
# 1. Client -> Server
|
|
33
|
-
# Master receives messages from the LSP client on main thread.
|
|
34
|
-
#
|
|
35
|
-
# 2. Master -> Worker
|
|
36
|
-
# Master writes messages to workers on main thread.
|
|
37
|
-
#
|
|
38
|
-
# Message passing: Worker -> Server (Master) -> (reconciliation queue) -> (write queue) -> Client
|
|
39
|
-
#
|
|
40
|
-
# 3. Worker -> Master
|
|
41
|
-
# Master receives messages on threads dedicated for each worker.
|
|
42
|
-
# The messages sent from workers are then forwarded to the reconciliation thread through reconciliation queue.
|
|
43
|
-
#
|
|
44
|
-
# 4. Server -> Client
|
|
45
|
-
# The reconciliation thread reads messages from reconciliation queue, does something, and finally sends messages to the client via write queue.
|
|
46
|
-
#
|
|
47
|
-
attr_reader :write_queue
|
|
48
|
-
attr_reader :recon_queue
|
|
49
|
-
|
|
50
|
-
include Utils
|
|
51
|
-
|
|
52
|
-
class ResponseHandler
|
|
53
|
-
attr_reader :workers
|
|
23
|
+
def uri(path)
|
|
24
|
+
URI.parse(path.to_s).tap do |uri|
|
|
25
|
+
uri.scheme = "file"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
54
28
|
|
|
55
|
-
|
|
56
|
-
|
|
29
|
+
def as_json(assignment:)
|
|
30
|
+
{
|
|
31
|
+
guid: guid,
|
|
32
|
+
library_uris: library_paths.grep(assignment).map {|path| uri(path).to_s },
|
|
33
|
+
signature_uris: signature_paths.grep(assignment).map {|path| uri(path).to_s },
|
|
34
|
+
code_uris: code_paths.grep(assignment).map {|path| uri(path).to_s },
|
|
35
|
+
priority_uris: priority_paths.map {|path| uri(path).to_s }
|
|
36
|
+
}
|
|
37
|
+
end
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
def total
|
|
40
|
+
library_paths.size + signature_paths.size + code_paths.size
|
|
41
|
+
end
|
|
60
42
|
|
|
61
|
-
def
|
|
62
|
-
|
|
43
|
+
def percentage
|
|
44
|
+
checked_paths.size * 100 / total
|
|
45
|
+
end
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
def all_paths
|
|
48
|
+
library_paths + signature_paths + code_paths
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def checking_path?(path)
|
|
52
|
+
library_paths.include?(path) ||
|
|
53
|
+
signature_paths.include?(path) ||
|
|
54
|
+
code_paths.include?(path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def checked(path)
|
|
58
|
+
raise unless checking_path?(path)
|
|
59
|
+
checked_paths << path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def finished?
|
|
63
|
+
unchecked_paths.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def unchecked_paths
|
|
67
|
+
all_paths - checked_paths
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unchecked_code_paths
|
|
71
|
+
code_paths - checked_paths
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def unchecked_library_paths
|
|
75
|
+
library_paths - checked_paths
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unchecked_signature_paths
|
|
79
|
+
signature_paths - checked_paths
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class TypeCheckController
|
|
84
|
+
attr_reader :project
|
|
85
|
+
attr_reader :priority_paths
|
|
86
|
+
attr_reader :changed_paths
|
|
87
|
+
attr_reader :target_paths
|
|
88
|
+
|
|
89
|
+
class TargetPaths
|
|
90
|
+
attr_reader :project
|
|
91
|
+
attr_reader :target
|
|
92
|
+
attr_reader :code_paths
|
|
93
|
+
attr_reader :signature_paths
|
|
94
|
+
attr_reader :library_paths
|
|
95
|
+
|
|
96
|
+
def initialize(project:, target:)
|
|
97
|
+
@project = project
|
|
98
|
+
@target = target
|
|
99
|
+
@code_paths = Set[]
|
|
100
|
+
@signature_paths = Set[]
|
|
101
|
+
@library_paths = Set[]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def all_paths
|
|
105
|
+
code_paths + signature_paths + library_paths
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def library_path?(path)
|
|
109
|
+
library_paths.include?(path)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def signature_path?(path)
|
|
113
|
+
signature_paths.include?(path)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def code_path?(path)
|
|
117
|
+
code_paths.include?(path)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def add(path)
|
|
121
|
+
return if library_path?(path) || signature_path?(path) || code_path?(path)
|
|
122
|
+
|
|
123
|
+
relative_path = project.relative_path(path)
|
|
124
|
+
|
|
125
|
+
case
|
|
126
|
+
when target.source_pattern =~ relative_path
|
|
127
|
+
code_paths << path
|
|
128
|
+
when target.signature_pattern =~ relative_path
|
|
129
|
+
signature_paths << path
|
|
130
|
+
else
|
|
131
|
+
library_paths << path
|
|
132
|
+
end
|
|
67
133
|
end
|
|
68
134
|
|
|
69
|
-
|
|
70
|
-
@on_completion_handlers = []
|
|
135
|
+
alias << add
|
|
71
136
|
end
|
|
72
137
|
|
|
73
|
-
def
|
|
74
|
-
|
|
138
|
+
def initialize(project:)
|
|
139
|
+
@project = project
|
|
140
|
+
@priority_paths = Set[]
|
|
141
|
+
@changed_paths = Set[]
|
|
142
|
+
@target_paths = project.targets.each.map {|target| TargetPaths.new(project: project, target: target) }
|
|
75
143
|
end
|
|
76
144
|
|
|
77
|
-
def
|
|
78
|
-
|
|
145
|
+
def load(command_line_args:)
|
|
146
|
+
loader = Services::FileLoader.new(base_dir: project.base_dir)
|
|
147
|
+
|
|
148
|
+
target_paths.each do |paths|
|
|
149
|
+
target = paths.target
|
|
150
|
+
|
|
151
|
+
signature_service = Services::SignatureService.load_from(target.new_env_loader(project: project))
|
|
152
|
+
paths.library_paths.merge(signature_service.env_rbs_paths)
|
|
153
|
+
|
|
154
|
+
loader.each_path_in_patterns(target.source_pattern, command_line_args) do |path|
|
|
155
|
+
paths.code_paths << project.absolute_path(path)
|
|
156
|
+
end
|
|
157
|
+
loader.each_path_in_patterns(target.signature_pattern) do |path|
|
|
158
|
+
paths.signature_paths << project.absolute_path(path)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
changed_paths.merge(paths.all_paths)
|
|
162
|
+
end
|
|
79
163
|
end
|
|
80
164
|
|
|
81
|
-
def
|
|
82
|
-
|
|
165
|
+
def push_changes(path)
|
|
166
|
+
return if target_paths.any? {|paths| paths.library_path?(path) }
|
|
167
|
+
|
|
168
|
+
target_paths.each {|paths| paths << path }
|
|
169
|
+
|
|
170
|
+
if target_paths.any? {|paths| paths.code_path?(path) || paths.signature_path?(path) }
|
|
171
|
+
changed_paths << path
|
|
172
|
+
end
|
|
83
173
|
end
|
|
84
174
|
|
|
85
|
-
def
|
|
86
|
-
|
|
175
|
+
def update_priority(open: nil, close: nil)
|
|
176
|
+
path = open || close
|
|
87
177
|
|
|
88
|
-
|
|
89
|
-
|
|
178
|
+
target_paths.each {|paths| paths << path }
|
|
179
|
+
|
|
180
|
+
case
|
|
181
|
+
when open
|
|
182
|
+
priority_paths << path
|
|
183
|
+
when close
|
|
184
|
+
priority_paths.delete path
|
|
90
185
|
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def make_request(guid: SecureRandom.uuid, last_request: nil, include_unchanged: false)
|
|
189
|
+
return if changed_paths.empty? && !include_unchanged
|
|
91
190
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
191
|
+
TypeCheckRequest.new(guid: guid).tap do |request|
|
|
192
|
+
if last_request
|
|
193
|
+
request.library_paths.merge(last_request.unchecked_library_paths)
|
|
194
|
+
request.signature_paths.merge(last_request.unchecked_signature_paths)
|
|
195
|
+
request.code_paths.merge(last_request.unchecked_code_paths)
|
|
95
196
|
end
|
|
197
|
+
|
|
198
|
+
if include_unchanged
|
|
199
|
+
target_paths.each do |paths|
|
|
200
|
+
request.signature_paths.merge(paths.signature_paths)
|
|
201
|
+
request.library_paths.merge(paths.library_paths)
|
|
202
|
+
request.code_paths.merge(paths.code_paths)
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
updated_paths = target_paths.select {|paths| changed_paths.intersect?(paths.all_paths) }
|
|
206
|
+
|
|
207
|
+
updated_paths.each do |paths|
|
|
208
|
+
case
|
|
209
|
+
when paths.signature_paths.intersect?(changed_paths)
|
|
210
|
+
request.signature_paths.merge(paths.signature_paths)
|
|
211
|
+
request.library_paths.merge(paths.library_paths)
|
|
212
|
+
request.code_paths.merge(paths.code_paths)
|
|
213
|
+
when paths.code_paths.intersect?(changed_paths)
|
|
214
|
+
request.code_paths.merge(paths.code_paths & changed_paths)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
request.priority_paths.merge(priority_paths)
|
|
220
|
+
|
|
221
|
+
changed_paths.clear()
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class ResultHandler
|
|
227
|
+
attr_reader :request
|
|
228
|
+
attr_reader :completion_handler
|
|
229
|
+
attr_reader :response
|
|
230
|
+
|
|
231
|
+
def initialize(request:)
|
|
232
|
+
@request = request
|
|
233
|
+
@response = nil
|
|
234
|
+
@completion_handler = nil
|
|
235
|
+
@completed = false
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def process_response(message)
|
|
239
|
+
if request[:id] == message[:id]
|
|
240
|
+
completion_handler&.call(message)
|
|
241
|
+
@response = message
|
|
242
|
+
true
|
|
243
|
+
else
|
|
244
|
+
false
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def result
|
|
249
|
+
response&.dig(:result)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def completed?
|
|
253
|
+
!!@response
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def on_completion(&block)
|
|
257
|
+
@completion_handler = block
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class GroupHandler
|
|
262
|
+
attr_reader :request
|
|
263
|
+
attr_reader :handlers
|
|
264
|
+
attr_reader :completion_handler
|
|
265
|
+
|
|
266
|
+
def initialize()
|
|
267
|
+
@handlers = {}
|
|
268
|
+
@waiting_handlers = Set[]
|
|
269
|
+
@completion_handler = nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def process_response(message)
|
|
273
|
+
if handler = handlers[message[:id]]
|
|
274
|
+
handler.process_response(message)
|
|
275
|
+
|
|
276
|
+
if completed?
|
|
277
|
+
completion_handler&.call(handlers.values)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
true
|
|
281
|
+
else
|
|
282
|
+
false
|
|
96
283
|
end
|
|
97
284
|
end
|
|
98
285
|
|
|
99
286
|
def completed?
|
|
100
|
-
|
|
287
|
+
handlers.each_value.all? {|handler| handler.completed? }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def <<(handler)
|
|
291
|
+
handlers[handler.request[:id]] = handler
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def on_completion(&block)
|
|
295
|
+
@completion_handler = block
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
class ResultController
|
|
300
|
+
attr_reader :handlers
|
|
301
|
+
|
|
302
|
+
def initialize()
|
|
303
|
+
@handlers = []
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def <<(handler)
|
|
307
|
+
@handlers << handler
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def request_group()
|
|
311
|
+
group = GroupHandler.new()
|
|
312
|
+
yield group
|
|
313
|
+
group
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def process_response(message)
|
|
317
|
+
handlers.each do |handler|
|
|
318
|
+
return true if handler.process_response(message)
|
|
319
|
+
end
|
|
320
|
+
false
|
|
321
|
+
ensure
|
|
322
|
+
handlers.reject!(&:completed?)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
ReceiveMessageJob = Struct.new(:source, :message, keyword_init: true) do
|
|
327
|
+
def response?
|
|
328
|
+
message.key?(:id) && !message.key?(:method)
|
|
101
329
|
end
|
|
102
330
|
end
|
|
103
331
|
|
|
104
|
-
|
|
332
|
+
SendMessageJob = Struct.new(:dest, :message, keyword_init: true) do
|
|
333
|
+
def self.to_worker(worker, message:)
|
|
334
|
+
new(dest: worker, message: message)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def self.to_client(message:)
|
|
338
|
+
new(dest: :client, message: message)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
attr_reader :steepfile
|
|
343
|
+
attr_reader :project
|
|
344
|
+
attr_reader :reader, :writer
|
|
345
|
+
attr_reader :commandline_args
|
|
346
|
+
|
|
347
|
+
attr_reader :interaction_worker
|
|
348
|
+
attr_reader :typecheck_workers
|
|
349
|
+
|
|
350
|
+
attr_reader :job_queue
|
|
351
|
+
|
|
352
|
+
attr_reader :current_type_check_request
|
|
353
|
+
attr_reader :controller
|
|
354
|
+
attr_reader :result_controller
|
|
355
|
+
|
|
356
|
+
attr_reader :initialize_params
|
|
357
|
+
attr_accessor :typecheck_automatically
|
|
358
|
+
|
|
359
|
+
def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new)
|
|
105
360
|
@project = project
|
|
106
361
|
@reader = reader
|
|
107
362
|
@writer = writer
|
|
108
|
-
@write_queue = queue
|
|
109
|
-
@recon_queue = Queue.new
|
|
110
363
|
@interaction_worker = interaction_worker
|
|
111
|
-
@
|
|
112
|
-
@
|
|
113
|
-
@
|
|
114
|
-
@
|
|
115
|
-
@
|
|
364
|
+
@typecheck_workers = typecheck_workers
|
|
365
|
+
@current_type_check_request = nil
|
|
366
|
+
@typecheck_automatically = true
|
|
367
|
+
@commandline_args = []
|
|
368
|
+
@job_queue = queue
|
|
369
|
+
|
|
370
|
+
@controller = TypeCheckController.new(project: project)
|
|
371
|
+
@result_controller = ResultController.new()
|
|
116
372
|
end
|
|
117
373
|
|
|
118
374
|
def start
|
|
119
375
|
Steep.logger.tagged "master" do
|
|
120
376
|
tags = Steep.logger.formatter.current_tags.dup
|
|
121
377
|
|
|
122
|
-
Steep.logger.info "Registering all code to workers..."
|
|
123
|
-
source_paths = project.all_source_files
|
|
124
|
-
bin_size = (source_paths.size / code_workers.size) + 1
|
|
125
|
-
source_paths.each_slice(bin_size).with_index do |paths, index|
|
|
126
|
-
register_code_to_worker(paths, worker: code_workers[index])
|
|
127
|
-
end
|
|
128
|
-
|
|
129
378
|
worker_threads = []
|
|
130
379
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
worker_threads << Thread.new do
|
|
139
|
-
Steep.logger.formatter.push_tags(*tags, "from-worker@signature")
|
|
140
|
-
signature_worker.reader.read do |message|
|
|
141
|
-
process_message_from_worker(message, worker: signature_worker)
|
|
380
|
+
if interaction_worker
|
|
381
|
+
worker_threads << Thread.new do
|
|
382
|
+
Steep.logger.formatter.push_tags(*tags, "from-worker@interaction")
|
|
383
|
+
interaction_worker.reader.read do |message|
|
|
384
|
+
job_queue << ReceiveMessageJob.new(source: interaction_worker, message: message)
|
|
385
|
+
end
|
|
142
386
|
end
|
|
143
387
|
end
|
|
144
388
|
|
|
145
|
-
|
|
389
|
+
typecheck_workers.each do |worker|
|
|
146
390
|
worker_threads << Thread.new do
|
|
147
391
|
Steep.logger.formatter.push_tags(*tags, "from-worker@#{worker.name}")
|
|
148
392
|
worker.reader.read do |message|
|
|
149
|
-
|
|
393
|
+
job_queue << ReceiveMessageJob.new(source: worker, message: message)
|
|
150
394
|
end
|
|
151
395
|
end
|
|
152
396
|
end
|
|
153
397
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
writer.io.close
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
worker_threads << Thread.new do
|
|
164
|
-
Steep.logger.formatter.push_tags(*tags, "reconciliation")
|
|
165
|
-
while (message, worker = recon_queue.pop)
|
|
166
|
-
id = message[:id]
|
|
167
|
-
handler = response_handlers[id] or raise
|
|
168
|
-
|
|
169
|
-
Steep.logger.info "Processing response to #{handler.request[:method]}(#{id}) from #{worker.name}"
|
|
170
|
-
|
|
171
|
-
handler.process_response(message, worker)
|
|
172
|
-
|
|
173
|
-
if handler.completed?
|
|
174
|
-
Steep.logger.info "Response to #{handler.request[:method]}(#{id}) completed"
|
|
175
|
-
response_handlers.delete(id)
|
|
176
|
-
end
|
|
398
|
+
read_client_thread = Thread.new do
|
|
399
|
+
reader.read do |message|
|
|
400
|
+
job_queue << ReceiveMessageJob.new(source: :client, message: message)
|
|
401
|
+
break if message[:method] == "exit"
|
|
177
402
|
end
|
|
178
403
|
end
|
|
179
404
|
|
|
180
405
|
Steep.logger.tagged "main" do
|
|
181
|
-
|
|
182
|
-
|
|
406
|
+
while job = job_queue.deq
|
|
407
|
+
case job
|
|
408
|
+
when ReceiveMessageJob
|
|
409
|
+
src = if job.source == :client
|
|
410
|
+
:client
|
|
411
|
+
else
|
|
412
|
+
job.source.name
|
|
413
|
+
end
|
|
414
|
+
Steep.logger.tagged("ReceiveMessageJob(#{src}/#{job.message[:method]}/#{job.message[:id]})") do
|
|
415
|
+
if job.response? && result_controller.process_response(job.message)
|
|
416
|
+
# nop
|
|
417
|
+
Steep.logger.info { "Processed by ResultController" }
|
|
418
|
+
else
|
|
419
|
+
case job.source
|
|
420
|
+
when :client
|
|
421
|
+
process_message_from_client(job.message)
|
|
422
|
+
|
|
423
|
+
if job.message[:method] == "exit"
|
|
424
|
+
job_queue.close()
|
|
425
|
+
end
|
|
426
|
+
when WorkerProcess
|
|
427
|
+
process_message_from_worker(job.message, worker: job.source)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
when SendMessageJob
|
|
432
|
+
case job.dest
|
|
433
|
+
when :client
|
|
434
|
+
Steep.logger.info { "Processing SendMessageJob: dest=client, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
|
|
435
|
+
writer.write job.message
|
|
436
|
+
when WorkerProcess
|
|
437
|
+
Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
|
|
438
|
+
job.dest << job.message
|
|
439
|
+
end
|
|
440
|
+
end
|
|
183
441
|
end
|
|
184
442
|
|
|
443
|
+
read_client_thread.join()
|
|
185
444
|
worker_threads.each do |thread|
|
|
186
445
|
thread.join
|
|
187
446
|
end
|
|
@@ -191,181 +450,340 @@ module Steep
|
|
|
191
450
|
|
|
192
451
|
def each_worker(&block)
|
|
193
452
|
if block_given?
|
|
194
|
-
yield interaction_worker
|
|
195
|
-
|
|
196
|
-
code_workers.each &block
|
|
453
|
+
yield interaction_worker if interaction_worker
|
|
454
|
+
typecheck_workers.each &block
|
|
197
455
|
else
|
|
198
456
|
enum_for :each_worker
|
|
199
457
|
end
|
|
200
458
|
end
|
|
201
459
|
|
|
460
|
+
def pathname(uri)
|
|
461
|
+
Pathname(URI.parse(uri).path)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def work_done_progress_supported?
|
|
465
|
+
initialize_params&.dig(:capabilities, :window, :workDoneProgress)
|
|
466
|
+
end
|
|
467
|
+
|
|
202
468
|
def process_message_from_client(message)
|
|
203
|
-
Steep.logger.info "
|
|
469
|
+
Steep.logger.info "Processing message from client: method=#{message[:method]}, id=#{message[:id]}"
|
|
204
470
|
id = message[:id]
|
|
205
471
|
|
|
206
472
|
case message[:method]
|
|
207
473
|
when "initialize"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
474
|
+
@initialize_params = message[:params]
|
|
475
|
+
result_controller << group_request do |group|
|
|
476
|
+
each_worker do |worker|
|
|
477
|
+
group << send_request(method: "initialize", params: message[:params], worker: worker)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
group.on_completion do
|
|
481
|
+
controller.load(command_line_args: commandline_args)
|
|
482
|
+
|
|
483
|
+
job_queue << SendMessageJob.to_client(
|
|
484
|
+
message: {
|
|
485
|
+
id: id,
|
|
486
|
+
result: LSP::Interface::InitializeResult.new(
|
|
487
|
+
capabilities: LSP::Interface::ServerCapabilities.new(
|
|
488
|
+
text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
|
|
489
|
+
change: LSP::Constant::TextDocumentSyncKind::INCREMENTAL,
|
|
490
|
+
save: LSP::Interface::SaveOptions.new(include_text: false),
|
|
491
|
+
open_close: true
|
|
492
|
+
),
|
|
493
|
+
hover_provider: {
|
|
494
|
+
workDoneProgress: true,
|
|
495
|
+
partialResults: true,
|
|
496
|
+
partialResult: true
|
|
497
|
+
},
|
|
498
|
+
completion_provider: LSP::Interface::CompletionOptions.new(
|
|
499
|
+
trigger_characters: [".", "@"],
|
|
500
|
+
work_done_progress: true
|
|
501
|
+
),
|
|
502
|
+
workspace_symbol_provider: true,
|
|
503
|
+
definition_provider: true,
|
|
504
|
+
declaration_provider: true,
|
|
505
|
+
implementation_provider: true,
|
|
506
|
+
type_definition_provider: true
|
|
507
|
+
)
|
|
222
508
|
)
|
|
223
|
-
|
|
224
|
-
|
|
509
|
+
}
|
|
510
|
+
)
|
|
225
511
|
end
|
|
226
512
|
end
|
|
227
513
|
|
|
228
|
-
when "
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
unless registered_path?(path)
|
|
235
|
-
register_code_to_worker [path], worker: least_busy_worker()
|
|
514
|
+
when "initialized"
|
|
515
|
+
if typecheck_automatically
|
|
516
|
+
if request = controller.make_request(include_unchanged: true)
|
|
517
|
+
start_type_check(request, last_request: nil, start_progress: request.total > 10)
|
|
518
|
+
end
|
|
236
519
|
end
|
|
237
520
|
|
|
521
|
+
when "textDocument/didChange"
|
|
238
522
|
broadcast_notification(message)
|
|
523
|
+
path = pathname(message[:params][:textDocument][:uri])
|
|
524
|
+
controller.push_changes(path)
|
|
525
|
+
|
|
526
|
+
when "textDocument/didSave"
|
|
527
|
+
if typecheck_automatically
|
|
528
|
+
if request = controller.make_request(last_request: current_type_check_request)
|
|
529
|
+
start_type_check(
|
|
530
|
+
request,
|
|
531
|
+
last_request: current_type_check_request,
|
|
532
|
+
start_progress: request.total > 10
|
|
533
|
+
)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
when "textDocument/didOpen"
|
|
538
|
+
path = pathname(message[:params][:textDocument][:uri])
|
|
539
|
+
controller.update_priority(open: path)
|
|
540
|
+
|
|
541
|
+
when "textDocument/didClose"
|
|
542
|
+
path = pathname(message[:params][:textDocument][:uri])
|
|
543
|
+
controller.update_priority(close: path)
|
|
239
544
|
|
|
240
545
|
when "textDocument/hover", "textDocument/completion"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
546
|
+
if interaction_worker
|
|
547
|
+
result_controller << send_request(method: message[:method], params: message[:params], worker: interaction_worker) do |handler|
|
|
548
|
+
handler.on_completion do |response|
|
|
549
|
+
job_queue << SendMessageJob.to_client(
|
|
550
|
+
message: {
|
|
551
|
+
id: message[:id],
|
|
552
|
+
result: response[:result]
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
end
|
|
244
556
|
end
|
|
245
557
|
end
|
|
246
558
|
|
|
247
|
-
when "textDocument/open"
|
|
248
|
-
# Ignores open notification
|
|
249
|
-
|
|
250
559
|
when "workspace/symbol"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
560
|
+
result_controller << group_request do |group|
|
|
561
|
+
typecheck_workers.each do |worker|
|
|
562
|
+
group << send_request(method: "workspace/symbol", params: message[:params], worker: worker)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
group.on_completion do |handlers|
|
|
566
|
+
result = handlers.flat_map(&:result)
|
|
567
|
+
job_queue << SendMessageJob.to_client(message: { id: message[:id], result: result })
|
|
254
568
|
end
|
|
255
569
|
end
|
|
256
570
|
|
|
257
571
|
when "workspace/executeCommand"
|
|
258
572
|
case message[:params][:command]
|
|
259
573
|
when "steep/stats"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
574
|
+
result_controller << group_request do |group|
|
|
575
|
+
typecheck_workers.each do |worker|
|
|
576
|
+
group << send_request(method: "workspace/executeCommand", params: message[:params], worker: worker)
|
|
577
|
+
end
|
|
263
578
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
579
|
+
group.on_completion do |handlers|
|
|
580
|
+
stats = handlers.flat_map(&:result)
|
|
581
|
+
job_queue << SendMessageJob.to_client(
|
|
582
|
+
message: {
|
|
583
|
+
id: message[:id],
|
|
584
|
+
result: stats
|
|
585
|
+
}
|
|
586
|
+
)
|
|
268
587
|
end
|
|
269
588
|
end
|
|
270
589
|
end
|
|
271
590
|
|
|
591
|
+
when "textDocument/definition", "textDocument/implementation"
|
|
592
|
+
result_controller << group_request do |group|
|
|
593
|
+
typecheck_workers.each do |worker|
|
|
594
|
+
group << send_request(method: message[:method], params: message[:params], worker: worker)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
group.on_completion do |handlers|
|
|
598
|
+
links = handlers.flat_map(&:result)
|
|
599
|
+
job_queue << SendMessageJob.to_client(
|
|
600
|
+
message: {
|
|
601
|
+
id: message[:id],
|
|
602
|
+
result: links
|
|
603
|
+
}
|
|
604
|
+
)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
when "$/typecheck"
|
|
609
|
+
request = controller.make_request(
|
|
610
|
+
guid: message[:params][:guid],
|
|
611
|
+
last_request: current_type_check_request,
|
|
612
|
+
include_unchanged: true
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if request
|
|
616
|
+
start_type_check(
|
|
617
|
+
request,
|
|
618
|
+
last_request: current_type_check_request,
|
|
619
|
+
start_progress: true
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
|
|
272
623
|
when "shutdown"
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
624
|
+
result_controller << group_request do |group|
|
|
625
|
+
each_worker do |worker|
|
|
626
|
+
group << send_request(method: "shutdown", worker: worker)
|
|
627
|
+
end
|
|
276
628
|
|
|
277
|
-
|
|
278
|
-
|
|
629
|
+
group.on_completion do
|
|
630
|
+
job_queue << SendMessageJob.to_client(message: { id: message[:id], result: nil })
|
|
279
631
|
end
|
|
280
632
|
end
|
|
281
633
|
|
|
282
634
|
when "exit"
|
|
283
635
|
broadcast_notification(message)
|
|
284
|
-
|
|
285
|
-
return false
|
|
286
636
|
end
|
|
287
|
-
|
|
288
|
-
true
|
|
289
637
|
end
|
|
290
638
|
|
|
291
|
-
def
|
|
292
|
-
Steep.logger.
|
|
293
|
-
|
|
294
|
-
|
|
639
|
+
def process_message_from_worker(message, worker:)
|
|
640
|
+
Steep.logger.tagged "#process_message_from_worker (worker=#{worker.name})" do
|
|
641
|
+
Steep.logger.info { "Processing message from worker: method=#{message[:method] || "-"}, id=#{message[:id] || "*"}" }
|
|
642
|
+
|
|
643
|
+
case
|
|
644
|
+
when message.key?(:id) && !message.key?(:method)
|
|
645
|
+
Steep.logger.tagged "response(id=#{message[:id]})" do
|
|
646
|
+
Steep.logger.error { "Received unexpected response" }
|
|
647
|
+
Steep.logger.debug { "result = #{message[:result].inspect}" }
|
|
648
|
+
end
|
|
649
|
+
when message.key?(:method) && !message.key?(:id)
|
|
650
|
+
case message[:method]
|
|
651
|
+
when "$/typecheck/progress"
|
|
652
|
+
on_type_check_update(
|
|
653
|
+
guid: message[:params][:guid],
|
|
654
|
+
path: Pathname(message[:params][:path])
|
|
655
|
+
)
|
|
656
|
+
else
|
|
657
|
+
# Forward other notifications
|
|
658
|
+
job_queue << SendMessageJob.to_client(message: message)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
295
661
|
end
|
|
296
662
|
end
|
|
297
663
|
|
|
298
|
-
def
|
|
299
|
-
Steep.logger.
|
|
300
|
-
|
|
301
|
-
|
|
664
|
+
def start_type_check(request, last_request:, start_progress:)
|
|
665
|
+
Steep.logger.tagged "#start_type_check(#{request.guid}, #{last_request&.guid}" do
|
|
666
|
+
if last_request
|
|
667
|
+
Steep.logger.info "Cancelling last request"
|
|
668
|
+
|
|
669
|
+
job_queue << SendMessageJob.to_client(
|
|
670
|
+
message: {
|
|
671
|
+
method: "$/progress",
|
|
672
|
+
params: {
|
|
673
|
+
token: last_request.guid,
|
|
674
|
+
value: { kind: "end" }
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
if start_progress
|
|
681
|
+
Steep.logger.info "Starting new progress..."
|
|
682
|
+
|
|
683
|
+
@current_type_check_request = request
|
|
684
|
+
|
|
685
|
+
if work_done_progress_supported?
|
|
686
|
+
job_queue << SendMessageJob.to_client(
|
|
687
|
+
message: {
|
|
688
|
+
id: fresh_request_id,
|
|
689
|
+
method: "window/workDoneProgress/create",
|
|
690
|
+
params: { token: request.guid }
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
end
|
|
302
694
|
|
|
303
|
-
|
|
304
|
-
|
|
695
|
+
job_queue << SendMessageJob.to_client(
|
|
696
|
+
message: {
|
|
697
|
+
method: "$/progress",
|
|
698
|
+
params: {
|
|
699
|
+
token: request.guid,
|
|
700
|
+
value: { kind: "begin", title: "Type checking", percentage: 0 }
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if request.finished?
|
|
706
|
+
job_queue << SendMessageJob.to_client(
|
|
707
|
+
message: {
|
|
708
|
+
method: "$/progress",
|
|
709
|
+
params: { token: request.guid, value: { kind: "end" } }
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
end
|
|
713
|
+
else
|
|
714
|
+
@current_type_check_request = nil
|
|
715
|
+
end
|
|
305
716
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
response_handlers[handler.request_id] = handler
|
|
717
|
+
Steep.logger.info "Sending $/typecheck/start notifications"
|
|
718
|
+
typecheck_workers.each do |worker|
|
|
719
|
+
assignment = Services::PathAssignment.new(max_index: typecheck_workers.size, index: worker.index)
|
|
310
720
|
|
|
311
|
-
|
|
312
|
-
|
|
721
|
+
job_queue << SendMessageJob.to_worker(
|
|
722
|
+
worker,
|
|
723
|
+
message: {
|
|
724
|
+
method: "$/typecheck/start",
|
|
725
|
+
params: request.as_json(assignment: assignment)
|
|
726
|
+
}
|
|
727
|
+
)
|
|
728
|
+
end
|
|
313
729
|
end
|
|
314
730
|
end
|
|
315
731
|
|
|
316
|
-
def
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
732
|
+
def on_type_check_update(guid:, path:)
|
|
733
|
+
if current = current_type_check_request()
|
|
734
|
+
if current.guid == guid
|
|
735
|
+
current.checked(path)
|
|
736
|
+
Steep.logger.info { "Request updated: checked=#{path}, unchecked=#{current.unchecked_paths.size}" }
|
|
737
|
+
percentage = current.percentage
|
|
738
|
+
value = if percentage == 100
|
|
739
|
+
{ kind: "end" }
|
|
740
|
+
else
|
|
741
|
+
progress_string = ("▮"*(percentage/5)) + ("▯"*(20 - percentage/5))
|
|
742
|
+
{ kind: "report", percentage: percentage, message: "#{progress_string} (#{percentage}%)" }
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
job_queue << SendMessageJob.to_client(
|
|
746
|
+
message: {
|
|
747
|
+
method: "$/progress",
|
|
748
|
+
params: { token: current.guid, value: value }
|
|
749
|
+
}
|
|
750
|
+
)
|
|
321
751
|
|
|
322
|
-
|
|
323
|
-
|
|
752
|
+
@current_type_check_request = nil if current.finished?
|
|
753
|
+
end
|
|
324
754
|
end
|
|
325
755
|
end
|
|
326
756
|
|
|
327
|
-
def
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Steep.logger.info "Received response #{message[:id]} from worker"
|
|
332
|
-
recon_queue << [message, worker]
|
|
333
|
-
when message.key?(:method) && !message.key?(:id)
|
|
334
|
-
# Notification from worker
|
|
335
|
-
Steep.logger.info "Received notification #{message[:method]} from worker"
|
|
336
|
-
write_queue << message
|
|
757
|
+
def broadcast_notification(message)
|
|
758
|
+
Steep.logger.info "Broadcasting notification #{message[:method]}"
|
|
759
|
+
each_worker do |worker|
|
|
760
|
+
job_queue << SendMessageJob.new(dest: worker, message: message)
|
|
337
761
|
end
|
|
338
762
|
end
|
|
339
763
|
|
|
340
|
-
def
|
|
341
|
-
|
|
764
|
+
def send_notification(message, worker:)
|
|
765
|
+
Steep.logger.info "Sending notification #{message[:method]} to #{worker.name}"
|
|
766
|
+
job_queue << SendMessageJob.new(dest: worker, message: message)
|
|
342
767
|
end
|
|
343
768
|
|
|
344
|
-
def
|
|
345
|
-
|
|
346
|
-
paths_for(w).size
|
|
347
|
-
end
|
|
769
|
+
def fresh_request_id
|
|
770
|
+
SecureRandom.alphanumeric(10)
|
|
348
771
|
end
|
|
349
772
|
|
|
350
|
-
def
|
|
351
|
-
|
|
352
|
-
end
|
|
773
|
+
def send_request(method:, id: fresh_request_id(), params: nil, worker:, &block)
|
|
774
|
+
Steep.logger.info "Sending request #{method}(#{id}) to #{worker.name}"
|
|
353
775
|
|
|
354
|
-
|
|
355
|
-
|
|
776
|
+
message = { method: method, id: id, params: params }
|
|
777
|
+
ResultHandler.new(request: message).tap do |handler|
|
|
778
|
+
yield handler if block_given?
|
|
779
|
+
job_queue << SendMessageJob.to_worker(worker, message: message)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
356
782
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
command: "steep/registerSourceToWorker",
|
|
362
|
-
arguments: paths.map do |path|
|
|
363
|
-
"file://#{project.absolute_path(path)}"
|
|
364
|
-
end
|
|
365
|
-
)
|
|
366
|
-
},
|
|
367
|
-
worker: worker
|
|
368
|
-
)
|
|
783
|
+
def group_request()
|
|
784
|
+
GroupHandler.new().tap do |group|
|
|
785
|
+
yield group
|
|
786
|
+
end
|
|
369
787
|
end
|
|
370
788
|
|
|
371
789
|
def kill
|