class YARP::LanguageServer

YARP additionally ships with a language server conforming to the language server protocol. It can be invoked by running the yarp-lsp bin script (bin/yarp-lsp)

Constants

GITHUB_TEMPLATE

Attributes

input[R]
output[R]

Public Class Methods

new( input: $stdin, output: $stdout ) click to toggle source
# File yarp/language_server.rb, line 31
def initialize(
  input: $stdin,
  output: $stdout
)
  @input = input.binmode
  @output = output.binmode
end

Public Instance Methods

run() click to toggle source

rubocop:disable Layout/LineLength

# File yarp/language_server.rb, line 40
def run
  store =
    Hash.new do |hash, uri|
      filepath = CGI.unescape(URI.parse(uri).path)
      File.exist?(filepath) ? (hash[uri] = File.read(filepath)) : nil
    end

  while (headers = input.gets("\r\n\r\n"))
    source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
    request = JSON.parse(source, symbolize_names: true)

    # stree-ignore
    case request
    in { method: "initialize", id: }
      store.clear
      write(id: id, result: { capabilities: capabilities })
    in { method: "initialized" }
      # ignored
    in { method: "shutdown" } # tolerate missing ID to be a good citizen
      store.clear
      write(id: request[:id], result: {})
    in { method: "exit"}
      return
    in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
      store[uri] = text
    in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
      store[uri] = text
    in { method: "textDocument/didClose", params: { textDocument: { uri: } } }
      store.delete(uri)
    in { method: "textDocument/diagnostic", id:, params: { textDocument: { uri: } } }
      contents = store[uri]
      write(id: id, result: contents ? diagnostics(contents) : nil)
    in { method: "textDocument/codeAction", id:, params: { textDocument: { uri: }, context: { diagnostics: }}}
      contents = store[uri]
      write(id: id, result: contents ? code_actions(contents, diagnostics) : nil)
    in { method: %r{\$/.+} }
      # ignored
    end
  end
end

Private Instance Methods

capabilities() click to toggle source

rubocop:enable Layout/LineLength

# File yarp/language_server.rb, line 84
def capabilities
  {
    codeActionProvider: {
      codeActionKinds: [
        'quickfix',
      ],
    },
    diagnosticProvider: {
      interFileDependencies: false,
      workspaceDiagnostics: false,
    },
    textDocumentSync: {
      change: 1,
      openClose: true
    },
  }
end
code_actions(source, diagnostics) click to toggle source
# File yarp/language_server.rb, line 102
def code_actions(source, diagnostics)
  diagnostics.map do |diagnostic|
    message = diagnostic[:message]
    issue_content = URI.encode_www_form_component(GITHUB_TEMPLATE % {error: message})
    issue_link = "https://github.com/ruby/yarp/issues/new?&labels=Bug&body=#{issue_content}"

    {
      title: "Report incorrect error: `#{diagnostic[:message]}`",
      kind: "quickfix",
      diagnostics: [diagnostic],
      command: {
        title: "Report incorrect error",
        command: "vscode.open",
        arguments: [issue_link]
      }
    }
  end
end
diagnostics(source) click to toggle source
# File yarp/language_server.rb, line 121
def diagnostics(source)
  offsets = Hash.new do |hash, key|
    slice = source.byteslice(...key)
    lineno = slice.count("\n")

    char = slice.length
    newline = source.rindex("\n", [char - 1, 0].max) || -1
    hash[key] = { line: lineno, character: char - newline - 1 }
  end

  parse_output = YARP.parse(source)

  {
    kind: "full",
    items: [
      *parse_output.errors.map do |error|
        {
          range: {
            start: offsets[error.location.start_offset],
            end: offsets[error.location.end_offset],
          },
          message: error.message,
          severity: 1,
        }
      end,
      *parse_output.warnings.map do |warning|
        {
          range: {
            start: offsets[warning.location.start_offset],
            end: offsets[warning.location.end_offset],
          },
          message: warning.message,
          severity: 2,
        }
      end,
    ]
  }
end
write(value) click to toggle source
# File yarp/language_server.rb, line 160
def write(value)
  response = value.merge(jsonrpc: "2.0").to_json
  output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}")
  output.flush
end