steep 0.42.0 → 0.43.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/steep.rb +4 -3
  4. data/lib/steep/annotation_parser.rb +10 -2
  5. data/lib/steep/cli.rb +1 -0
  6. data/lib/steep/diagnostic/ruby.rb +15 -6
  7. data/lib/steep/diagnostic/signature.rb +28 -11
  8. data/lib/steep/drivers/annotations.rb +1 -3
  9. data/lib/steep/drivers/check.rb +17 -7
  10. data/lib/steep/drivers/diagnostic_printer.rb +4 -0
  11. data/lib/steep/drivers/langserver.rb +1 -0
  12. data/lib/steep/drivers/print_project.rb +1 -1
  13. data/lib/steep/drivers/stats.rb +125 -105
  14. data/lib/steep/drivers/utils/driver_helper.rb +35 -0
  15. data/lib/steep/drivers/validate.rb +1 -1
  16. data/lib/steep/drivers/watch.rb +12 -10
  17. data/lib/steep/index/signature_symbol_provider.rb +20 -6
  18. data/lib/steep/project/target.rb +4 -4
  19. data/lib/steep/server/interaction_worker.rb +2 -3
  20. data/lib/steep/server/master.rb +621 -170
  21. data/lib/steep/server/type_check_worker.rb +127 -13
  22. data/lib/steep/server/worker_process.rb +7 -4
  23. data/lib/steep/services/completion_provider.rb +2 -2
  24. data/lib/steep/services/hover_content.rb +5 -4
  25. data/lib/steep/services/path_assignment.rb +6 -8
  26. data/lib/steep/services/signature_service.rb +43 -9
  27. data/lib/steep/services/type_check_service.rb +184 -138
  28. data/lib/steep/signature/validator.rb +17 -9
  29. data/lib/steep/source.rb +21 -18
  30. data/lib/steep/subtyping/constraints.rb +2 -2
  31. data/lib/steep/type_construction.rb +223 -125
  32. data/lib/steep/type_inference/block_params.rb +1 -1
  33. data/lib/steep/type_inference/context.rb +22 -0
  34. data/lib/steep/type_inference/logic.rb +1 -1
  35. data/lib/steep/type_inference/logic_type_interpreter.rb +3 -3
  36. data/lib/steep/version.rb +1 -1
  37. data/smoke/implements/b.rb +13 -0
  38. data/smoke/implements/b.rbs +12 -0
  39. data/smoke/regression/issue_328.rb +1 -0
  40. data/smoke/regression/issue_328.rbs +0 -0
  41. data/smoke/regression/issue_332.rb +11 -0
  42. data/smoke/regression/issue_332.rbs +19 -0
  43. data/smoke/regression/masgn.rb +4 -0
  44. data/smoke/regression/test_expectations.yml +29 -0
  45. data/smoke/regression/thread.rb +7 -0
  46. data/smoke/super/test_expectations.yml +2 -12
  47. data/steep.gemspec +2 -2
  48. metadata +40 -20
@@ -20,6 +20,41 @@ module Steep
20
20
  end
21
21
  end
22
22
  end
23
+
24
+ def request_id
25
+ SecureRandom.alphanumeric(10)
26
+ end
27
+
28
+ def wait_for_response_id(reader:, id:, unknown_responses: :ignore)
29
+ wait_for_message(reader: reader, unknown_messages: unknown_responses) do |response|
30
+ response[:id] == id
31
+ end
32
+ end
33
+
34
+ def shutdown_exit(writer:, reader:)
35
+ request_id().tap do |id|
36
+ writer.write({ method: :shutdown, id: id })
37
+ wait_for_response_id(reader: reader, id: id)
38
+ end
39
+ writer.write({ method: :exit })
40
+ end
41
+
42
+ def wait_for_message(reader:, unknown_messages: :ignore, &block)
43
+ reader.read do |message|
44
+ if yield(message)
45
+ return message
46
+ else
47
+ case unknown_messages
48
+ when :ignore
49
+ # nop
50
+ when :log
51
+ Steep.logger.error { "Unexpected message: #{message.inspect}" }
52
+ when :raise
53
+ raise "Unexpected message: #{message.inspect}"
54
+ end
55
+ end
56
+ end
57
+ end
23
58
  end
24
59
  end
25
60
  end
@@ -18,7 +18,7 @@ module Steep
18
18
  any_error = false
