lib/steep/server/signature_worker.rb
module Steep
module Server
class SignatureWorker < BaseWorker
attr_reader :queue
attr_reader :last_target_validated_at
def initialize(project:, reader:, writer:, queue: Queue.new)
super(project: project, reader: reader, writer: writer)
@queue = queue
@last_target_validated_at = {}
end
def validate_signature_if_required(request)
path = source_path(URI.parse(request[:params][:textDocument][:uri]))
project.targets.each do |target|
if target.signature_file?(path)
enqueue_target target: target, timestamp: Time.now
end
end
end
def enqueue_target(target:, timestamp:)
Steep.logger.debug "queueing target #{target.name}@#{timestamp}"
last_target_validated_at[target] = timestamp
queue << [target, timestamp]
end
def handle_request(request)
case request[:method]
when "initialize"
# Don't respond to initialize request, but start type checking.
project.targets.each do |target|
enqueue_target(target: target, timestamp: Time.now)
end
when "textDocument/didChange"
update_source(request)
validate_signature_if_required(request)
end
end
def validate_signature(target, timestamp:)
Steep.logger.info "Starting signature validation: #{target.name} (#{timestamp})..."
target.type_check(target_sources: [], validate_signatures: true)
Steep.logger.info "Finished signature validation: #{target.name} (#{timestamp})"
diagnostics = case status = target.status
when Project::Target::SignatureSyntaxErrorStatus
target.signature_files.each.with_object({}) do |(path, file), hash|
if file.status.is_a?(Project::SignatureFile::ParseErrorStatus)
location = case error = file.status.error
when RBS::Parser::SyntaxError
if error.error_value.is_a?(String)
buf = RBS::Buffer.new(name: path, content: file.content)
RBS::Location.new(buffer: buf, start_pos: buf.content.size, end_pos: buf.content.size)
else
error.error_value.location
end
when RBS::Parser::SemanticsError
error.location
else
raise
end
hash[path] =
[
LSP::Interface::Diagnostic.new(
message: file.status.error.message,
severity: LSP::Constant::DiagnosticSeverity::ERROR,
range: LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: location.start_line - 1,
character: location.start_column,
),
end: LSP::Interface::Position.new(
line: location.end_line - 1,
character: location.end_column
)
)
)
]
else
hash[path] = []
end
end
when Project::Target::SignatureValidationErrorStatus
error_hash = status.errors.group_by {|error| error.location.buffer.name }
target.signature_files.each_key.with_object({}) do |path, hash|
errors = error_hash[path] || []
hash[path] = errors.map do |error|
LSP::Interface::Diagnostic.new(
message: StringIO.new.tap {|io| error.puts(io) }.string.split(/\t/, 2).last,
severity: LSP::Constant::DiagnosticSeverity::ERROR,
range: LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: error.location.start_line - 1,
character: error.location.start_column,
),
end: LSP::Interface::Position.new(
line: error.location.end_line - 1,
character: error.location.end_column
)
)
)
end
end
when Project::Target::TypeCheckStatus
target.signature_files.each_key.with_object({}) do |path, hash|
hash[path] = []
end
else
Steep.logger.info "Unexpected target status: #{status.class}"
end
diagnostics.each do |path, diags|
writer.write(
method: :"textDocument/publishDiagnostics",
params: LSP::Interface::PublishDiagnosticsParams.new(
uri: URI.parse(project.absolute_path(path).to_s).tap {|uri| uri.scheme = "file"},
diagnostics: diags
)
)
end
end
def active_job?(target, timestamp)
if last_target_validated_at[target] == timestamp
sleep 0.1
last_target_validated_at[target] == timestamp
end
end
def handle_job(job)
target, timestamp = job
if active_job?(target, timestamp)
validate_signature(target, timestamp: timestamp)
else
Steep.logger.info "Skipping signature validation: #{target.name}, queued timestamp=#{timestamp}, latest timestamp=#{last_target_validated_at[target]}"
end
end
end
end
end