Skip to content

Commit

Permalink
Merge branch 'main' into andyw8/cspell
Browse files Browse the repository at this point in the history
  • Loading branch information
andyw8 authored Sep 30, 2024
2 parents b7ff45e + c4f78a7 commit 8e92cae
Show file tree
Hide file tree
Showing 20 changed files with 834 additions and 5,468 deletions.
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
ruby-lsp (0.18.4)
ruby-lsp (0.18.5)
language_server-protocol (~> 3.17.0)
prism (~> 1.0)
rbs (>= 3, < 4)
Expand Down Expand Up @@ -48,7 +48,7 @@ GEM
rbi (0.2.0)
prism (~> 1.0)
sorbet-runtime (>= 0.5.9204)
rbs (3.5.3)
rbs (3.6.0)
logger
rdoc (6.6.3.1)
psych (>= 4.0.0)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.18.4
0.18.5
43 changes: 43 additions & 0 deletions jekyll/composed-bundle.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
layout: default
title: Composed Ruby LSP bundle
nav_order: 40
parent: Ruby LSP
---

# Composed Ruby LSP bundle

In language ecosystems other than Ruby, it is not super common to have to add editor tooling as part of your project
dependencies. Usually, a language server is an executable that gets downloaded and then run independently from your
projects.

In the Ruby ecosystem, there are a few blockers to fully adopting this approach:

1. Not writing the language server in Ruby would make it challenging to integrate seamlessly with existing tools used by
the community that are already written in Ruby, like test frameworks, linters, formatters and so on
2. Discovering project dependencies automatically allows the language server to detect which files on disk must be read
and indexed, so that we can extract declarations from gems without requiring any configuration from the user. This means
that we need to integrate directly with Bundler
3. Bundler only allows requiring gems that are set up as part of the `$LOAD_PATH`. If the Ruby LSP executable was running
independently from a global installation, then the Ruby process would only be able to require the Ruby LSP's own
dependencies, but it would not be able to require any gems used by the project being worked on. Not being able to require
the project's dependencies limits integrations that the language server can automatically make with linters, formatters,
test frameworks and so on

To overcome these limitations, while at the same time not requiring users to add `ruby-lsp` as a dependency of their projects,
the Ruby LSP uses a composed bundle strategy. The flow of execution is as follows:

1. The executable is run as `ruby-lsp` without `bundle exec` to indicate that the composed bundle must first be configured
2. The executable sets up a composed bundle under `your_project/.ruby-lsp`. The generated Gemfile includes the `ruby-lsp` gem
and everything in the project's Gemfile as well. It may also include `debug` and `ruby-lsp-rails`
3. After the composed bundle is fully set up, then the original `ruby-lsp` Ruby process is fully replaced by
`BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp`, thus launching the real language server with access to the project's
dependencies, but without requiring adding the gem to the project's own Gemfile

In addition to performing this setup, the composed bundle logic will also `bundle install` and attempt to auto-update
the `ruby-lsp` language server gem to ensure fast distribution of bug fixes and new features.

