Skip to content

Commit

Permalink
Merge pull request #521 from testdouble/enhance-lsp
Browse files Browse the repository at this point in the history
Enhance LSP
  • Loading branch information
searls authored Feb 9, 2023
2 parents 4651c48 + fd89000 commit 1a2f650
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 127 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ gem "minitest", "~> 5.0"
gem "pry"
gem "rake", "~> 13.0"
gem "gimme"
gem "m"


if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5")
gem "simplecov"
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ GEM
gimme (0.5.0)
json (2.6.3)
language_server-protocol (3.17.0.3)
m (1.6.1)
method_source (>= 0.6.7)
rake (>= 0.9.2.2)
method_source (1.0.0)
minitest (5.17.0)
parallel (1.22.1)
Expand Down Expand Up @@ -58,6 +61,7 @@ PLATFORMS
DEPENDENCIES
bundler
gimme
m
minitest (~> 5.0)
pry
rake (~> 13.0)
Expand Down
9 changes: 9 additions & 0 deletions lib/standard/lsp/logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Standard
module Lsp
class Logger
def puts(message)
warn("[server] #{message}")
end
end
end
end
155 changes: 155 additions & 0 deletions lib/standard/lsp/routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
module Standard
module Lsp
class Routes
def initialize(writer, logger, standardizer)
@writer = writer
@logger = logger
@standardizer = standardizer

@text_cache = {}
end

def self.handle(name, &block)
define_method("handle_#{name}", &block)
end

def for(name)
name = "handle_#{name}"
if respond_to?(name)
method(name)
end
end

handle "initialize" do |request|
@writer.write(id: request[:id], result: Proto::Interface::InitializeResult.new(
capabilities: Proto::Interface::ServerCapabilities.new(
document_formatting_provider: true,
diagnostic_provider: true,
text_document_sync: Proto::Interface::TextDocumentSyncOptions.new(
change: Proto::Constant::TextDocumentSyncKind::FULL
)
)
))
end

handle "initialized" do |request|
@logger.puts "Standard Ruby v#{Standard::VERSION} LSP server initialized, pid #{Process.pid}"
end

handle "shutdown" do |request|
@logger.puts "Client asked to shutdown Standard LSP server. Exiting..."
at_exit {
@writer.write(id: request[:id], result: nil)
}
exit 0
end

handle "textDocument/diagnostic" do |request|
doc = request[:params][:textDocument]
result = diagnostic(doc[:uri], doc[:text])
@writer.write(result)
end

handle "textDocument/didChange" do |request|
params = request[:params]
result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text])
@writer.write(result)
end

handle "textDocument/didOpen" do |request|
doc = request[:params][:textDocument]
result = diagnostic(doc[:uri], doc[:text])
@writer.write(result)
end

handle "textDocument/didClose" do |request|
@text_cache.delete(request.dig(:params, :textDocument, :uri))
end

handle "textDocument/formatting" do |request|
uri = request[:params][:textDocument][:uri]
@writer.write({id: request[:id], result: format_file(uri)})
end

handle "textDocument/didSave" do |_request|
# No-op
end

handle "$/cancelRequest" do |_request|
# No-op
end

handle "$/setTrace" do |_request|
# No-op
end

private

def format_file(file_uri)
text = @text_cache[file_uri]
if text.nil?
@logger.puts "Format request arrived before text synchonized; skipping: `#{file_uri}'"
[]
else
new_text = @standardizer.format(text)
if new_text == text
[]
else
[{
newText: new_text,
range: {
start: {line: 0, character: 0},
end: {line: text.count("\n") + 1, character: 0}
}
}]
end
end
end

def diagnostic(file_uri, text)
@text_cache[file_uri] = text
offenses = @standardizer.offenses(text)

lsp_diagnostics = offenses.map { |o|
code = o[:cop_name]

msg = o[:message].delete_prefix(code)
loc = o[:location]

severity = case o[:severity]
when "error", "fatal"
SEV::ERROR
when "warning"
SEV::WARNING
when "convention"
SEV::INFORMATION
when "refactor", "info"
SEV::HINT
else # the above cases fully cover what RuboCop sends at this time
logger.puts "Unknown severity: #{severity.inspect}"
SEV::HINT
end

{
code: code,
message: msg,
range: {
start: {character: loc[:start_column] - 1, line: loc[:start_line] - 1},
end: {character: loc[:last_column] - 1, line: loc[:last_line] - 1}
},
severity: severity,
source: "standard"
}
}

{
method: "textDocument/publishDiagnostics",
params: {
uri: file_uri,
diagnostics: lsp_diagnostics
}
}
end
end
end
end
143 changes: 21 additions & 122 deletions lib/standard/lsp/server.rb
Original file line number Diff line number Diff line change
@@ -1,144 +1,43 @@
require "language_server-protocol"
require_relative "standardizer"
require_relative "routes"
require_relative "logger"

