steep 0.40.0 → 0.44.0

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