19
19
 
20
20
  project.targets.each do |target|
21
- controller = Services::SignatureService.load_from(target.new_env_loader)
21
+ controller = Services::SignatureService.load_from(target.new_env_loader(project: project))
22
22
 
23
23
  changes = file_loader.load_changes(target.signature_pattern, changes: {})
24
24
  controller.update(changes)
@@ -48,13 +48,17 @@ module Steep
48
48
  interaction_worker: nil,
49
49
  typecheck_workers: typecheck_workers
50
50
  )
51
+ master.typecheck_automatically = false
52
+ master.commandline_args.push(*dirs)
51
53
 
52
54
  main_thread = Thread.start do
53
55
  master.start()
54
56
  end
55
57
  main_thread.abort_on_exception = true
56
58
 
57
- client_writer.write(method: "initialize", id: 0)
59
+ initialize_id = request_id()
60
+ client_writer.write(method: "initialize", id: initialize_id)
61
+ wait_for_response_id(reader: client_reader, id: initialize_id)
58
62
 
59
63
  Steep.logger.info "Watching #{dirs.join(", ")}..."
60
64
 
@@ -104,10 +108,15 @@ module Steep
104
108
  end
105
109
  end
106
110
  end
111
+
112
+ client_writer.write(method: "$/typecheck", params: { guid: nil })
107
113
  end.tap(&:start)
108
114
 
109
115
  begin
110
116
  stdout.puts Rainbow("👀 Watching directories, Ctrl-C to stop.").bold
117
+
118
+ client_writer.write(method: "$/typecheck", params: { guid: nil })
119
+
111
120
  client_reader.read do |response|
112
121
  case response[:method]
113
122
  when "textDocument/publishDiagnostics"
@@ -121,6 +130,7 @@ module Steep
121
130
  unless diagnostics.empty?
122
131
  diagnostics.each do |diagnostic|
123
132
  printer.print(diagnostic)
133
+ stdout.flush
124
134
  end
125
135
  end
126
136
  when "window/showMessage"
@@ -132,16 +142,8 @@ module Steep
132
142
  end
133
143
  end
134
144
  rescue Interrupt
135
- shutdown_id = -1
136
145
  stdout.puts "Shutting down workers..."
137
- client_writer.write({ method: :shutdown, id: shutdown_id })
138
- client_reader.read do |response|
139
- if response[:id] == shutdown_id
140
- break
141
- end
142
- end
143
- client_writer.write({ method: :exit })
144
- client_writer.io.close()
146
+ shutdown_exit(reader: client_reader, writer: client_writer)
145
147
  end
146
148
 
147
149
  listener.stop
@@ -4,10 +4,14 @@ module Steep
4
4
  LSP = LanguageServer::Protocol
5
5
  SymbolInformation = Struct.new(:name, :kind, :container_name, :location, keyword_init: true)
6
6
 
7
+ attr_reader :project
7
8
  attr_reader :indexes
9
+ attr_reader :assignment
8
10
 
9
- def initialize()
11
+ def initialize(project:, assignment:)
10
12
  @indexes = []
13
+ @project = project
14
+ @assignment = assignment
11
15
  end
12
16
 
13
17
  def self.test_type_name(query, type_name)
@@ -33,7 +37,17 @@ module Steep
33
37
  end
34
38
  end
35
39
 
36
- def query_symbol(query, assignment:)
40
+ def assigned?(path)
41
+ if path.relative?
42
+ if project.targets.any? {|target| target.possible_signature_file?(path) }
43
+ path = project.absolute_path(path)
44
+ end
45
+ end
46
+
47
+ assignment =~ path
48
+ end
49
+
50
+ def query_symbol(query)
37
51
  symbols = []
38
52
 
39
53
  indexes.each do |index|
@@ -46,7 +60,7 @@ module Steep
46
60
  name = entry.type_name.name.to_s
47
61
 
48
62
  entry.declarations.each do |decl|
49
- next unless assignment =~ decl.location.buffer.name
63
+ next unless assigned?(Pathname(decl.location.buffer.name))
50
64
 
51
65
  case decl
52
66
  when RBS::AST::Declarations::Class
@@ -91,7 +105,7 @@ module Steep
91
105
  container_name = entry.method_name.type_name.relative!.to_s
92
106
 
93
107
  entry.declarations.each do |decl|
