Skip to content

Commit

Permalink
Showing 5 changed files with 279 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/steep.rb
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
require "rainbow"
require "listen"
require 'pry'
require 'language_server-protocol'

require "steep/ast/namespace"
require "steep/names"
@@ -85,6 +86,7 @@
require "steep/drivers/scaffold"
require "steep/drivers/print_interface"
require "steep/drivers/watch"
require "steep/drivers/langserver"

if ENV["NO_COLOR"]
Rainbow.enabled = false
27 changes: 26 additions & 1 deletion lib/steep/cli.rb
Original file line number Diff line number Diff line change
@@ -132,7 +132,7 @@ def initialize(stdout:, stdin:, stderr:, argv:)
end

def self.available_commands
[:check, :validate, :annotations, :scaffold, :interface, :version, :paths, :watch]
[:check, :validate, :annotations, :scaffold, :interface, :version, :paths, :watch, :langserver]
end

def process_global_options
@@ -270,6 +270,31 @@ def process_watch
end
end

def process_langserver
with_signature_options do |signature_options|
strict = false
fallback_any_is_error = false

OptionParser.new do |opts|
handle_dir_options opts, signature_options
opts.on("--strict") { strict = true }
opts.on("--fallback-any-is-error") { fallback_any_is_error = true }
end.parse!(argv)

source_dirs = argv.map { |path| Pathname(path) }
if source_dirs.empty?
source_dirs << Pathname(".")
end

Drivers::Langserver.new(source_dirs: source_dirs, signature_dirs: signature_options.paths).tap do |driver|
driver.options.fallback_any_is_error = fallback_any_is_error || strict
driver.options.allow_missing_definitions = false if strict
end.run

0
end
end

def process_version
stdout.puts Steep::VERSION
0
134 changes: 134 additions & 0 deletions lib/steep/drivers/langserver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
module Steep
module Drivers
class Langserver
attr_reader :source_dirs
attr_reader :signature_dirs
attr_reader :options
attr_reader :subscribers

include Utils::EachSignature

def initialize(source_dirs:, signature_dirs:)
@source_dirs = source_dirs
@signature_dirs = signature_dirs
@options = Project::Options.new
@subscribers = {}

subscribe :initialize do |request:, notifier:|
LanguageServer::Protocol::Interface::InitializeResult.new(
capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
open_close: true,
change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL,
),
),
)
end

subscribe :shutdown do |request:, notifier:|
Steep.logger.warn "Shutting down the server..."
exit
end

subscribe :"textDocument/didOpen" do |request:, notifier:|
uri = URI.parse(request[:params][:textDocument][:uri])
text = request[:params][:textDocument][:text]
synchronize_project(uri: uri, text: text, notifier: notifier)
end

subscribe :"textDocument/didChange" do |request:, notifier:|
uri = URI.parse(request[:params][:textDocument][:uri])
text = request[:params][:contentChanges][0][:text]
synchronize_project(uri: uri, text: text, notifier: notifier)
end
end

def subscribe(method, &callback)
@subscribers[method] = callback
end

def project
@project ||= Project.new.tap do |project|
source_dirs.each do |path|
each_file_in_path(".rb", path) do |file_path|
file = Project::SourceFile.new(path: file_path, options: options)
file.content = file_path.read
project.source_files[file_path] = file
end
end

signature_dirs.each do |path|
each_file_in_path(".rbi", path) do |file_path|
file = Project::SignatureFile.new(path: file_path)
file.content = file_path.read
project.signature_files[file_path] = file
end
end
end
end

def run
writer = LanguageServer::Protocol::Transport::Stdio::Writer.new
reader = LanguageServer::Protocol::Transport::Stdio::Reader.new
notifier = Proc.new { |method:, params: {}| writer.write(method: method, params: params) }

reader.read do |request|
id = request[:id]
method = request[:method].to_sym
Steep.logger.warn "Received event: #{method}"
subscriber = subscribers[method]
if subscriber
result = subscriber.call(request: request, notifier: notifier)
if id
writer.write(id: id, result: result)
end
else
Steep.logger.warn "Ignored event: #{method}"
end
end
end

def synchronize_project(uri:, text:, notifier:)
path = Pathname(uri.path).relative_path_from(Pathname.pwd)

