steep 0.40.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile +1 -0
  4. data/bin/output_rebaseline.rb +15 -30
  5. data/bin/output_test.rb +23 -57
  6. data/lib/steep.rb +89 -15
  7. data/lib/steep/annotation_parser.rb +10 -2
  8. data/lib/steep/ast/types/class.rb +4 -0
  9. data/lib/steep/cli.rb +31 -6
  10. data/lib/steep/diagnostic/ruby.rb +13 -8
  11. data/lib/steep/diagnostic/signature.rb +152 -2
  12. data/lib/steep/drivers/annotations.rb +18 -36
  13. data/lib/steep/drivers/check.rb +140 -31
  14. data/lib/steep/drivers/diagnostic_printer.rb +20 -11
  15. data/lib/steep/drivers/langserver.rb +4 -8
  16. data/lib/steep/drivers/print_project.rb +10 -9
  17. data/lib/steep/drivers/stats.rb +135 -119
  18. data/lib/steep/drivers/utils/driver_helper.rb +35 -0
  19. data/lib/steep/drivers/utils/jobs_count.rb +9 -0
  20. data/lib/steep/drivers/validate.rb +29 -18
  21. data/lib/steep/drivers/watch.rb +55 -49
  22. data/lib/steep/drivers/worker.rb +11 -8
  23. data/lib/steep/expectations.rb +159 -0
  24. data/lib/steep/index/signature_symbol_provider.rb +23 -1
  25. data/lib/steep/index/source_index.rb +55 -5
  26. data/lib/steep/interface/block.rb +4 -0
  27. data/lib/steep/project.rb +0 -30
  28. data/lib/steep/project/dsl.rb +5 -3
  29. data/lib/steep/project/pattern.rb +56 -0
  30. data/lib/steep/project/target.rb +11 -227
  31. data/lib/steep/server/base_worker.rb +1 -3
  32. data/lib/steep/server/change_buffer.rb +63 -0
  33. data/lib/steep/server/interaction_worker.rb +72 -57
  34. data/lib/steep/server/master.rb +652 -234
  35. data/lib/steep/server/type_check_worker.rb +304 -0
  36. data/lib/steep/server/worker_process.rb +16 -11
  37. data/lib/steep/{project → services}/completion_provider.rb +5 -5
  38. data/lib/steep/services/content_change.rb +61 -0
  39. data/lib/steep/services/file_loader.rb +48 -0
  40. data/lib/steep/services/goto_service.rb +321 -0
  41. data/lib/steep/{project → services}/hover_content.rb +19 -20
  42. data/lib/steep/services/path_assignment.rb +27 -0
  43. data/lib/steep/services/signature_service.rb +403 -0
  44. data/lib/steep/services/stats_calculator.rb +69 -0
  45. data/lib/steep/services/type_check_service.rb +413 -0
  46. data/lib/steep/signature/validator.rb +187 -85
  47. data/lib/steep/source.rb +21 -18
  48. data/lib/steep/subtyping/check.rb +246 -45
  49. data/lib/steep/subtyping/constraints.rb +4 -4
  50. data/lib/steep/type_construction.rb +428 -193
  51. data/lib/steep/type_inference/block_params.rb +1 -1
  52. data/lib/steep/type_inference/context.rb +22 -0
  53. data/lib/steep/type_inference/local_variable_type_env.rb +26 -12
  54. data/lib/steep/type_inference/logic.rb +1 -1
  55. data/lib/steep/type_inference/logic_type_interpreter.rb +4 -4
  56. data/lib/steep/type_inference/type_env.rb +43 -17
  57. data/lib/steep/version.rb +1 -1
  58. data/smoke/alias/test_expectations.yml +96 -0
  59. data/smoke/and/test_expectations.yml +31 -0
  60. data/smoke/array/test_expectations.yml +103 -0
  61. data/smoke/block/test_expectations.yml +125 -0
  62. data/smoke/case/test_expectations.yml +47 -0
  63. data/smoke/class/test_expectations.yml +120 -0
  64. data/smoke/const/test_expectations.yml +129 -0
  65. data/smoke/diagnostics-rbs-duplicated/test_expectations.yml +13 -0
  66. data/smoke/diagnostics-rbs/Steepfile +7 -4
  67. data/smoke/diagnostics-rbs/test_expectations.yml +231 -0
  68. data/smoke/diagnostics-rbs/unknown-type-name-2.rbs +5 -0
  69. data/smoke/{broken → diagnostics-ruby-unsat}/Steepfile +0 -0
  70. data/smoke/diagnostics-ruby-unsat/a.rbs +3 -0
  71. data/smoke/diagnostics-ruby-unsat/test_expectations.yml +27 -0
  72. data/smoke/{diagnostics → diagnostics-ruby-unsat}/unsatisfiable_constraint.rb +0 -1
  73. data/smoke/diagnostics/a.rbs +0 -4
  74. data/smoke/diagnostics/test_expectations.yml +451 -0
  75. data/smoke/dstr/test_expectations.yml +13 -0
  76. data/smoke/ensure/test_expectations.yml +62 -0
  77. data/smoke/enumerator/test_expectations.yml +135 -0
  78. data/smoke/extension/f.rb +2 -0
  79. data/smoke/extension/f.rbs +3 -0
  80. data/smoke/extension/test_expectations.yml +73 -0
  81. data/smoke/hash/test_expectations.yml +81 -0
  82. data/smoke/hello/test_expectations.yml +25 -0
  83. data/smoke/if/test_expectations.yml +34 -0
  84. data/smoke/implements/b.rb +13 -0
  85. data/smoke/implements/b.rbs +12 -0
  86. data/smoke/implements/test_expectations.yml +23 -0
  87. data/smoke/initialize/test_expectations.yml +1 -0
  88. data/smoke/integer/test_expectations.yml +101 -0
  89. data/smoke/interface/test_expectations.yml +23 -0
  90. data/smoke/kwbegin/test_expectations.yml +17 -0
  91. data/smoke/lambda/test_expectations.yml +39 -0
  92. data/smoke/literal/test_expectations.yml +106 -0
  93. data/smoke/map/test_expectations.yml +1 -0
  94. data/smoke/method/test_expectations.yml +90 -0
  95. data/smoke/module/test_expectations.yml +75 -0
  96. data/smoke/regexp/test_expectations.yml +615 -0
  97. data/smoke/regression/issue_328.rb +1 -0
  98. data/smoke/regression/issue_328.rbs +0 -0
  99. data/smoke/regression/issue_332.rb +11 -0
  100. data/smoke/regression/issue_332.rbs +19 -0
  101. data/smoke/regression/issue_372.rb +8 -0
  102. data/smoke/regression/issue_372.rbs +4 -0
  103. data/smoke/regression/masgn.rb +4 -0
  104. data/smoke/regression/test_expectations.yml +60 -0
  105. data/smoke/regression/thread.rb +7 -0
  106. data/smoke/rescue/test_expectations.yml +79 -0
  107. data/smoke/self/test_expectations.yml +23 -0
  108. data/smoke/skip/test_expectations.yml +23 -0
  109. data/smoke/stdout/test_expectations.yml +1 -0
  110. data/smoke/super/test_expectations.yml +69 -0
  111. data/smoke/toplevel/test_expectations.yml +15 -0
  112. data/smoke/tsort/Steepfile +2 -0
  113. data/smoke/tsort/test_expectations.yml +63 -0
  114. data/smoke/type_case/test_expectations.yml +48 -0
  115. data/smoke/unexpected/Steepfile +5 -0
  116. data/smoke/unexpected/test_expectations.yml +25 -0
  117. data/smoke/unexpected/unexpected.rb +1 -0
  118. data/smoke/unexpected/unexpected.rbs +3 -0
  119. data/smoke/yield/test_expectations.yml +68 -0
  120. data/steep.gemspec +4 -3
  121. metadata +127 -80
  122. data/lib/steep/project/file_loader.rb +0 -68
  123. data/lib/steep/project/signature_file.rb +0 -39
  124. data/lib/steep/project/source_file.rb +0 -129
  125. data/lib/steep/project/stats_calculator.rb +0 -80
  126. data/lib/steep/server/code_worker.rb +0 -150
  127. data/lib/steep/server/signature_worker.rb +0 -157
  128. data/lib/steep/server/utils.rb +0 -69
  129. data/smoke/alias/test.yaml +0 -73
  130. data/smoke/and/test.yaml +0 -24
  131. data/smoke/array/test.yaml +0 -80
  132. data/smoke/block/test.yaml +0 -96
  133. data/smoke/broken/broken.rb +0 -0
  134. data/smoke/broken/broken.rbs +0 -0
  135. data/smoke/broken/test.yaml +0 -6
  136. data/smoke/case/test.yaml +0 -36
  137. data/smoke/class/test.yaml +0 -89
  138. data/smoke/const/test.yaml +0 -96
  139. data/smoke/diagnostics-rbs-duplicated/test.yaml +0 -10
  140. data/smoke/diagnostics-rbs/test.yaml +0 -142
  141. data/smoke/diagnostics/test.yaml +0 -333
  142. data/smoke/dstr/test.yaml +0 -10
  143. data/smoke/ensure/test.yaml +0 -47
  144. data/smoke/enumerator/test.yaml +0 -100
  145. data/smoke/extension/test.yaml +0 -50
  146. data/smoke/hash/test.yaml +0 -62
  147. data/smoke/hello/test.yaml +0 -18
  148. data/smoke/if/test.yaml +0 -27
  149. data/smoke/implements/test.yaml +0 -16
  150. data/smoke/initialize/test.yaml +0 -4
  151. data/smoke/integer/test.yaml +0 -66
  152. data/smoke/interface/test.yaml +0 -16
  153. data/smoke/kwbegin/test.yaml +0 -14
  154. data/smoke/lambda/test.yaml +0 -28
  155. data/smoke/literal/test.yaml +0 -79
  156. data/smoke/map/test.yaml +0 -4
  157. data/smoke/method/test.yaml +0 -71
  158. data/smoke/module/test.yaml +0 -51
  159. data/smoke/regexp/test.yaml +0 -372
  160. data/smoke/regression/test.yaml +0 -38
  161. data/smoke/rescue/test.yaml +0 -60
  162. data/smoke/self/test.yaml +0 -16
  163. data/smoke/skip/test.yaml +0 -16
  164. data/smoke/stdout/test.yaml +0 -4
  165. data/smoke/super/test.yaml +0 -52
  166. data/smoke/toplevel/test.yaml +0 -12
  167. data/smoke/tsort/test.yaml +0 -32
  168. data/smoke/type_case/test.yaml +0 -33
  169. data/smoke/yield/test.yaml +0 -49