module Standard
module LSP
class Server
Proto = LanguageServer::Protocol
SEV = Proto::Constant::DiagnosticSeverity
module Lsp
Proto = LanguageServer::Protocol
SEV = Proto::Constant::DiagnosticSeverity

class Server
def self.start(standardizer)
new(standardizer).start
end

attr_accessor :standardizer, :writer, :reader, :logger, :text_cache, :subscribers

def initialize(standardizer)
self.standardizer = standardizer
self.writer = Proto::Transport::Io::Writer.new($stdout)
self.reader = Proto::Transport::Io::Reader.new($stdin)
self.logger = $stderr
self.text_cache = {}

self.subscribers = {
"initialize" => ->(request) {
init_result = Proto::Interface::InitializeResult.new(
capabilities: Proto::Interface::ServerCapabilities.new(
document_formatting_provider: true,
diagnostic_provider: true,
text_document_sync: Proto::Constant::TextDocumentSyncKind::FULL
)
)
writer.write(id: request[:id], result: init_result)
},

"initialized" => ->(request) { logger.puts "standard v#{Standard::VERSION} initialized, pid #{Process.pid}" },

"shutdown" => ->(request) {
logger.puts "asked to shutdown, exiting..."
exit
},

"textDocument/didChange" => ->(request) {
params = request[:params]
result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text])
writer.write(result)
},

"textDocument/didOpen" => ->(request) {
td = request[:params][:textDocument]
result = diagnostic(td[:uri], td[:text])
writer.write(result)
},

"textDocument/didClose" => ->(request) {
text_cache.delete(request.dig(:params, :textDocument, :uri))
},

"textDocument/formatting" => ->(request) {
uri = request[:params][:textDocument][:uri]
writer.write({id: request[:id], result: format_file(uri)})
},

"textDocument/didSave" => ->(request) {}
}
@standardizer = standardizer
@writer = Proto::Transport::Io::Writer.new($stdout)
@reader = Proto::Transport::Io::Reader.new($stdin)
@logger = Logger.new
@routes = Routes.new(@writer, @logger, @standardizer)
end

def start
reader.read do |request|
@reader.read do |request|
method = request[:method]
if (subscriber = subscribers[method])
subscriber.call(request)
if (route = @routes.for(method))
route.call(request)
else
logger.puts "unknown method: #{method}"
@writer.write({id: request[:id], error: Proto::Interface::ResponseError.new(
code: Proto::Constant::ErrorCodes::METHOD_NOT_FOUND,
message: "Unsupported Method: #{method}"
)})
@logger.puts "Unsupported Method: #{method}"
end
rescue => e
logger.puts "error #{e.class} #{e.message[0..100]}"
logger.puts e.backtrace.inspect
@logger.puts "Error #{e.class} #{e.message[0..100]}"
@logger.puts e.backtrace.inspect
end
end

def format_file(file_uri)
text = text_cache[file_uri]
new_text = standardizer.format(text)

if new_text == text
[]
else
[{
newText: new_text,
range: {
start: {line: 0, character: 0},
end: {line: text.count("\n") + 1, character: 0}
}
}]
end
end

def diagnostic(file_uri, text)
text_cache[file_uri] = text
offenses = standardizer.offenses(text)

lsp_diagnostics = offenses.map { |o|
code = o[:cop_name]

msg = o[:message].delete_prefix(code)
loc = o[:location]

severity = case o[:severity]
when "error", "fatal"
SEV::ERROR
when "warning"
SEV::WARNING
when "convention"
SEV::INFORMATION
when "refactor", "info"
SEV::HINT
else # the above cases fully cover what RuboCop sends at this time
logger.puts "unknown severity: #{severity.inspect}"
SEV::HINT
end

{
code: code,
message: msg,
range: {
start: {character: loc[:start_column] - 1, line: loc[:start_line] - 1},
end: {character: loc[:last_column] - 1, line: loc[:last_line] - 1}
},
severity: severity,
source: "standard"
}
}

{
method: "textDocument/publishDiagnostics",
params: {
diagnostics: lsp_diagnostics,
uri: file_uri
}
}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/standard/lsp/standardizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
require "tempfile"

module Standard
module LSP
module Lsp
class Standardizer
def initialize(config)
@template_options = config
Expand Down
4 changes: 2 additions & 2 deletions lib/standard/runners/lsp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ module Standard
module Runners
class Lsp
def call(config)
standardizer = Standard::LSP::Standardizer.new(config)
Standard::LSP::Server.start(standardizer)
standardizer = Standard::Lsp::Standardizer.new(config)
Standard::Lsp::Server.start(standardizer)
end
end
end
Expand Down
Loading

0 comments on commit 1a2f650

Please sign in to comment.