lib/steep/server/code_worker.rb



module Steep
  module Server
    class CodeWorker < BaseWorker
      LSP = LanguageServer::Protocol

      include Utils

      attr_reader :typecheck_paths
      attr_reader :queue

      def initialize(project:, reader:, writer:, queue: Queue.new)
        super(project: project, reader: reader, writer: writer)

        @typecheck_paths = Set[]
        @queue = queue
      end

      def enqueue_type_check(target:, path:)
        Steep.logger.info "Enqueueing type check: #{target.name}::#{path}..."
        queue << [target, path]
      end

      def typecheck_file(path, target)
        Steep.logger.info "Starting type checking: #{target.name}::#{path}..."

        source = target.source_files[path]
        target.type_check(target_sources: [source], validate_signatures: false)

        if target.status.is_a?(Project::Target::TypeCheckStatus) && target.status.type_check_sources.empty?
          Steep.logger.debug "Skipped type checking: #{target.name}::#{path}"
        else
          Steep.logger.info "Finished type checking: #{target.name}::#{path}"
        end

        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 typecheck_paths.include?(path)
                enqueue_type_check(target: target, path: path)
              end
            end
          end

        when "workspace/executeCommand"
          if request[:params][:command] == "steep/registerSourceToWorker"
            paths = request[:params][:arguments].map {|arg| source_path(URI.parse(arg)) }
            typecheck_paths.merge(paths)
          end

        when "textDocument/didChange"
          update_source(request) do |path, _|
            source_target, signature_targets = project.targets_for_path(path)

            if source_target
              if typecheck_paths.include?(path)
                enqueue_type_check(target: source_target, path: path)
              end
            end

            signature_targets.each do |target|
              target.source_files.each_key do |source_path|
                if typecheck_paths.include?(source_path)
                  enqueue_type_check(target: target, path: source_path)
                end
              end
            end
          end
        end
      end

      def handle_job(job)
        target, path = job

        typecheck_file(path, target)
      end
    end
  end
end