@@ -3,10 +3,8 @@ module Steep
3
3
  class BaseWorker
4
4
  LSP = LanguageServer::Protocol
5
5
 
6
- include Utils
7
-
8
6
  attr_reader :project
9
- attr_reader :reader, :writer
7
+ attr_reader :reader, :writer, :queue
10
8
 
11
9
  ShutdownJob = Struct.new(:id, keyword_init: true)
12
10
 
@@ -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
- attr_reader :queue
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.debug "Handling job: id=#{job[:id]}, result=#{job[:result]&.to_hash}"
13
- writer.write(job)
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
- # nop
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
- update_source(request)
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[:context][:triggerCharacter]
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 response_to_hover(path:, line:, column:)
55
- Steep.logger.tagged "#response_to_hover" do
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 = Project::HoverContent.new(project: project)
60
- content = hover.content_for(path: path, line: line+1, column: column+1)
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 Project::HoverContent::VariableContent
102
+ when Services::HoverContent::VariableContent
83
103
  "`#{content.name}`: `#{content.type.to_s}`"
84
- when Project::HoverContent::MethodCallContent
104
+ when Services::HoverContent::MethodCallContent
85
105
  method_name = case content.method_name
86
- when Project::HoverContent::InstanceMethodName
106
+ when Services::HoverContent::InstanceMethodName
87
107
  "#{content.method_name.class_name}##{content.method_name.method_name}"