94
- next unless assignment =~ decl.location.buffer.name
108
+ next unless assigned?(Pathname(decl.location.buffer.name))
95
109
 
96
110
  case decl
97
111
  when RBS::AST::Members::MethodDefinition
@@ -130,7 +144,7 @@ module Steep
130
144
  next unless SignatureSymbolProvider.test_const_name(query, entry.const_name)
131
145
 
132
146
  entry.declarations.each do |decl|
133
- next unless assignment =~ decl.location.buffer.name
147
+ next unless assigned?(Pathname(decl.location.buffer.name))
134
148
 
135
149
  symbols << SymbolInformation.new(
136
150
  name: entry.const_name.name.to_s,
@@ -143,7 +157,7 @@ module Steep
143
157
  next unless SignatureSymbolProvider.test_global_name(query, entry.global_name)
144
158
 
145
159
  entry.declarations.each do |decl|
146
- next unless assignment =~ decl.location.buffer.name
160
+ next unless assigned?(Pathname(decl.location.buffer.name))
147
161
 
148
162
  symbols << SymbolInformation.new(
149
163
  name: decl.name.to_s,
@@ -25,14 +25,14 @@ module Steep
25
25
  signature_pattern =~ path
26
26
  end
27
27
 
28
- def new_env_loader
29
- Target.construct_env_loader(options: options)
28
+ def new_env_loader(project:)
29
+ Target.construct_env_loader(options: options, project: project)
30
30
  end
31
31
 
32
- def self.construct_env_loader(options:)
32
+ def self.construct_env_loader(options:, project:)
33
33
  repo = RBS::Repository.new(no_stdlib: options.vendor_path)
34
34
  options.repository_paths.each do |path|
35
- repo.add(path)
35
+ repo.add(project.absolute_path(path))
36
36
  end
37
37
 
38
38
  loader = RBS::EnvironmentLoader.new(
@@ -12,8 +12,7 @@ module Steep
12
12
  def initialize(project:, reader:, writer:, queue: Queue.new)
13
13
  super(project: project, reader: reader, writer: writer)
14
14
  @queue = queue
15
- @service = Services::TypeCheckService.new(project: project, assignment: Services::PathAssignment.all)
16
- service.no_type_checking!
15
+ @service = Services::TypeCheckService.new(project: project)
17
16
  @mutex = Mutex.new
18
17
  @buffered_changes = {}
19
18
  end
@@ -24,7 +23,7 @@ module Steep
24
23
 
25
24
  unless changes.empty?
26
25
  Steep.logger.debug { "Applying changes for #{changes.size} files..." }
27
- service.update(changes: changes) {}
26
+ service.update(changes: changes)
28
27
  end
29
28
 
30
29
  case job
@@ -3,109 +3,372 @@ module Steep
3
3
  class Master
4
4
  LSP = LanguageServer::Protocol
5
5
 
6
- attr_reader :steepfile
7
- attr_reader :project
8
- attr_reader :reader, :writer
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
9
22
 
10
- attr_reader :interaction_worker
11
- attr_reader :typecheck_workers
23
+ def uri(path)
24
+ URI.parse(path.to_s).tap do |uri|
25
+ uri.scheme = "file"
26
+ end
27
+ end
12
28
 
13
- attr_reader :response_handlers
14
-
15
- # There are four types of threads:
16
- #
17
- # 1. Main thread -- Reads messages from client
18
- # 2. Worker threads -- Reads messages from associated worker
19
- # 3. Reconciliation thread -- Receives message from worker threads, reconciles, processes, and forwards to write thread
20
- # 4. Write thread -- Writes messages to client
21
- #
22
- # We have two queues:
23
- #
24
- # 1. `recon_queue` is to pass messages from worker threads to reconciliation thread
25
- # 2. `write` thread is to pass messages to write thread
26
- #
27
- # Message passing: Client -> Server (Master) -> Worker
28
- #
29
- # 1. Client -> Server
30
- # Master receives messages from the LSP client on main thread.
31
- #
32
- # 2. Master -> Worker
33
- # Master writes messages to workers on main thread.
34
- #
35
- # Message passing: Worker -> Server (Master) -> (reconciliation queue) -> (write queue) -> Client
36
- #
37
- # 3. Worker -> Master
38
- # Master receives messages on threads dedicated for each worker.
39
- # The messages sent from workers are then forwarded to the reconciliation thread through reconciliation queue.
40
- #
41
- # 4. Server -> Client
42
- # The reconciliation thread reads messages from reconciliation queue, does something, and finally sends messages to the client via write queue.
43
- #
44
- attr_reader :write_queue
45
- attr_reader :recon_queue
46
-
47
- class ResponseHandler
48
- attr_reader :workers
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
49
38
 
50
- attr_reader :request
51
- attr_reader :responses
39
+ def total
40
+ library_paths.size + signature_paths.size + code_paths.size
41
+ end
52
42
 
53
- attr_reader :on_response_handlers
54
- attr_reader :on_completion_handlers
43
+ def percentage
44
+ checked_paths.size * 100 / total
45
+ end
55
46
 
56
- def initialize(request:, workers:)
57
- @workers = []
47
+ def all_paths
48
+ library_paths + signature_paths + code_paths
49
+ end
58
50
 
59
- @request = request
60
- @responses = workers.each.with_object({}) do |worker, hash|
61
- hash[worker] = nil
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[]
62
102
  end
63
103
 
64
- @on_response_handlers = []
65
- @on_completion_handlers = []
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
133
+ end
134
+
135
+ alias << add
66
136
  end
67
137
 
68
- def on_response(&block)
69
- on_response_handlers << block
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) }
70
143
  end
71
144
 
72
- def on_completion(&block)
73
- on_completion_handlers << block
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
74
163
  end
75
164
 
76
- def request_id
77
- request[:id]
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
78
173
  end
79
174
 
80
- def process_response(response, worker)
81
- responses[worker] = response
175
+ def update_priority(open: nil, close: nil)
176
+ path = open || close
177
+
178
+ target_paths.each {|paths| paths << path }
82
179
 
83
- on_response_handlers.each do |handler|
84
- handler[worker, response]
180
+ case
181
+ when open
182
+ priority_paths << path
183
+ when close
184
+ priority_paths.delete path
85
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
190
+
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)
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
86
247
 