{: .note }
Setting up the composed bundle requires several integrations with Bundler and there are many edge cases to consider,
like how to handle configurations or installing private dependencies. If you encounter a problem with the composed
bundle setup, please let us know by [reporting an issue](https://github.com/Shopify/ruby-lsp/issues/new).
6 changes: 6 additions & 0 deletions jekyll/index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Want to discuss Ruby developer experience? Consider joining the public
- [Usage](#usage)
- [With VS Code](#with-vs-code)
- [With other editors](#with-other-editors)
- [Composed Ruby LSP bundle](#composed-ruby-lsp-bundle)
- [Add-ons](#add-ons)
- [General features](#general-features)
- [Hover](#hover)
Expand Down Expand Up @@ -80,6 +81,11 @@ gem install ruby-lsp
and the language server can be launched running `ruby-lsp` (without bundle exec in order to properly hook into your
project's dependencies).

### Composed Ruby LSP bundle

The Ruby LSP executable generates a composed bundle with the goal of not requiring users to add the `ruby-lsp` gem to
their Gemfiles, and at the same time being able to hook into project dependencies. [Learn more](composed-bundle).

## Add-ons

The Ruby LSP provides an add-on system that allows other gems to enhance the base functionality with more editor
Expand Down
8 changes: 4 additions & 4 deletions jekyll/rails-add-on.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ nav_order: 10

[Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails) is a Ruby LSP add-on that enhances the base Ruby LSP functionality
with Rails-specific features. It provides improved code navigation, document symbols for Rails-specific elements,
and runtime inspection capabilities.
and runtime introspection capabilities.

It [communicates with a running Rails instance](#runtime-inspection) to provide dynamic information about the application,
It [communicates with a running Rails instance](#runtime-introspection) to provide dynamic information about the application,
enabling more accurate and context-aware language server features.

## Table of Contents

- [Installation](#installation)
- [Runtime Inspection](#runtime-inspection)
- [Runtime Introspection](#runtime-introspection)
- [Features](#features)
- [**Document Symbol**](#document-symbol)
- [Active Record Callbacks, Validations, and Associations](#active-record-callbacks-validations-and-associations)
Expand All @@ -41,7 +41,7 @@ enabling more accurate and context-aware language server features.
Ruby LSP detects Rails projects and installs the [Rails add-on](https://github.com/Shopify/ruby-lsp-rails) for you.

## Runtime Inspection
## Runtime Introspection

LSP tooling is typically based on static analysis, but `ruby-lsp-rails` actually communicates with your Rails app for
some features.
Expand Down
12 changes: 12 additions & 0 deletions jekyll/troubleshooting.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ ruby-lsp
Is there any extra information given from booting the server manually? Or does it only fail when booting through the
extension?

## Indexing

When Ruby LSP starts, it attempts to index your code as well as your dependencies as described in [Configuring code indexing](index#configuring-code-indexing).

In rare cases, Ruby LSP will encounter an error which prevents indexing from completing, which will result in incomplete information in the editor.

Firstly, ensure that you are using the latest release of the `ruby-lsp` gem, as the problem may have been already fixed.

To diagnose the particular file(s) causing a problem, run `ruby-lsp-check`. Please log an issue so that we can address it. If the code is not open source then please provide a minimal reproduction.

In the meantime, you can [configure Ruby LSP to ignore a particular gem or file for indexing](index#configuring-code-indexing).

## After troubleshooting

If after troubleshooting the Ruby LSP is still not initializing properly, please report an issue
Expand Down
20 changes: 15 additions & 5 deletions lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,23 @@ def signatures(member)

sig { params(overload: RBS::AST::Members::MethodDefinition::Overload).returns(T::Array[Entry::Parameter]) }
def process_overload(overload)
function = T.cast(overload.method_type.type, RBS::Types::Function)
parameters = parse_arguments(function)
function = overload.method_type.type

block = overload.method_type.block
parameters << Entry::BlockParameter.anonymous if block&.required
if function.is_a?(RBS::Types::Function)
parameters = parse_arguments(function)

parameters
block = overload.method_type.block
parameters << Entry::BlockParameter.anonymous if block&.required
return parameters
end

# Untyped functions are a new RBS feature (since v3.6.0) to declare methods that accept any parameters. For our
# purposes, accepting any argument is equivalent to `...`
if defined?(RBS::Types::UntypedFunction) && function.is_a?(RBS::Types::UntypedFunction)
[Entry::ForwardingParameter.new]
else
[]
end
end

sig { params(function: RBS::Types::Function).returns(T::Array[Entry::Parameter]) }
Expand Down
8 changes: 8 additions & 0 deletions lib/ruby_indexer/test/rbs_indexer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,14 @@ def test_signature_alias
assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.")
end

def test_indexing_untyped_functions
entries = @index.resolve_method("call", "Method")

parameters = entries.first.signatures.first.parameters
assert_equal(1, parameters.length)
assert_instance_of(Entry::ForwardingParameter, parameters.first)
end

private

def parse_rbs_methods(rbs, method_name)
Expand Down
6 changes: 6 additions & 0 deletions lib/ruby_lsp/base_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ def process_message(message); end
sig { abstract.void }
def shutdown; end

sig { params(id: Integer, message: String, type: Integer).void }
def fail_request_and_notify(id, message, type: Constant::MessageType::INFO)
send_message(Error.new(id: id, code: Constant::ErrorCodes::REQUEST_FAILED, message: message))
send_message(Notification.window_show_message(message, type: type))
end

sig { returns(Thread) }
def new_worker
Thread.new do
Expand Down
51 changes: 28 additions & 23 deletions lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,9 @@ def run_initialized
rescue RuboCop::Error => e
# The user may have provided unknown config switches in .rubocop or
# is trying to load a non-existent config file.
send_message(Notification.window_show_error(
send_message(Notification.window_show_message(
"RuboCop configuration error: #{e.message}. Formatting will not be available.",
type: Constant::MessageType::ERROR,
))
end
end
Expand Down Expand Up @@ -531,10 +532,16 @@ def text_document_formatting(message)
response = Requests::Formatting.new(@global_state, document).perform
send_message(Result.new(id: message[:id], response: response))
rescue Requests::Request::InvalidFormatter => error
send_message(Notification.window_show_error("Configuration error: #{error.message}"))
send_message(Notification.window_show_message(
"Configuration error: #{error.message}",
type: Constant::MessageType::ERROR,
))
send_empty_response(message[:id])
rescue StandardError, LoadError => error
send_message(Notification.window_show_error("Formatting error: #{error.message}"))
send_message(Notification.window_show_message(
"Formatting error: #{error.message}",
type: Constant::MessageType::ERROR,
))
send_empty_response(message[:id])
end

Expand Down Expand Up @@ -673,30 +680,19 @@ def code_action_resolve(message)
document = @store.get(uri)

unless document.is_a?(RubyDocument)
send_message(Notification.window_show_error("Code actions are currently only available for Ruby documents"))
raise Requests::CodeActionResolve::CodeActionError
fail_request_and_notify(message[:id], "Code actions are currently only available for Ruby documents")
return
end

result = Requests::CodeActionResolve.new(document, params).perform

case result
when Requests::CodeActionResolve::Error::EmptySelection
send_message(Notification.window_show_error("Invalid selection for Extract Variable refactor"))
raise Requests::CodeActionResolve::CodeActionError
fail_request_and_notify(message[:id], "Invalid selection for extract variable refactor")
when Requests::CodeActionResolve::Error::InvalidTargetRange
send_message(
Notification.window_show_error(
"Couldn't find an appropriate location to place extracted refactor",
),
)
raise Requests::CodeActionResolve::CodeActionError
fail_request_and_notify(message[:id], "Couldn't find an appropriate location to place extracted refactor")
when Requests::CodeActionResolve::Error::UnknownCodeAction
send_message(
Notification.window_show_error(
"Unknown code action",
),
)
raise Requests::CodeActionResolve::CodeActionError
fail_request_and_notify(message[:id], "Unknown code action")
else
send_message(Result.new(id: message[:id], response: result))
end
Expand Down Expand Up @@ -729,10 +725,16 @@ def text_document_diagnostic(message)
),
)
rescue Requests::Request::InvalidFormatter => error
send_message(Notification.window_show_error("Configuration error: #{error.message}"))
send_message(Notification.window_show_message(
"Configuration error: #{error.message}",
type: Constant::MessageType::ERROR,
))
send_empty_response(message[:id])
rescue StandardError, LoadError => error
send_message(Notification.window_show_error("Error running diagnostics: #{error.message}"))
send_message(Notification.window_show_message(
"Error running diagnostics: #{error.message}",
type: Constant::MessageType::ERROR,
))
send_empty_response(message[:id])
end

Expand Down Expand Up @@ -969,7 +971,9 @@ def perform_initial_indexing
false
end
rescue StandardError => error
send_message(Notification.window_show_error("Error while indexing: #{error.message}"))
message = "Error while indexing (see [troubleshooting steps]" \
"(https://shopify.github.io/ruby-lsp/troubleshooting#indexing)): #{error.message}"
send_message(Notification.window_show_message(message, type: Constant::MessageType::ERROR))
end

# Indexing produces a high number of short lived object allocations. That might lead to some fragmentation and
Expand Down Expand Up @@ -1054,8 +1058,9 @@ def check_formatter_is_available
@global_state.formatter = "none"

send_message(
Notification.window_show_error(
Notification.window_show_message(
"Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the Gemfile or gemspec.",
type: Constant::MessageType::ERROR,
),
)
end
Expand Down
9 changes: 3 additions & 6 deletions lib/ruby_lsp/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,11 @@ class Notification < Message
class << self
extend T::Sig

sig { params(message: String).returns(Notification) }
def window_show_error(message)
sig { params(message: String, type: Integer).returns(Notification) }
def window_show_message(message, type: Constant::MessageType::INFO)
new(
method: "window/showMessage",
params: Interface::ShowMessageParams.new(
type: Constant::MessageType::ERROR,
message: message,
),
params: Interface::ShowMessageParams.new(type: type, message: message),
)
end

Expand Down
Loading

0 comments on commit 8e92cae

Please sign in to comment.