88
- when Project::HoverContent::SingletonMethodName
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 Project::HoverContent::DefinitionContent
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 Project::HoverContent::TypeContent
145
+ when Services::HoverContent::TypeContent
126
146
  "`#{content.type}`"
127
147
  end
128
148
  end
129
149
 
130
- def response_to_completion(path:, line:, column:, trigger:)
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
- case (status = target&.status)
139
- when Project::Target::TypeCheckStatus
140
- subtyping = status.subtyping
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
- provider = Project::CompletionProvider.new(source_text: source.content, path: path, subtyping: subtyping)
144
- items = begin
145
- provider.run(line: line, column: column)
146
- rescue Parser::SyntaxError
147
- []
148
- end
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
- completion_items = items.map do |item|
151
- format_completion_item(item)
152
- end
166
+ completion_items = items.map do |item|
167
+ format_completion_item(item)
168
+ end
153
169
 
154
- Steep.logger.debug "items = #{completion_items.inspect}"
170
+ Steep.logger.debug "items = #{completion_items.inspect}"
155
171
 
156
- LSP::Interface::CompletionList.new(
157
- is_incomplete: false,
158
- items: completion_items
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 Project::CompletionProvider::LocalVariableItem
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 Project::CompletionProvider::MethodNameItem
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 Project::CompletionProvider::InstanceVariableItem
217
+ when Services::CompletionProvider::InstanceVariableItem
203
218
  label = "#{item.identifier}: #{item.type}"
204
219
  LanguageServer::Protocol::Interface::CompletionItem.new(
205
220
  label: label,
@@ -3,185 +3,444 @@ 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
9
- attr_reader :worker_count
10
- attr_reader :worker_to_paths
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
- attr_reader :interaction_worker
13
- attr_reader :signature_worker
14
- attr_reader :code_workers
15
-
16
- attr_reader :response_handlers
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
- attr_reader :request
56
- attr_reader :responses
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
- attr_reader :on_response_handlers
59
- attr_reader :on_completion_handlers
39
+ def total
40
+ library_paths.size + signature_paths.size + code_paths.size
41
+ end
60
42
 
61
- def initialize(request:, workers:)
62
- @workers = []
43
+ def percentage
44
+ checked_paths.size * 100 / total
45
+ end
63
46
 
64
- @request = request
65
- @responses = workers.each.with_object({}) do |worker, hash|
66
- hash[worker] = nil
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
- @on_response_handlers = []
70
- @on_completion_handlers = []
135
+ alias << add
71
136
  end
72
137
 
73
- def on_response(&block)
74
- 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) }
75
143
  end