87
- if completed?
88
- on_completion_handlers.each do |handler|
89
- handler[*responses.values]
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)
90
278
  end
279
+
280
+ true
281
+ else
282
+ false
91
283
  end
92
284
  end
93
285
 
94
286
  def completed?
95
- responses.each_value.none?(&:nil?)
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?)
96
323
  end
97
324
  end
98
325
 
326
+ ReceiveMessageJob = Struct.new(:source, :message, keyword_init: true) do
327
+ def response?
328
+ message.key?(:id) && !message.key?(:method)
329
+ end
330
+ end
331
+
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
+
99
359
  def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new)
100
360
  @project = project
101
361
  @reader = reader
102
362
  @writer = writer
103
- @write_queue = queue
104
- @recon_queue = Queue.new
105
363
  @interaction_worker = interaction_worker
106
364
  @typecheck_workers = typecheck_workers
107
- @shutdown_request_id = nil
108
- @response_handlers = {}
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()
109
372
  end
110
373
 
111
374
  def start
@@ -118,7 +381,7 @@ module Steep
118
381
  worker_threads << Thread.new do
119
382
  Steep.logger.formatter.push_tags(*tags, "from-worker@interaction")
120
383
  interaction_worker.reader.read do |message|
121
- process_message_from_worker(message, worker: interaction_worker)
384
+ job_queue << ReceiveMessageJob.new(source: interaction_worker, message: message)
122
385
  end
123
386
  end
124
387
  end
@@ -127,42 +390,57 @@ module Steep
127
390
  worker_threads << Thread.new do
128
391
  Steep.logger.formatter.push_tags(*tags, "from-worker@#{worker.name}")
129
392
  worker.reader.read do |message|
130
- process_message_from_worker(message, worker: worker)
393
+ job_queue << ReceiveMessageJob.new(source: worker, message: message)
131
394
  end
132
395
  end
133
396
  end
134
397
 
135
- worker_threads << Thread.new do
136
- Steep.logger.formatter.push_tags(*tags, "write")
137
- while message = write_queue.pop
138
- writer.write(message)
139
- end
140
-
141
- writer.io.close
142
- end
143
-
144
- worker_threads << Thread.new do
145
- Steep.logger.formatter.push_tags(*tags, "reconciliation")
146
- while (message, worker = recon_queue.pop)
147
- id = message[:id]
148
- handler = response_handlers[id] or raise
149
-
150
- Steep.logger.info "Processing response to #{handler.request[:method]}(#{id}) from #{worker.name}"
151
-
152
- handler.process_response(message, worker)
153
-
154
- if handler.completed?
155
- Steep.logger.info "Response to #{handler.request[:method]}(#{id}) completed"
156
- response_handlers.delete(id)
157
- 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"
158
402
  end
