steep 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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