76
144
 
77
- def on_completion(&block)
78
- 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
79
163
  end
80
164
 
81
- def request_id
82
- 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
83
173
  end
84
174
 
85
- def process_response(response, worker)
86
- responses[worker] = response
175
+ def update_priority(open: nil, close: nil)
176
+ path = open || close
87
177
 
88
- on_response_handlers.each do |handler|
89
- handler[worker, response]
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
- if completed?
93
- on_completion_handlers.each do |handler|
94
- handler[*responses.values]
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
- 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?)
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
- def initialize(project:, reader:, writer:, interaction_worker:, signature_worker:, code_workers:, queue: Queue.new)
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
- @signature_worker = signature_worker
112
- @code_workers = code_workers
113
- @worker_to_paths = {}
114
- @shutdown_request_id = nil
115
- @response_handlers = {}
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
- worker_threads << Thread.new do
132
- Steep.logger.formatter.push_tags(*tags, "from-worker@interaction")
133
- interaction_worker.reader.read do |message|
134
- process_message_from_worker(message, worker: interaction_worker)
135
- end
136
- end
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
- code_workers.each do |worker|
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
- process_message_from_worker(message, worker: worker)
393
+ job_queue << ReceiveMessageJob.new(source: worker, message: message)
150
394
  end
151
395
  end