159
403
  end
160
404
 
161
405
  Steep.logger.tagged "main" do
162
- reader.read do |request|
163
- process_message_from_client(request) or break
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
164
441
  end
165
442
 
443
+ read_client_thread.join()
166
444
  worker_threads.each do |thread|
167
445
  thread.join
168
446
  end
@@ -179,138 +457,311 @@ module Steep
179
457
  end
180
458
  end
181
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
+
182
468
  def process_message_from_client(message)
183
- Steep.logger.info "Received message #{message[:method]}(#{message[:id]})"
469
+ Steep.logger.info "Processing message from client: method=#{message[:method]}, id=#{message[:id]}"
184
470
  id = message[:id]
185
471
 
186
472
  case message[:method]
187
473
  when "initialize"
188
- broadcast_request(message) do |handler|
189
- handler.on_completion do
190
- write_queue << {
191
- id: id,
192
- result: LSP::Interface::InitializeResult.new(
193
- capabilities: LSP::Interface::ServerCapabilities.new(
194
- text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
195
- change: LSP::Constant::TextDocumentSyncKind::INCREMENTAL
196
- ),
197
- hover_provider: true,
198
- completion_provider: LSP::Interface::CompletionOptions.new(
199
- trigger_characters: [".", "@"]
200
- ),
201
- workspace_symbol_provider: true
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
+ )
202
504
  )
203
- )
204
- }
505
+ }
506
+ )
205
507
  end
206
508
  end
207
509
 
510
+ when "initialized"
511
+ if typecheck_automatically
512
+ request = controller.make_request(include_unchanged: true)
513
+ start_type_check(request, last_request: nil, start_progress: request.total > 10)
514
+ end
515
+
208
516
  when "textDocument/didChange"
209
517
  broadcast_notification(message)
518
+ path = pathname(message[:params][:textDocument][:uri])
519
+ controller.push_changes(path)
520
+
521
+ when "textDocument/didSave"
522
+ if typecheck_automatically
523
+ request = controller.make_request(last_request: current_type_check_request)
524
+ start_type_check(
525
+ request,
526
+ last_request: current_type_check_request,
527
+ start_progress: request.total > 10
528
+ )
529
+ end
530
+
531
+ when "textDocument/didOpen"
532
+ path = pathname(message[:params][:textDocument][:uri])
533
+ controller.update_priority(open: path)
534
+
535
+ when "textDocument/didClose"
536
+ path = pathname(message[:params][:textDocument][:uri])
537
+ controller.update_priority(close: path)
210
538
 
211
539
  when "textDocument/hover", "textDocument/completion"
212
540
  if interaction_worker
213
- send_request(message, worker: interaction_worker) do |handler|
541
+ result_controller << send_request(method: message[:method], params: message[:params], worker: interaction_worker) do |handler|
214
542
  handler.on_completion do |response|
215
- write_queue << response
543
+ job_queue << SendMessageJob.to_client(
544
+ message: {
545
+ id: message[:id],
546
+ result: response[:result]
547
+ }
548
+ )
216
549
  end
217
550
  end
218
551
  end
219
552
 
220
- when "textDocument/open"
221
- # Ignores open notification
222
-
223
553
  when "workspace/symbol"
224
- send_request(message, workers: typecheck_workers) do |handler|
225
- handler.on_completion do |*responses|
226
- result = responses.flat_map {|resp| resp[:result] || [] }
554
+ result_controller << group_request do |group|
555
+ typecheck_workers.each do |worker|
556
+ group << send_request(method: "workspace/symbol", params: message[:params], worker: worker)
557
+ end
227
558
 
228
- write_queue << {
229
- id: handler.request_id,
230
- result: result
231
- }
559
+ group.on_completion do |handlers|
560
+ result = handlers.flat_map(&:result)
561
+ job_queue << SendMessageJob.to_client(message: { id: message[:id], result: result })
232
562
  end
233
563
  end
234
564
 
235
565
  when "workspace/executeCommand"
