lib/steep/server/code_worker.rb
module Steep
module Server
class CodeWorker < BaseWorker
LSP = LanguageServer::Protocol
include Utils
attr_reader :target_files
attr_reader :queue
def initialize(project:, reader:, writer:, queue: Queue.new)
super(project: project, reader: reader, writer: writer)
@target_files = {}
@queue = queue
end
def enqueue_type_check(target:, path:, version: target_files[path])
Steep.logger.info "Enqueueing type check: #{path}(#{version})@#{target.name}..."
target_files[path] = version
queue << [path, version, target]
end
def each_type_check_subject(path:, version:)
case
when !(updated_targets = project.targets.select {|target| target.signature_file?(path) }).empty?
updated_targets.each do |target|
target_files.each_key do |path|
if target.source_file?(path)
yield target, path, target_files[path]
end
end
end
when target = project.targets.find {|target| target.source_file?(path) }
if target_files.key?(path)
yield target, path, version
end
end
end
def typecheck_file(path, target)
Steep.logger.info "Starting type checking: #{path}@#{target.name}..."
source = target.source_files[path]
target.type_check(target_sources: [source], validate_signatures: false)
Steep.logger.info "Finished type checking: #{path}@#{target.name}"
diagnostics = source_diagnostics(source, target.options)
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: diagnostics
)
)
end
def source_diagnostics(source, options)
case status = source.status
when Project::SourceFile::ParseErrorStatus
[]
when Project::SourceFile::AnnotationSyntaxErrorStatus
[
LSP::Interface::Diagnostic.new(
message: "Annotation syntax error: #{status.error.cause.message}",
severity: LSP::Constant::DiagnosticSeverity::ERROR,
range: LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: status.location.start_line - 1,
character: status.location.start_column
),
end: LSP::Interface::Position.new(
line: status.location.end_line - 1,
character: status.location.end_column
)
)
)
]
when Project::SourceFile::TypeCheckStatus
status.typing.errors.select {|error| options.error_to_report?(error) }.map do |error|
loc = error.location_to_str
LSP::Interface::Diagnostic.new(
message: StringIO.new.tap {|io| error.print_to(io) }.string.gsub(/\A#{Regexp.escape(loc)}: /, "").chomp,
severity: LSP::Constant::DiagnosticSeverity::ERROR,
range: LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: error.node.loc.line - 1,
character: error.node.loc.column
),
end: LSP::Interface::Position.new(
line: error.node.loc.last_line - 1,
character: error.node.loc.last_column
)
)
)
end
when Project::SourceFile::TypeCheckErrorStatus
[]
end
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|
target.source_files.each_key do |path|
if target_files.key?(path)
enqueue_type_check(target: target, path: path, version: target_files[path])
end
end
end
when "workspace/executeCommand"
if request[:params][:command] == "steep/registerSourceToWorker"
paths = request[:params][:arguments].map {|arg| source_path(URI.parse(arg)) }
paths.each do |path|
target_files[path] = 0
end
end
when "textDocument/didChange"
update_source(request) do |path, version|
if target_files.key?(path)
target_files[path] = version
end
end
path = source_path(URI.parse(request[:params][:textDocument][:uri]))
version = request[:params][:textDocument][:version]
each_type_check_subject(path: path, version: version) do |target, path, version|
enqueue_type_check(target: target, path: path, version: version)
end
end
end
def handle_job(job)
path, version, target = job
if !version || target_files[path] == version
typecheck_file(path, target)
else
Steep.logger.info "Skipping type check: #{path}@#{target.name}, queued version=#{version}, latest version=#{target_files[path]}"
end
end
end
end
end