Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
st0012 committed Aug 28, 2024
1 parent e815ef6 commit 4e6b904
Show file tree
Hide file tree
Showing 8 changed files with 678 additions and 583 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo"
gem "tzinfo-data"
end

gem "ruby-lsp", github: "Shopify/ruby-lsp", branch: "add-on-client-server-framework"
23 changes: 12 additions & 11 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ GIT
rdoc (6.6.3.1)
psych (>= 4.0.0)

GIT
remote: https://github.com/Shopify/ruby-lsp.git
revision: 6484963320dc6d225d2572adcb0c3aadc7598d40
branch: add-on-client-server-framework
specs:
ruby-lsp (0.17.17)
language_server-protocol (~> 3.17.0)
prism (>= 0.29.0, < 0.31)
rbs (>= 3, < 4)
sorbet-runtime (>= 0.5.10782)

PATH
remote: .
specs:
Expand Down Expand Up @@ -146,8 +157,6 @@ GEM
nio4r (2.7.3)
nokogiri (1.16.5-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.5-x64-mingw-ucrt)
racc (~> 1.4)
nokogiri (1.16.5-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.5-x86_64-linux)
Expand Down Expand Up @@ -234,11 +243,6 @@ GEM
rubocop (~> 1.51)
rubocop-sorbet (0.8.3)
rubocop (>= 0.90.0)
ruby-lsp (0.17.12)
language_server-protocol (~> 3.17.0)
prism (>= 0.29.0, < 0.31)
rbs (>= 3, < 4)
sorbet-runtime (>= 0.5.10782)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sorbet (0.5.11406)
Expand All @@ -255,7 +259,6 @@ GEM
sorbet-static-and-runtime (>= 0.5.10187)
thor (>= 0.19.2)
sqlite3 (1.7.3-arm64-darwin)
sqlite3 (1.7.3-x64-mingw-ucrt)
sqlite3 (1.7.3-x86_64-darwin)
sqlite3 (1.7.3-x86_64-linux)
stringio (3.1.0)
Expand All @@ -273,8 +276,6 @@ GEM
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0)
unicode-display_width (2.5.0)
webmock (3.23.1)
addressable (>= 2.8.0)
Expand All @@ -292,7 +293,6 @@ GEM

PLATFORMS
arm64-darwin
x64-mingw-ucrt
x86_64-darwin
x86_64-linux

Expand All @@ -307,6 +307,7 @@ DEPENDENCIES
rubocop-rake (~> 0.6.0)
rubocop-shopify (~> 2.15)
rubocop-sorbet (~> 0.8)
ruby-lsp!
ruby-lsp-rails!
sorbet-static-and-runtime
sqlite3 (< 2)
Expand Down
4 changes: 2 additions & 2 deletions lib/ruby_lsp/ruby_lsp_rails/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def initialize
sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
def activate(global_state, message_queue)
@global_state = T.let(global_state, T.nilable(RubyLsp::GlobalState))
$stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
$stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}") unless ENV["RAILS_ENV"] == "test"
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
Thread.new { @client = RunnerClient.create_client }
Thread.new { @client = RunnerClient.create_client(self) }
register_additional_file_watchers(global_state: global_state, message_queue: message_queue)

T.must(@global_state).index.register_enhancement(IndexingEnhancement.new)
Expand Down
175 changes: 38 additions & 137 deletions lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@

require "json"
require "open3"
require "ruby_lsp/addon/process_client"

module RubyLsp
module Rails
class RunnerClient
class RunnerClient < RubyLsp::Addon::ProcessClient
COMMAND = T.let(["bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start"].join(" "), String)

class << self
extend T::Sig

sig { returns(RunnerClient) }
def create_client
sig { params(addon: RubyLsp::Addon).returns(RunnerClient) }
def create_client(addon)
if File.exist?("bin/rails")
new
new(addon, COMMAND)
else
$stderr.puts(<<~MSG)
Ruby LSP Rails failed to locate bin/rails in the current directory: #{Dir.pwd}"
Expand All @@ -28,76 +31,44 @@ def create_client
end
end

class InitializationError < StandardError; end
class IncompleteMessageError < StandardError; end
class EmptyMessageError < StandardError; end

MAX_RETRIES = 5

extend T::Sig

sig { returns(String) }
attr_reader :rails_root

sig { void }
def initialize
@mutex = T.let(Mutex.new, Mutex)
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
# set its own session ID
begin
Process.setpgrp
Process.setsid
rescue Errno::EPERM
# If we can't set the session ID, continue
rescue NotImplementedError
# setpgrp() may be unimplemented on some platform
# https://github.com/Shopify/ruby-lsp-rails/issues/348
end

stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
Open3.popen3("bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start")
end

@stdin = T.let(stdin, IO)
@stdout = T.let(stdout, IO)
@stderr = T.let(stderr, IO)
@wait_thread = T.let(wait_thread, Process::Waiter)
@stdin.binmode # for Windows compatibility
@stdout.binmode # for Windows compatibility

$stderr.puts("Ruby LSP Rails booting server")
count = 0
def rails_root
T.must(@rails_root)
end

begin
count += 1
initialize_response = T.must(read_response)
@rails_root = T.let(initialize_response[:root], String)
rescue EmptyMessageError
$stderr.puts("Ruby LSP Rails is retrying initialize (#{count})")
retry if count < MAX_RETRIES
sig { params(message: String).void }
def log_output(message)
# We don't want to log output in tests
unless ENV["RAILS_ENV"] == "test"
super
end
end

$stderr.puts("Finished booting Ruby LSP Rails server")
sig { override.params(response: T::Hash[Symbol, T.untyped]).void }
def handle_initialize_response(response)
@rails_root = T.let(response[:root], T.nilable(String))
end

sig { override.void }
def register_exit_handler
unless ENV["RAILS_ENV"] == "test"
at_exit do
if @wait_thread.alive?
$stderr.puts("Ruby LSP Rails is force killing the server")
if wait_thread.alive?
log_output("force killing the server")
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
force_kill
end
end
end
rescue Errno::EPIPE, IncompleteMessageError
raise InitializationError, @stderr.read
end

sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
def model(name)
make_request("model", name: name)
rescue IncompleteMessageError
$stderr.puts("Ruby LSP Rails failed to get model information: #{@stderr.read}")
log_output("failed to get model information: #{stderr.read}")
nil
end

Expand All @@ -114,117 +85,47 @@ def association_target_location(model_name:, association_name:)
association_name: association_name,
)
rescue => e
$stderr.puts("Ruby LSP Rails failed with #{e.message}: #{@stderr.read}")
log_output("failed with #{e.message}: #{stderr.read}")
nil
end

sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
def route_location(name)
make_request("route_location", name: name)
rescue IncompleteMessageError
$stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
log_output("failed to get route location: #{stderr.read}")
nil
end

sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
def route(controller:, action:)
make_request("route_info", controller: controller, action: action)
rescue IncompleteMessageError
$stderr.puts("Ruby LSP Rails failed to get route information: #{@stderr.read}")
log_output("failed to get route information: #{stderr.read}")
nil
end

sig { void }
def trigger_reload
$stderr.puts("Reloading Rails application")
log_output("triggering reload")
send_notification("reload")
rescue IncompleteMessageError
$stderr.puts("Ruby LSP Rails failed to trigger reload")
nil
end

sig { void }
def shutdown
$stderr.puts("Ruby LSP Rails shutting down server")
send_message("shutdown")
sleep(0.5) # give the server a bit of time to shutdown
[@stdin, @stdout, @stderr].each(&:close)
rescue IOError
# The server connection may have died
force_kill
end

sig { returns(T::Boolean) }
def stopped?
[@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive?
end

private

sig do
params(
request: String,
params: T.nilable(T::Hash[Symbol, T.untyped]),
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
end
def make_request(request, params = nil)
send_message(request, params)
read_response
end

sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
def send_message(request, params = nil)
message = { method: request }
message[:params] = params if params
json = message.to_json

@mutex.synchronize do
@stdin.write("Content-Length: #{json.length}\r\n\r\n", json)
end
rescue Errno::EPIPE
# The server connection died
end

# Notifications are like messages, but one-way, with no response sent back.
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
def send_notification(request, params = nil) = send_message(request, params)

sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
def read_response
raw_response = @mutex.synchronize do
headers = @stdout.gets("\r\n\r\n")
raise IncompleteMessageError unless headers

content_length = headers[/Content-Length: (\d+)/i, 1].to_i
raise EmptyMessageError if content_length.zero?

@stdout.read(content_length)
end

response = JSON.parse(T.must(raw_response), symbolize_names: true)

if response[:error]
$stderr.puts("Ruby LSP Rails error: " + response[:error])
return
end

response.fetch(:result)
rescue Errno::EPIPE
# The server connection died
log_output("failed to trigger reload")
nil
end

sig { void }
def force_kill
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
end
end

class NullClient < RunnerClient
extend T::Sig

sig { void }
def initialize # rubocop:disable Lint/MissingSuper
def initialize
# no-op
end

sig { override.params(response: T::Hash[Symbol, T.untyped]).void }
def handle_initialize_response(response)
# no-op
end

sig { override.void }
Expand Down
Loading

0 comments on commit 4e6b904

Please sign in to comment.