Skip to content

Commit

Permalink
Rename extensions to addons
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Sep 22, 2023
1 parent 0e4ef38 commit b7cf80b
Show file tree
Hide file tree
Showing 21 changed files with 190 additions and 188 deletions.
69 changes: 35 additions & 34 deletions SERVER_EXTENSIONS.md → ADDONS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Server extensions
# Ruby LSP addons

> **WARNING**
> The Ruby LSP server extensions system is currently experimental and subject to changes in the API
> The Ruby LSP addon system is currently experimental and subject to changes in the API
Need help writing extensions? Consider joining the #ruby-lsp-extensions channel in the [Ruby DX Slack
Need help writing addons? Consider joining the #ruby-lsp-addons channel in the [Ruby DX Slack
workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-1zjp7lmgk-zL7bGvze8gj5hFaYS~r5vg).

## Motivation and goals
Expand All @@ -17,49 +17,49 @@ ecosystem. It would also create a bottleneck for authors to push new features. B
hand, increases fragmentation which tends to increase the effort required by users to configure their development
environments.

For these reasons, the Ruby LSP ships with a server extension system that authors can use to enhance the behavior of the
base LSP with tool specific functionality, aimed at
For these reasons, the Ruby LSP ships with an addon system that authors can use to enhance the behavior of the base LSP
with tool specific functionality, aimed at

- Allowing gem authors to export Ruby LSP extensions from their own gems
- Allowing LSP features to be enhanced by extensions present in the application the developer is currently working on
- Allowing gem authors to export Ruby LSP addons from their own gems
- Allowing LSP features to be enhanced by addons present in the application the developer is currently working on
- Not requiring extra configuration from the user
- Seamlessly integrating with the base features of the Ruby LSP

## Guidelines

When building a Ruby LSP extension, refer to these guidelines to ensure a good developer experience.
When building a Ruby LSP addon, refer to these guidelines to ensure a good developer experience.

- Performance over features. A single slow request may result in lack of responsiveness in the editor
- There are two types of LSP requests: automatic (e.g.: semantic highlighting) and user initiated (go to definition).
The performance of automatic requests is critical for responsiveness as they are executed every time the user types
- Avoid duplicate work where possible. If something can be computed once and memoized, like configurations, do it
- Do not mutate LSP state directly. Extensions sometimes have access to important state such as document objects, which
- Do not mutate LSP state directly. Addons sometimes have access to important state such as document objects, which
should never be mutated directly, but instead through the mechanisms provided by the LSP specification - like text edits
- Do not overnotify users. It's generally annoying and diverts attention from the current task

## Building a Ruby LSP extension
## Building a Ruby LSP addon