case path.extname
when ".rb"
file = project.source_files[path] || Project::SourceFile.new(path: path, options: options)
file.content = text
project.source_files[path] = file
project.type_check

diags = (file.errors || []).map do |error|
LanguageServer::Protocol::Interface::Diagnostic.new(
message: error.to_s,
severity: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
range: LanguageServer::Protocol::Interface::Range.new(
start: LanguageServer::Protocol::Interface::Position.new(
line: error.node.loc.line - 1,
character: error.node.loc.column,
),
end: LanguageServer::Protocol::Interface::Position.new(
line: error.node.loc.last_line - 1,
character: error.node.loc.last_column,
),
)
)
end

notifier.call(
method: :"textDocument/publishDiagnostics",
params: LanguageServer::Protocol::Interface::PublishDiagnosticsParams.new(
uri: uri,
diagnostics: diags,
),
)
when ".rbi"
file = project.signature_files[path] || Project::SignatureFile.new(path: path)
file.content = text
project.signature_files[path] = file
project.type_check
end
end
end
end
end
1 change: 1 addition & 0 deletions steep.gemspec
Original file line number Diff line number Diff line change
@@ -35,4 +35,5 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency "rainbow", "~> 2.2.2", "< 4.0"
spec.add_runtime_dependency "listen", "~> 3.1"
spec.add_runtime_dependency "pry", "~> 0.12.2"
spec.add_runtime_dependency "language_server-protocol", "~> 3.14.0"
end
116 changes: 116 additions & 0 deletions test/langserver_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
require_relative "test_helper"

class LangserverTest < Minitest::Test
include ShellHelper

def dirs
@dirs ||= []
end

def langserver_command
"#{__dir__}/../exe/steep langserver #{current_dir}"
end

def jsonrpc(hash)
hash_str = hash.to_json
"Content-Length: #{hash_str.bytesize}\r\n" + "\r\n" + hash_str
end

def test_initialize
in_tmpdir do
Open3.popen3(langserver_command) do |stdin, stdout, stderr, wait_thr|
stdin.puts jsonrpc(
id: 0,
method: "initialize",
params: {},
jsonrpc: "2.0",
)
stdin.close
wait_thr.join

assert_equal jsonrpc(
id: 0,
result: {
capabilities: {
textDocumentSync: { openClose: true, change: 1 }
}
},
jsonrpc: "2.0",
), stdout.read
end
end
end

def test_did_open
in_tmpdir do
Open3.popen3(langserver_command) do |stdin, stdout, stderr, wait_thr|
stdin.puts jsonrpc(
method: "textDocument/didOpen",
params: {
textDocument: {
uri: "file:///root/workdir/example.rb",
languageId: "ruby",
version: 1,
text: "[\"foo\"].join(\",\").map {|str| puts str}",
}
}
)
stdin.close
wait_thr.join

assert_equal jsonrpc(
method: "textDocument/publishDiagnostics",
params: {
uri: "file:///root/workdir/example.rb",
diagnostics: [{
range: {
start: { line: 0, character: 0 },
end: { line: 0, character: 38 },
},
severity: 1,
message: "#{Pathname("/root/workdir/example.rb").relative_path_from(Pathname.pwd)}:1:0: NoMethodError: type=::String, method=map"
}]
},
jsonrpc: "2.0",
), stdout.read
end
end
end

def test_did_change
in_tmpdir do
Open3.popen3(langserver_command) do |stdin, stdout, stderr, wait_thr|
stdin.puts jsonrpc(
method: "textDocument/didChange",
params: {
textDocument: {
uri: "file:///root/workdir/example.rb",
version: 2,
},
contentChanges: [
{ text: "[\"foo\"].join(\",\").map {|str| puts str}" }
]
}
)
stdin.close
wait_thr.join

assert_equal jsonrpc(
method: "textDocument/publishDiagnostics",
params: {
uri: "file:///root/workdir/example.rb",
diagnostics: [{
range: {
start: { line: 0, character: 0 },
end: { line: 0, character: 38 },
},
severity: 1,
message: "#{Pathname("/root/workdir/example.rb").relative_path_from(Pathname.pwd)}:1:0: NoMethodError: type=::String, method=map"
}]
},
jsonrpc: "2.0",
), stdout.read
end
end
end
end

0 comments on commit 18544c7

Please sign in to comment.