152
396
  end
153
397
 
154
- worker_threads << Thread.new do
155
- Steep.logger.formatter.push_tags(*tags, "write")
156
- while message = write_queue.pop
157
- writer.write(message)
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
- reader.read do |request|
182
- 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
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
- yield signature_worker
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 "Received message #{message[:method]}(#{message[:id]})"
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
- broadcast_request(message) do |handler|
209
- handler.on_completion do
210
- write_queue << {
211
- id: id,
212
- result: LSP::Interface::InitializeResult.new(
213
- capabilities: LSP::Interface::ServerCapabilities.new(
214
- text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
215
- change: LSP::Constant::TextDocumentSyncKind::INCREMENTAL
216
- ),
217
- hover_provider: true,
218
- completion_provider: LSP::Interface::CompletionOptions.new(
219
- trigger_characters: [".", "@"]
220
- ),
221
- 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
+ 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 "textDocument/didChange"
229
- update_source(message)
230
-
231
- uri = URI.parse(message[:params][:textDocument][:uri])
232
- path = project.relative_path(Pathname(uri.path))
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
- send_request(message, worker: interaction_worker) do |handler|
242
- handler.on_completion do |response|
243
- write_queue << response
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
- send_request(message, worker: signature_worker) do |handler|
252
- handler.on_completion do |response|
253
- write_queue << response
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
- send_request(message, workers: code_workers) do |handler|
261
- handler.on_completion do |*responses|
262
- stats = responses.flat_map {|resp| resp[:result] }
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
- write_queue << {
265
- id: handler.request_id,
266
- result: stats
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
- broadcast_request(message) do |handler|
274
- handler.on_completion do |*_|
275
- write_queue << { id: id, result: nil}
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
- write_queue.close
278
- recon_queue.close
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 broadcast_notification(message)
292
- Steep.logger.info "Broadcasting notification #{message[:method]}"
293
- each_worker do |worker|
294
- worker << message
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 send_notification(message, worker:)
299
- Steep.logger.info "Sending notification #{message[:method]} to #{worker.name}"
300
- worker << message
301
- end
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
- def send_request(message, worker: nil, workers: [])
304
- workers << worker if worker
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
- Steep.logger.info "Sending request #{message[:method]}(#{message[:id]}) to #{workers.map(&:name).join(", ")}"
307
- handler = ResponseHandler.new(request: message, workers: workers)
308
- yield(handler) if block_given?
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
- workers.each do |w|
312
- w << message
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 broadcast_request(message)
317
- Steep.logger.info "Broadcasting request #{message[:method]}(#{message[:id]})"
318
- handler = ResponseHandler.new(request: message, workers: each_worker.to_a)
319
- yield(handler) if block_given?
320
- response_handlers[handler.request_id] = handler
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
- each_worker do |worker|
323
- worker << message
752
+ @current_type_check_request = nil if current.finished?
753
+ end
324
754
  end
325
755
  end
326
756
 
327
- def process_message_from_worker(message, worker:)
328
- case
329
- when message.key?(:id) && !message.key?(:method)
330
- # Response from worker
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 paths_for(worker)
341
- worker_to_paths[worker] ||= Set[]
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 least_busy_worker
345
- code_workers.min_by do |w|
346
- paths_for(w).size
347
- end
769
+ def fresh_request_id
770
+ SecureRandom.alphanumeric(10)
348
771
  end
349
772
 
350
- def registered_path?(path)
351
- worker_to_paths.each_value.any? {|set| set.include?(path) }
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
- def register_code_to_worker(paths, worker:)
355
- paths_for(worker).merge(paths)
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
- send_notification(
358
- {
359
- method: "workspace/executeCommand",
360
- params: LSP::Interface::ExecuteCommandParams.new(
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