steep 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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