steep 0.15.0 → 0.16.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.
@@ -0,0 +1,281 @@
1
+ module Steep
2
+ module Server
3
+ class InteractionWorker < BaseWorker
4
+ attr_reader :queue
5
+
6
+ def initialize(project:, reader:, writer:, queue: Queue.new)
7
+ super(project: project, reader: reader, writer: writer)
8
+ @queue = queue
9
+ end
10
+
11
+ def handle_job(job)
12
+ Steep.logger.debug "Handling job: id=#{job[:id]}, result=#{job[:result]&.to_hash}"
13
+ writer.write(job)
14
+ end
15
+
16
+ def handle_request(request)
17
+ case request[:method]
18
+ when "initialize"
19
+ # nop
20
+
21
+ when "textDocument/didChange"
22
+ update_source(request)
23
+
24
+ when "textDocument/hover"
25
+ id = request[:id]
26
+
27
+ uri = URI.parse(request[:params][:textDocument][:uri])
28
+ path = project.relative_path(Pathname(uri.path))
29
+ line = request[:params][:position][:line]
30
+ column = request[:params][:position][:character]
31
+
32
+ queue << {
33
+ id: id,
34
+ result: response_to_hover(path: path, line: line, column: column)
35
+ }
36
+
37
+ when "textDocument/completion"
38
+ id = request[:id]
39
+
40
+ params = request[:params]
41
+ uri = URI.parse(params[:textDocument][:uri])
42
+ path = project.relative_path(Pathname(uri.path))
43
+ line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
44
+ trigger = params[:context][:triggerCharacter]
45
+
46
+ queue << {
47
+ id: id,
48
+ result: response_to_completion(path: path, line: line, column: column, trigger: trigger)
49
+ }
50
+ end
51
+ end
52
+
53
+ def response_to_hover(path:, line:, column:)
54
+ Steep.logger.tagged "#response_to_hover" do
55
+ Steep.logger.debug { "path=#{path}, line=#{line}, column=#{column}" }
56
+
57
+ hover = Project::HoverContent.new(project: project)
58
+ content = hover.content_for(path: path, line: line+1, column: column+1)
59
+ if content
60
+ range = content.location.yield_self do |location|
61
+ start_position = { line: location.line - 1, character: location.column }
62
+ end_position = { line: location.last_line - 1, character: location.last_column }
63
+ { start: start_position, end: end_position }
64
+ end
65
+
66
+ LSP::Interface::Hover.new(
67
+ contents: { kind: "markdown", value: format_hover(content) },
68
+ range: range
69
+ )
70
+ end
71
+ rescue Typing::UnknownNodeError => exn
72
+ Steep.log_error exn, message: "Failed to compute hover: #{exn.inspect}"
73
+ nil
74
+ end
75
+ end
76
+
77
+ def format_hover(content)
78
+ case content
79
+ when Project::HoverContent::VariableContent
80
+ "`#{content.name}`: `#{content.type.to_s}`"
81
+ when Project::HoverContent::MethodCallContent
82
+ method_name = case content.method_name
83
+ when Project::HoverContent::InstanceMethodName
84
+ "#{content.method_name.class_name}##{content.method_name.method_name}"
85
+ when Project::HoverContent::SingletonMethodName
86
+ "#{content.method_name.class_name}.#{content.method_name.method_name}"
87
+ else
88
+ nil
89
+ end
90
+
91
+ if method_name
92
+ string = <<HOVER
93
+ ```
94
+ #{method_name} ~> #{content.type}
95
+ ```
96
+ HOVER
97
+ if content.definition
98
+ if content.definition.comment
99
+ string << "\n----\n\n#{content.definition.comment.string}"
100
+ end
101
+
102
+ string << "\n----\n\n#{content.definition.method_types.map {|x| "- `#{x}`\n" }.join()}"
103
+ end
104
+ else
105
+ "`#{content.type}`"
106
+ end
107
+ when Project::HoverContent::DefinitionContent
108
+ string = <<HOVER
109
+ ```
110
+ def #{content.method_name}: #{content.method_type}
111
+ ```
112
+ HOVER
113
+ if (comment = content.definition.comment)
114
+ string << "\n----\n\n#{comment.string}\n"
115
+ end
116
+
117
+ if content.definition.method_types.size > 1
118
+ string << "\n----\n\n#{content.definition.method_types.map {|x| "- `#{x}`\n" }.join()}"
119
+ end
120
+
121
+ string
122
+ when Project::HoverContent::TypeContent
123
+ "`#{content.type}`"
124
+ end
125
+ end
126
+
127
+ def response_to_completion(path:, line:, column:, trigger:)
128
+ Steep.logger.tagged("#response_to_completion") do
129
+ Steep.logger.info "path: #{path}, line: #{line}, column: #{column}, trigger: #{trigger}"
130
+
131
+ target = project.targets.find {|target| target.source_file?(path) } or return
132
+ target.type_check(target_sources: [], validate_signatures: false)
133
+
134
+ case (status = target&.status)
135
+ when Project::Target::TypeCheckStatus
136
+ subtyping = status.subtyping
137
+ source = target.source_files[path]
138
+
139
+ provider = Project::CompletionProvider.new(source_text: source.content, path: path, subtyping: subtyping)
140
+ items = begin
141
+ provider.run(line: line, column: column)
142
+ rescue Parser::SyntaxError
143
+ []
144
+ end
145
+
146
+ completion_items = items.map do |item|
147
+ format_completion_item(item)
148
+ end
149
+
150
+ Steep.logger.debug "items = #{completion_items.inspect}"
151
+
152
+ LSP::Interface::CompletionList.new(
153
+ is_incomplete: false,
154
+ items: completion_items
155
+ )
156
+ end
157
+ end
158
+ end
159
+
160
+ def format_completion_item(item)
161
+ range = LanguageServer::Protocol::Interface::Range.new(
162
+ start: LanguageServer::Protocol::Interface::Position.new(
163
+ line: item.range.start.line-1,
164
+ character: item.range.start.column
165
+ ),
166
+ end: LanguageServer::Protocol::Interface::Position.new(
167
+ line: item.range.end.line-1,
168
+ character: item.range.end.column
169
+ )
170
+ )
171
+
172
+ case item
173
+ when Project::CompletionProvider::LocalVariableItem
174
+ LanguageServer::Protocol::Interface::CompletionItem.new(
175
+ label: item.identifier,
176
+ kind: LanguageServer::Protocol::Constant::CompletionItemKind::VARIABLE,
177
+ detail: "#{item.identifier}: #{item.type}",
178
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
179
+ range: range,
180
+ new_text: "#{item.identifier}"
181
+ )
182
+ )
183
+ when Project::CompletionProvider::MethodNameItem
184
+ label = "def #{item.identifier}: #{item.method_type}"
185
+ method_type_snippet = method_type_to_snippet(item.method_type)
186
+ LanguageServer::Protocol::Interface::CompletionItem.new(
187
+ label: label,
188
+ kind: LanguageServer::Protocol::Constant::CompletionItemKind::METHOD,
189
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
190
+ new_text: "#{item.identifier}#{method_type_snippet}",
191
+ range: range
192
+ ),
193
+ documentation: item.definition.comment&.string,
194
+ insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET
195
+ )
196
+ when Project::CompletionProvider::InstanceVariableItem
197
+ label = "#{item.identifier}: #{item.type}"
198
+ LanguageServer::Protocol::Interface::CompletionItem.new(
199
+ label: label,
200
+ kind: LanguageServer::Protocol::Constant::CompletionItemKind::FIELD,
201
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
202
+ range: range,
203
+ new_text: item.identifier,
204
+ ),
205
+ insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET
206
+ )
207
+ end
208
+ end
209
+
210
+ def method_type_to_snippet(method_type)
211
+ params = if method_type.type.each_param.count == 0
212
+ ""
213
+ else
214
+ "(#{params_to_snippet(method_type.type)})"
215
+ end
216
+
217
+
218
+ block = if method_type.block
219
+ open, space, close = if method_type.block.type.return_type.is_a?(RBS::Types::Bases::Void)
220
+ ["do", " ", "end"]
221
+ else
222
+ ["{", "", "}"]
223
+ end
224
+
225
+ if method_type.block.type.each_param.count == 0
226
+ " #{open} $0 #{close}"
227
+ else
228
+ " #{open}#{space}|#{params_to_snippet(method_type.block.type)}| $0 #{close}"
229
+ end
230
+ else
231
+ ""
232
+ end
233
+
234
+ "#{params}#{block}"
235
+ end
236
+
237
+ def params_to_snippet(fun)
238
+ params = []
239
+
240
+ index = 1
241
+
242
+ fun.required_positionals.each do |param|
243
+ if name = param.name
244
+ params << "${#{index}:#{param.type}}"
245
+ else
246
+ params << "${#{index}:#{param.type}}"
247
+ end
248
+
249
+ index += 1
250
+ end
251
+
252
+ if fun.rest_positionals
253
+ params << "${#{index}:*#{fun.rest_positionals.type}}"
254
+ index += 1
255
+ end
256
+
257
+ fun.trailing_positionals.each do |param|
258
+ if name = param.name
259
+ params << "${#{index}:#{param.type}}"
260
+ else
261
+ params << "${#{index}:#{param.type}}"
262
+ end
263
+
264
+ index += 1
265
+ end
266
+
267
+ fun.required_keywords.each do |keyword, param|
268
+ if name = param.name
269
+ params << "#{keyword}: ${#{index}:#{name}_}"
270
+ else
271
+ params << "#{keyword}: ${#{index}:#{param.type}_}"
272
+ end
273
+
274
+ index += 1
275
+ end
276
+
277
+ params.join(", ")
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,196 @@
1
+ module Steep
2
+ module Server
3
+ class Master
4
+ LSP = LanguageServer::Protocol
5
+
6
+ attr_reader :steepfile
7
+ attr_reader :project
8
+ attr_reader :reader, :writer
9
+ attr_reader :queue
10
+ attr_reader :worker_count
11
+ attr_reader :worker_to_paths
12
+
13
+ attr_reader :interaction_worker
14
+ attr_reader :signature_worker
15
+ attr_reader :code_workers
16
+
17
+ def initialize(project:, reader:, writer:, interaction_worker:, signature_worker:, code_workers:, queue: Queue.new)
18
+ @project = project
19
+ @reader = reader
20
+ @writer = writer
21
+ @queue = queue
22
+ @interaction_worker = interaction_worker
23
+ @signature_worker = signature_worker
24
+ @code_workers = code_workers
25
+ @worker_to_paths = {}
26
+ end
27
+
28
+ def start
29
+ source_paths = project.targets.flat_map {|target| target.source_files.keys }
30
+ bin_size = (source_paths.size / code_workers.size) + 1
31
+ source_paths.each_slice(bin_size).with_index do |paths, index|
32
+ register_code_to_worker(paths, worker: code_workers[index])
33
+ end
34
+
35
+ Thread.new do
36
+ interaction_worker.reader.read do |message|
37
+ process_message_from_worker(message)
38
+ end
39
+ end
40
+
41
+ Thread.new do
42
+ signature_worker.reader.read do |message|
43
+ process_message_from_worker(message)
44
+ end
45
+ end
46
+
47
+ code_workers.each do |worker|
48
+ Thread.new do
49
+ worker.reader.read do |message|
50
+ process_message_from_worker(message)
51
+ end
52
+ end
53
+ end
54
+
55
+ Thread.new do
56
+ reader.read do |request|
57
+ process_message_from_client(request)
58
+ end
59
+ end
60
+
61
+ while job = queue.pop
62
+ writer.write(job)
63
+ end
64
+
65
+ writer.io.close
66
+
67
+ each_worker do |w|
68
+ w.shutdown()
69
+ end
70
+ end
71
+
72
+ def each_worker(&block)
73
+ if block_given?
74
+ yield interaction_worker
75
+ yield signature_worker
76
+ code_workers.each &block
77
+ else
78
+ enum_for :each_worker
79
+ end
80
+ end
81
+
82
+ def process_message_from_client(message)
83
+ id = message[:id]
84
+
85
+ case message[:method]
86
+ when "initialize"
87
+ queue << {
88
+ id: id,
89
+ result: LSP::Interface::InitializeResult.new(
90
+ capabilities: LSP::Interface::ServerCapabilities.new(
91
+ text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
92
+ change: LSP::Constant::TextDocumentSyncKind::FULL
93
+ ),
94
+ hover_provider: true,
95
+ completion_provider: LSP::Interface::CompletionOptions.new(
96
+ trigger_characters: [".", "@"]
97
+ )
98
+ )
99
+ )
100
+ }
101
+
102
+ each_worker do |worker|
103
+ worker << message
104
+ end
105
+
106
+ when "textDocument/didChange"
107
+ uri = URI.parse(message[:params][:textDocument][:uri])
108
+ path = project.relative_path(Pathname(uri.path))
109
+ text = message[:params][:contentChanges][0][:text]
110
+
111
+ project.targets.each do |target|
112
+ case
113
+ when target.source_file?(path)
114
+ if text.empty? && !path.file?
115
+ Steep.logger.info { "Deleting source file: #{path}..." }
116
+ target.remove_source(path)
117
+ else
118
+ Steep.logger.info { "Updating source file: #{path}..." }
119
+ target.update_source(path, text)
120
+ end
121
+ when target.possible_source_file?(path)
122
+ Steep.logger.info { "Adding source file: #{path}..." }
123
+ target.add_source(path, text)
124
+ when target.signature_file?(path)
125
+ if text.empty? && !path.file?
126
+ Steep.logger.info { "Deleting signature file: #{path}..." }
127
+ target.remove_signature(path)
128
+ else
129
+ Steep.logger.info { "Updating signature file: #{path}..." }
130
+ target.update_signature(path, text)
131
+ end
132
+ when target.possible_signature_file?(path)
133
+ Steep.logger.info { "Adding signature file: #{path}..." }
134
+ target.add_signature(path, text)
135
+ end
136
+ end
137
+
138
+ unless registered_path?(path)
139
+ register_code_to_worker [path], worker: least_busy_worker()
140
+ end
141
+
142
+ each_worker do |worker|
143
+ worker << message
144
+ end
145
+
146
+ when "textDocument/hover"
147
+ interaction_worker << message
148
+
149
+ when "textDocument/completion"
150
+ interaction_worker << message
151
+
152
+ when "textDocument/open"
153
+ # Ignores open notification
154
+
155
+ when "shutdown"
156
+ queue << { id: id, result: nil }
157
+
158
+ when "exit"
159
+ queue << nil
160
+ end
161
+ end
162
+
163
+ def process_message_from_worker(message)
164
+ queue << message
165
+ end
166
+
167
+ def paths_for(worker)
168
+ worker_to_paths[worker] ||= Set[]
169
+ end
170
+
171
+ def least_busy_worker
172
+ code_workers.min_by do |w|
173
+ paths_for(w).size
174
+ end
175
+ end
176
+
177
+ def registered_path?(path)
178
+ worker_to_paths.each_value.any? {|set| set.include?(path) }
179
+ end
180
+
181
+ def register_code_to_worker(paths, worker:)
182
+ paths_for(worker).merge(paths)
183
+
184
+ worker << {
185
+ method: "workspace/executeCommand",
186
+ params: LSP::Interface::ExecuteCommandParams.new(
187
+ command: "steep/registerSourceToWorker",
188
+ arguments: paths.map do |path|
189
+ "file://#{project.absolute_path(path)}"
190
+ end
191
+ )
192
+ }
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,148 @@
1
+ module Steep
2
+ module Server
3
+ class SignatureWorker < BaseWorker
4
+ attr_reader :queue
5
+ attr_reader :last_target_validated_at
6
+
7
+ def initialize(project:, reader:, writer:, queue: Queue.new)
8
+ super(project: project, reader: reader, writer: writer)
9
+
10
+ @queue = queue
11
+ @last_target_validated_at = {}
12
+ end
13
+
14
+ def validate_signature_if_required(request)
15
+ path = source_path(URI.parse(request[:params][:textDocument][:uri]))
16
+
17
+ project.targets.each do |target|
18
+ if target.signature_file?(path)
19
+ enqueue_target target: target, timestamp: Time.now
20
+ end
21
+ end
22
+ end
23
+
24
+ def enqueue_target(target:, timestamp:)
25
+ Steep.logger.debug "queueing target #{target.name}@#{timestamp}"
26
+ last_target_validated_at[target] = timestamp
27
+ queue << [target, timestamp]
28
+ end
29
+
30
+ def handle_request(request)
31
+ case request[:method]
32
+ when "initialize"
33
+ # Don't respond to initialize request, but start type checking.
34
+ project.targets.each do |target|
35
+ enqueue_target(target: target, timestamp: Time.now)
36
+ end
37
+ when "textDocument/didChange"
38
+ update_source(request)
39
+ validate_signature_if_required(request)
40
+ end
41
+ end
42
+
43
+ def validate_signature(target, timestamp:)
44
+ Steep.logger.info "Starting signature validation: #{target.name} (#{timestamp})..."
45
+
46
+ target.type_check(target_sources: [], validate_signatures: true)
47
+
48
+ Steep.logger.info "Finished signature validation: #{target.name} (#{timestamp})"
49
+
50
+ diagnostics = case status = target.status
51
+ when Project::Target::SignatureSyntaxErrorStatus
52
+ target.signature_files.each.with_object({}) do |(path, file), hash|
53
+ if file.status.is_a?(Project::SignatureFile::ParseErrorStatus)
54
+ location = case error = file.status.error
55
+ when RBS::Parser::SyntaxError
56
+ if error.error_value.is_a?(String)
57
+ buf = RBS::Buffer.new(name: path, content: file.content)
58
+ RBS::Location.new(buffer: buf, start_pos: buf.content.size, end_pos: buf.content.size)
59
+ else
60
+ error.error_value.location
61
+ end
62
+ when RBS::Parser::SemanticsError
63
+ error.location
64
+ else
65
+ raise
66
+ end
67
+
68
+ hash[path] =
69
+ [
70
+ LSP::Interface::Diagnostic.new(
71
+ message: file.status.error.message,
72
+ severity: LSP::Constant::DiagnosticSeverity::ERROR,
73
+ range: LSP::Interface::Range.new(
74
+ start: LSP::Interface::Position.new(
75
+ line: location.start_line,
76
+ character: location.start_column,
77
+ ),
78
+ end: LSP::Interface::Position.new(
79
+ line: location.end_line,
80
+ character: location.end_column
81
+ )
82
+ )
83
+ )
84
+ ]
85
+ else
86
+ hash[path] = []
87
+ end
88
+ end
89
+ when Project::Target::SignatureValidationErrorStatus
90
+ error_hash = status.errors.group_by {|error| error.location.buffer.name }
91
+
92
+ target.signature_files.each_key.with_object({}) do |path, hash|
93
+ errors = error_hash[path] || []
94
+ hash[path] = errors.map do |error|
95
+ LSP::Interface::Diagnostic.new(
96
+ message: StringIO.new.tap {|io| error.puts(io) }.string.split(/\t/, 2).last,
97
+ severity: LSP::Constant::DiagnosticSeverity::ERROR,
98
+ range: LSP::Interface::Range.new(
99
+ start: LSP::Interface::Position.new(
100
+ line: error.location.start_line,
101
+ character: error.location.start_column,
102
+ ),
103
+ end: LSP::Interface::Position.new(
104
+ line: error.location.end_line,
105
+ character: error.location.end_column
106
+ )
107
+ )
108
+ )
109
+ end
110
+ end
111
+ when Project::Target::TypeCheckStatus
112
+ target.signature_files.each_key.with_object({}) do |path, hash|
113
+ hash[path] = []
114
+ end
115
+ else
116
+ Steep.logger.info "Unexpected target status: #{status.class}"
117
+ end
118
+
119
+ diagnostics.each do |path, diags|
120
+ writer.write(
121
+ method: :"textDocument/publishDiagnostics",
122
+ params: LSP::Interface::PublishDiagnosticsParams.new(
123
+ uri: URI.parse(project.absolute_path(path).to_s).tap {|uri| uri.scheme = "file"},
124
+ diagnostics: diags
125
+ )
126
+ )
127
+ end
128
+ end
129
+
130
+ def active_job?(target, timestamp)
131
+ if last_target_validated_at[target] == timestamp
132
+ sleep 0.1
133
+ last_target_validated_at[target] == timestamp
134
+ end
135
+ end
136
+
137
+ def handle_job(job)
138
+ target, timestamp = job
139
+
140
+ if active_job?(target, timestamp)
141
+ validate_signature(target, timestamp: timestamp)
142
+ else
143
+ Steep.logger.info "Skipping signature validation: #{target.name}, queued timestamp=#{timestamp}, latest timestamp=#{last_target_validated_at[target]}"
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,36 @@
1
+ module Steep
2
+ module Server
3
+ module Utils
4
+ LSP = LanguageServer::Protocol
5
+
6
+ def source_path(uri)
7
+ project.relative_path(Pathname(uri.path))
8
+ end
9
+
10
+ def update_source(request)
11
+ path = source_path(URI.parse(request[:params][:textDocument][:uri]))
12
+ text = request[:params][:contentChanges][0][:text]
13
+ version = request[:params][:textDocument][:version]
14
+
15
+ Steep.logger.debug "Updateing source: path=#{path}, version=#{version}, size=#{text.bytesize}"
16
+
17
+ project.targets.each do |target|
18
+ case
19
+ when target.source_file?(path)
20
+ target.update_source path, text
21
+ when target.possible_source_file?(path)
22
+ target.add_source path, text
23
+ when target.signature_file?(path)
24
+ target.update_signature path, text
25
+ when target.possible_signature_file?(path)
26
+ target.add_signature path, text
27
+ end
28
+ end
29
+
30
+ if block_given?
31
+ yield path, version
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end