236
566
  case message[:params][:command]
237
567
  when "steep/stats"
238
- send_request(message, workers: typecheck_workers) do |handler|
239
- handler.on_completion do |*responses|
240
- stats = responses.flat_map {|resp| resp[:result] }
241
- write_queue << {
242
- id: handler.request_id,
243
- result: stats
244
- }
568
+ result_controller << group_request do |group|
569
+ typecheck_workers.each do |worker|
570
+ group << send_request(method: "workspace/executeCommand", params: message[:params], worker: worker)
571
+ end
572
+
573
+ group.on_completion do |handlers|
574
+ stats = handlers.flat_map(&:result)
575
+ job_queue << SendMessageJob.to_client(
576
+ message: {
577
+ id: message[:id],
578
+ result: stats
579
+ }
580
+ )
245
581
  end
246
582
  end
247
583
  end
248
584
 
585
+ when "$/typecheck"
586
+ request = controller.make_request(
587
+ guid: message[:params][:guid],
588
+ last_request: current_type_check_request,
589
+ include_unchanged: true
590
+ )
591
+ start_type_check(
592
+ request,
593
+ last_request: current_type_check_request,
594
+ start_progress: true
595
+ )
596
+
249
597
  when "shutdown"
250
- broadcast_request(message) do |handler|
251
- handler.on_completion do |*_|
252
- write_queue << { id: id, result: nil}
598
+ result_controller << group_request do |group|
599
+ each_worker do |worker|
600
+ group << send_request(method: "shutdown", worker: worker)
601
+ end
253
602
 
254
- write_queue.close
255
- recon_queue.close
603
+ group.on_completion do
604
+ job_queue << SendMessageJob.to_client(message: { id: message[:id], result: nil })
256
605
  end
257
606
  end
258
607
 
259
608
  when "exit"
260
609
  broadcast_notification(message)
610
+ end
611
+ end
612
+
613
+ def process_message_from_worker(message, worker:)
614
+ Steep.logger.tagged "#process_message_from_worker (worker=#{worker.name})" do
615
+ Steep.logger.info { "Processing message from worker: method=#{message[:method] || "-"}, id=#{message[:id] || "*"}" }
616
+
617
+ case
618
+ when message.key?(:id) && !message.key?(:method)
619
+ Steep.logger.tagged "response(id=#{message[:id]})" do
620
+ Steep.logger.error { "Received unexpected response" }
621
+ Steep.logger.debug { "result = #{message[:result].inspect}" }
622
+ end
623
+ when message.key?(:method) && !message.key?(:id)
624
+ case message[:method]
625
+ when "$/typecheck/progress"
626
+ on_type_check_update(
627
+ guid: message[:params][:guid],
628
+ path: Pathname(message[:params][:path])
629
+ )
630
+ else
631
+ # Forward other notifications
632
+ job_queue << SendMessageJob.to_client(message: message)
633
+ end
634
+ end
635
+ end
636
+ end
637
+
638
+ def start_type_check(request, last_request:, start_progress:)
639
+ Steep.logger.tagged "#start_type_check(#{request.guid}, #{last_request&.guid}" do
640
+ unless request
641
+ Steep.logger.info "Skip start type checking"
642
+ return
643
+ end
644
+
645
+ if last_request
646
+ Steep.logger.info "Cancelling last request"
647
+
648
+ job_queue << SendMessageJob.to_client(
649
+ message: {
650
+ method: "$/progress",
651
+ params: {
652
+ token: last_request.guid,
653
+ value: { kind: "end" }
654
+ }
655
+ }
656
+ )
657
+ end
658
+
659
+ if start_progress
660
+ Steep.logger.info "Starting new progress..."
261
661
 