**Note**: the Ruby LSP uses [Sorbet](https://sorbet.org/). We recommend using Sorbet in extensions as well, which allows
**Note**: the Ruby LSP uses [Sorbet](https://sorbet.org/). We recommend using Sorbet in addons as well, which allows
authors to benefit from types declared by the Ruby LSP.

As an example, check out [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails), which is a Ruby LSP extension to
As an example, check out [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails), which is a Ruby LSP addon to
provide Rails related features.

### Activating the extension
### Activating the addon

The Ruby LSP discovers extensions based on the existence of an `extension.rb` file placed inside a `ruby_lsp` folder.
For example, `my_gem/lib/ruby_lsp/my_gem/extension.rb`. This file must declare the extension class, which can be used to
perform any necessary activation when the server starts.
The Ruby LSP discovers addons based on the existence of an `addon.rb` file placed inside a `ruby_lsp` folder. For
example, `my_gem/lib/ruby_lsp/my_gem/addon.rb`. This file must declare the addon class, which can be used to perform any
necessary activation when the server starts.


```ruby
# frozen_string_literal: true

require "ruby_lsp/extension"
require "ruby_lsp/addon"

module RubyLsp
module MyGem
class Extension < ::RubyLsp::Extension
class Addon < ::RubyLsp::Addon
extend T::Sig

# Performs any activation that needs to happen once when the language server is booted
Expand All @@ -72,7 +72,7 @@ module RubyLsp
def deactivate
end

# Returns the name of the extension
# Returns the name of the addon
sig { override.returns(String) }
def name
"Ruby LSP My Gem"
Expand All @@ -84,9 +84,9 @@ end

### Enhancing features

All Ruby LSP requests are listeners that handle specific node types. To enhance a request, the extension must create a
All Ruby LSP requests are listeners that handle specific node types. To enhance a request, the addon must create a
listener that will collect extra results that will be automatically appended to the base language server response.
Additionally, `Extension` has to implement a factory method that instantiates the listener.
Additionally, `Addon` has to implement a factory method that instantiates the listener.

For example: to add a message on hover saying "Hello!" on top of the base hover behavior of the Ruby LSP, we can use the
following listener implementation.
Expand All @@ -96,7 +96,7 @@ following listener implementation.

module RubyLsp
module MyGem
class Extension < ::RubyLsp::Extension
class Addon < ::RubyLsp::Addon
extend T::Sig

sig { override.void }
Expand All @@ -123,7 +123,7 @@ module RubyLsp
end
def create_hover_listener(nesting, index emitter, message_queue)
# Use the listener factory methods to instantiate listeners with parameters sent by the LSP combined with any
# pre-computed information in the extension. These factory methods are invoked on every request
# pre-computed information in the addon. These factory methods are invoked on every request
Hover.new(@config, emitter, message_queue)
end
end
Expand All @@ -150,7 +150,8 @@ module RubyLsp
@_response = T.let(nil, ResponseType)
@config = config

# Register that this listener will handle `on_constant_read` events (i.e.: whenever a constant read is found in the code)
# Register that this listener will handle `on_constant_read` events (i.e.: whenever a constant read is found in
# the code)
emitter.register(self, :on_constant_read)
end

Expand All @@ -170,12 +171,12 @@ end

### Registering formatters

Gems may also provide a formatter to be used by the Ruby LSP. To do that, the extension must create a formatter runner
and register it. The formatter is used if the `rubyLsp.formatter` option configured by the user matches the identifier
Gems may also provide a formatter to be used by the Ruby LSP. To do that, the addon must create a formatter runner and
register it. The formatter is used if the `rubyLsp.formatter` option configured by the user matches the identifier
registered.

```ruby
class MyFormatterRubyLspExtension < RubyLsp::Extension
class MyFormatterRubyLspAddon < RubyLsp::Addon
def name
"My Formatter"
end
Expand All @@ -192,7 +193,7 @@ end
class MyFormatterRunner
# Make it a singleton class
include Singleton
# If using Sorbet to develop the extension, then include this interface to make sure the class is properly implemented
# If using Sorbet to develop the addon, then include this interface to make sure the class is properly implemented
include RubyLsp::Requests::Support::FormatterRunner

# Use the initialize method to perform any sort of ahead of time work. For example, reading configurations for your
Expand Down Expand Up @@ -241,7 +242,7 @@ ensure documentation is always up to date and consistent.
```ruby
require "ruby_lsp/check_docs"

# The first argument is the file list including all of the listeners declared by the extension
# The first argument is the file list including all of the listeners declared by the addon
# The second argument is the file list of GIF files with the demos of all listeners
RubyLsp::CheckDocs.new(
FileList["#{__dir__}/lib/ruby_lsp/ruby_lsp_rails/**/*.rb"],
Expand All @@ -251,17 +252,17 @@ RubyLsp::CheckDocs.new(

### Dependency constraints

While we figure out a good design for the extensions API, breaking changes are bound to happen. To avoid having your
extension accidentally break editor functionality, always restrict the dependency on the `ruby-lsp` gem based on minor
versions (breaking changes may land on minor versions until we reach v1.0.0).
While we figure out a good design for the addons API, breaking changes are bound to happen. To avoid having your addon
accidentally break editor functionality, always restrict the dependency on the `ruby-lsp` gem based on minor versions
(breaking changes may land on minor versions until we reach v1.0.0).

```ruby
spec.add_dependency("ruby-lsp", "~> 0.6.0")
```

### Testing extensions
### Testing addons

When writing unit tests for extensions, it's essential to keep in mind that code is rarely in its final state while the
When writing unit tests for addons, it's essential to keep in mind that code is rarely in its final state while the
developer is coding. Therefore, be sure to test valid scenarios where the code is still incomplete.

For example, if you are writing a feature related to `require`, do not test `require "library"` exclusively. Consider
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth de
For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
documentation](SEMANTIC_HIGHLIGHTING.md).

### Extensions
### Addons

The Ruby LSP provides a server extension system that allows other gems to enhance the base functionality with more
editor features. This is the mechanism that powers extensions like
The Ruby LSP provides an addon system that allows other gems to enhance the base functionality with more editor
features. This is the mechanism that powers addons like

- [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)

For instructions on how to create extensions, see the [server extensions documentation](SERVER_EXTENSIONS.md).
For instructions on how to create addons, see the [addons documentation](ADDONS.md).

## Learn More

Expand Down
2 changes: 1 addition & 1 deletion exe/ruby-lsp-check
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ end

require_relative "../lib/ruby_lsp/internal"

RubyLsp::Extension.load_extensions
RubyLsp::Addon.load_addons

T::Utils.run_all_sig_blocks

Expand Down
48 changes: 24 additions & 24 deletions lib/ruby_lsp/extension.rb → lib/ruby_lsp/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
# frozen_string_literal: true

module RubyLsp
# To register an extension, inherit from this class and implement both `name` and `activate`
# To register an addon, inherit from this class and implement both `name` and `activate`
#
# # Example
#
# ```ruby
# module MyGem
# class MyExtension < Extension
# class MyAddon < Addon
# def activate
# # Perform any relevant initialization
# end
#
# def name
# "My extension name"
# "My addon name"
# end
# end
# end
# ```
class Extension
class Addon
extend T::Sig
extend T::Helpers

Expand All @@ -28,37 +28,37 @@ class Extension
class << self
extend T::Sig

# Automatically track and instantiate extension classes
sig { params(child_class: T.class_of(Extension)).void }
# Automatically track and instantiate addon classes
sig { params(child_class: T.class_of(Addon)).void }
def inherited(child_class)
extensions << child_class.new
addons << child_class.new
super
end

sig { returns(T::Array[Extension]) }
def extensions
@extensions ||= T.let([], T.nilable(T::Array[Extension]))
sig { returns(T::Array[Addon]) }
def addons
@addons ||= T.let([], T.nilable(T::Array[Addon]))
end

# Discovers and loads all extensions. Returns the list of activated extensions
sig { returns(T::Array[Extension]) }
def load_extensions
# Require all extensions entry points, which should be placed under
# `some_gem/lib/ruby_lsp/your_gem_name/extension.rb`
Gem.find_files("ruby_lsp/**/extension.rb").each do |extension|
require File.expand_path(extension)
# Discovers and loads all addons. Returns the list of activated addons
sig { returns(T::Array[Addon]) }
def load_addons
# Require all addons entry points, which should be placed under
# `some_gem/lib/ruby_lsp/your_gem_name/addon.rb`
Gem.find_files("ruby_lsp/**/addon.rb").each do |addon|
require File.expand_path(addon)
rescue => e
warn(e.message)
warn(e.backtrace.to_s) # rubocop:disable Lint/RedundantStringCoercion
end

# Activate each one of the discovered extensions. If any problems occur in the extensions, we don't want to
# Activate each one of the discovered addons. If any problems occur in the addons, we don't want to
# fail to boot the server
extensions.each do |extension|
extension.activate
addons.each do |addon|
addon.activate
nil
rescue => e
extension.add_error(e)
addon.add_error(e)
end
end
end
Expand Down Expand Up @@ -92,17 +92,17 @@ def backtraces
@errors.filter_map(&:backtrace).join("\n\n")
end

# Each extension should implement `MyExtension#activate` and use to perform any sort of initialization, such as
# Each addon should implement `MyAddon#activate` and use to perform any sort of initialization, such as
# reading information into memory or even spawning a separate process
sig { abstract.void }
def activate; end

# Each extension should implement `MyExtension#deactivate` and use to perform any clean up, like shutting down a
# Each addon should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
# child process
sig { abstract.void }
def deactivate; end

# Extensions should override the `name` method to return the extension name
# Addons should override the `name` method to return the addon name
sig { abstract.returns(String) }
def name; end

Expand Down
15 changes: 7 additions & 8 deletions lib/ruby_lsp/check_docs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
require "objspace"

module RubyLsp
# This rake task checks that all requests or extensions are fully documented. Add the rake task to your Rakefile and
# specify the absolute path for all files that must be required in order to discover all listeners and their
# related GIFs
# This rake task checks that all requests or addons are fully documented. Add the rake task to your Rakefile and
# specify the absolute path for all files that must be required in order to discover all listeners and their related
# GIFs
#
# # Rakefile
# request_files = FileList.new("#{__dir__}/lib/ruby_lsp/requests/*.rb") do |fl|
Expand Down Expand Up @@ -53,8 +53,7 @@ def run_task
# documented
features = ObjectSpace.each_object(Class).filter_map do |k|
klass = T.unsafe(k)
klass if klass < RubyLsp::Requests::BaseRequest ||
(klass < RubyLsp::Listener && klass != RubyLsp::ExtensibleListener)
klass if klass < Requests::BaseRequest || (klass < Listener && klass != ExtensibleListener)
end

missing_docs = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
Expand Down Expand Up @@ -82,14 +81,14 @@ def run_task
T.must(missing_docs[class_name]) << "No documentation found"
elsif !%r{\(https://microsoft.github.io/language-server-protocol/specification#.*\)}.match?(documentation)
T.must(missing_docs[class_name]) << <<~DOCS
Missing specification link. Requests and extensions should include a link to the LSP specification for the
Missing specification link. Requests and addons should include a link to the LSP specification for the
related feature. For example:
[Inlay hint](https://microsoft.github.io/language-server-protocol/specification#textDocument_inlayHint)
DOCS
elsif !documentation.include?("# Example")
T.must(missing_docs[class_name]) << <<~DOCS
Missing example. Requests and extensions should include a code example that explains what the feature does.
Missing example. Requests and addons should include a code example that explains what the feature does.
# # Example
# ```ruby
Expand All @@ -99,7 +98,7 @@ def run_task
DOCS
elsif !/\[.* demo\]\(.*\.gif\)/.match?(documentation)
T.must(missing_docs[class_name]) << <<~DOCS
Missing demonstration GIF. Each request and extension must be documented with a GIF that shows the feature
Missing demonstration GIF. Each request and addon must be documented with a GIF that shows the feature
working. For example:
# [Inlay hint demo](../../inlay_hint.gif)
Expand Down
10 changes: 5 additions & 5 deletions lib/ruby_lsp/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@ def run(request)
when "initialize"
initialize_request(request.dig(:params))
when "initialized"
Extension.load_extensions
Addon.load_addons

errored_extensions = Extension.extensions.select(&:error?)
errored_addons = Addon.addons.select(&:error?)

if errored_extensions.any?
if errored_addons.any?
@message_queue << Notification.new(
message: "window/showMessage",
params: Interface::ShowMessageParams.new(
type: Constant::MessageType::WARNING,
message: "Error loading extensions:\n\n#{errored_extensions.map(&:formatted_errors).join("\n\n")}",
message: "Error loading addons:\n\n#{errored_addons.map(&:formatted_errors).join("\n\n")}",
),
)

warn(errored_extensions.map(&:backtraces).join("\n\n"))
warn(errored_addons.map(&:backtraces).join("\n\n"))
end

perform_initial_indexing
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
require "ruby_lsp/requests"
require "ruby_lsp/listener"
require "ruby_lsp/store"
require "ruby_lsp/extension"
require "ruby_lsp/addon"
require "ruby_lsp/requests/support/rubocop_runner"
Loading

0 comments on commit b7cf80b

Please sign in to comment.