262
- return false
662
+ @current_type_check_request = request
663
+
664
+ if work_done_progress_supported?
665
+ job_queue << SendMessageJob.to_client(
666
+ message: {
667
+ id: fresh_request_id,
668
+ method: "window/workDoneProgress/create",
669
+ params: { token: request.guid }
670
+ }
671
+ )
672
+ end
673
+
674
+ job_queue << SendMessageJob.to_client(
675
+ message: {
676
+ method: "$/progress",
677
+ params: {
678
+ token: request.guid,
679
+ value: { kind: "begin", title: "Type checking", percentage: 0 }
680
+ }
681
+ }
682
+ )
683
+
684
+ if request.finished?
685
+ job_queue << SendMessageJob.to_client(
686
+ message: {
687
+ method: "$/progress",
688
+ params: { token: request.guid, value: { kind: "end" } }
689
+ }
690
+ )
691
+ end
692
+ else
693
+ @current_type_check_request = nil
694
+ end
695
+
696
+ Steep.logger.info "Sending $/typecheck/start notifications"
697
+ typecheck_workers.each do |worker|
698
+ assignment = Services::PathAssignment.new(max_index: typecheck_workers.size, index: worker.index)
699
+
700
+ job_queue << SendMessageJob.to_worker(
701
+ worker,
702
+ message: {
703
+ method: "$/typecheck/start",
704
+ params: request.as_json(assignment: assignment)
705
+ }
706
+ )
707
+ end
263
708
  end
709
+ end
264
710
 
265
- true
711
+ def on_type_check_update(guid:, path:)
712
+ if current = current_type_check_request()
713
+ if current.guid == guid
714
+ current.checked(path)
715
+ Steep.logger.info { "Request updated: checked=#{path}, unchecked=#{current.unchecked_paths.size}" }
716
+ percentage = current.percentage
717
+ value = if percentage == 100
718
+ { kind: "end" }
719
+ else
720
+ progress_string = ("▮"*(percentage/5)) + ("▯"*(20 - percentage/5))
721
+ { kind: "report", percentage: percentage, message: "#{progress_string} (#{percentage}%)" }
722
+ end
723
+
724
+ job_queue << SendMessageJob.to_client(
725
+ message: {
726
+ method: "$/progress",
727
+ params: { token: current.guid, value: value }
728
+ }
729
+ )
730
+
731
+ @current_type_check_request = nil if current.finished?
732
+ end
733
+ end
266
734
  end
267
735
 
268
736
  def broadcast_notification(message)
269
737
  Steep.logger.info "Broadcasting notification #{message[:method]}"
270
738
  each_worker do |worker|
271
- worker << message
739
+ job_queue << SendMessageJob.new(dest: worker, message: message)
272
740
  end
273
741
  end
274
742
 
275
743
  def send_notification(message, worker:)
276
744
  Steep.logger.info "Sending notification #{message[:method]} to #{worker.name}"
277
- worker << message
745
+ job_queue << SendMessageJob.new(dest: worker, message: message)
278
746
  end
279
747
 
280
- def send_request(message, worker: nil, workers: [])
281
- workers << worker if worker
282
-
283
- Steep.logger.info "Sending request #{message[:method]}(#{message[:id]}) to #{workers.map(&:name).join(", ")}"
284
- handler = ResponseHandler.new(request: message, workers: workers)
285
- yield(handler) if block_given?
286
- response_handlers[handler.request_id] = handler
287
-
288
- workers.each do |w|
289
- w << message
290
- end
748
+ def fresh_request_id
749
+ SecureRandom.alphanumeric(10)
291
750
  end
292
751
 
293
- def broadcast_request(message)
294
- Steep.logger.info "Broadcasting request #{message[:method]}(#{message[:id]})"
295
- handler = ResponseHandler.new(request: message, workers: each_worker.to_a)
296
- yield(handler) if block_given?
297
- response_handlers[handler.request_id] = handler
752
+ def send_request(method:, id: fresh_request_id(), params: nil, worker:, &block)
753
+ Steep.logger.info "Sending request #{method}(#{id}) to #{worker.name}"
298
754
 
299
- each_worker do |worker|
300
- worker << message
755
+ message = { method: method, id: id, params: params }
756
+ ResultHandler.new(request: message).tap do |handler|
757
+ yield handler if block_given?
758
+ job_queue << SendMessageJob.to_worker(worker, message: message)
301
759
  end
302
760
  end
303
761
 
304
- def process_message_from_worker(message, worker:)
305
- case
306
- when message.key?(:id) && !message.key?(:method)
307
- # Response from worker
308
- Steep.logger.debug { "Received response #{message[:id]} from worker" }
309
- recon_queue << [message, worker]
310
- when message.key?(:method) && !message.key?(:id)
311
- # Notification from worker
312
- Steep.logger.debug { "Received notification #{message[:method]} from worker" }
313
- write_queue << message
762
+ def group_request()
763
+ GroupHandler.new().tap do |group|
764
+ yield group
314
765
  end
315
766
  end
316
767