The Ruby LSP project aims to be a complete solution to provide an IDE-like experience for the Ruby language on VS Code. Its parts are:
ruby-lspgem: language server implementation and extra custom functionality to support the VS Code extension. This is the top level of the repository- Ruby code indexer: static analysis engine to support features like go to definition, completion and workspace
symbols. This is entirely implemented inside
lib/ruby_indexer - Companion VS Code extension that includes several integrations. The extension is entirely implemented in the
vscodedirectory - Jekyll static documentation site. Fully implemented in
jekyll
The Ruby LSP is organized in components that composed the full language server functionality.
The basic server functionality for communicating with the LSP client, remembering client capabilities, handling requests and notifications.
lib/ruby_lsp/server.rblib/ruby_lsp/base_server.rblib/ruby_lsp/utils.rblib/ruby_lsp/client_capabilities.rblib/ruby_lsp/global_state.rb
Requests and notifications are implemented in an extensible way so that add-ons can contribute to the base features provided.
lib/ruby_lsp/requests/: base request implementationslib/ruby_lsp/listeners/: static analysis logic, implementing through listeners. Traversal of ASTs is performed by aPrism::Dispatcherand listeners register for the node events that they are interested in handling. This allows listeners to encapsulate distinct logic without having to perform multiple traversals of the ASTlib/ruby_lsp/response_builders/: builder pattern to allow multiple listeners to contribute to the same language server response
Document related information is saved in a hash stored of { uri => Document }. The language server handles Ruby, RBS
and ERB files to provide Ruby features.
lib/ruby_lsp/store.rb: document storagelib/ruby_lsp/document.rb: base document classlib/ruby_lsp/ruby_document.rb: Ruby document handlinglib/ruby_lsp/erb_document.rb: ERB document handlinglib/ruby_lsp/rbs_document.rb: RBS document handling
The Ruby LSP includes an add-on system that allows other gems to define callbacks and listeners that can contribute to features provided in the editor.
Examples:
-
Contributing a code lens button to jump from Rails controller action to corresponding view
-
Contributing location results when trying to go to definition on the symbol used to define a Rails callback
-
Contributing diagnostics and formatting for a specific linting tool
-
Displaying a window message warning
-
lib/ruby_lsp/addon.rb: major implementation of the add-on system. Feature contributions are connected to listeners and response builders
The gem uses a mix of unit tests, which are pure Ruby, and a custom built framework that matches response expectation to fixture files.
For request or notification related tests that aren't using fixtures, the structure should use the provided test helpers:
def test_feature_name
source = <<~RUBY
# Ruby code to test
RUBY
with_server(source) do |server, _uri|
# Make LSP request
# Assert response
end
endThe custom built framework runs all language server features against the files in test/fixtures. If there's a file
with the same name under test/expectations, it will assert that the response matches what is expected for each request
that has an expectation file. If there aren't any expectation files, the feature will still run against the fixture to
verify that it does not raise. Fixture files are simply Ruby and expectation files are JSON ending in .exp.json.
The Ruby LSP codebase is fully typed with Sorbet's typed strict sigils using inline comment RBS annotations and RBI files.
Common RBS Patterns:
# Method signatures (placed above method definition)
#: (String name) -> void
def process(name)
# ...
end
# Variable annotations (placed after assignment)
@documents = {} #: Hash[URI::Generic, Document]
# Attribute type declarations (placed above attribute)
#: String?
attr_reader :parent_class
# Generic types
#: [T] () { (String) -> T } -> T
def with_cache(&block)
# ...
end
# Union and nullable types
result = nil #: (String | Symbol)?Type syntax reference: https://sorbet.org/docs/rbs-support
# Run all tests
bundle exec rake
# Run specific test file
bin/test test/requests/completion_test.rb
# Run tests matching a pattern
bin/test test/requests/completion_test.rb test_name_pattern
# Type check with Sorbet
bundle exec srb tc
# Lint with RuboCop
bin/rubocop
# Auto-fix RuboCop violations
bin/rubocop -aThe VS Code extension provides several integrations, some of which interact with the language server.
The extension's entrypoint is implemented in vscode/src/extension.ts and vscode/src/rubyLsp.ts. This is where we
handle activation, detecting workspaces, registering commands and subscribers.
- Language server client:
vscode/src/client.ts - LLM chat agent:
vscode/src/chatAgent.ts - Debug gem client:
vscode/src/debugger.ts - Dependencies view:
vscode/src/dependenciesTree.ts(integrates with language server)
A critical part of the extension is integrating with version managers. This is necessary because the Ruby LSP server is
a Ruby process that requires gems from the user's application (such as their formatter or linter). In order to require
the correct version of the gems being used, the environment being used in the extension must match exactly the
environment of the user's shell. Otherwise, bundle install might fail or key environment variables like $GEM_HOME
might be pointing to the wrong path.
vscode/src/ruby.ts: the main Ruby environment handling objectvscode/src/ruby/*.ts: all supported version manager integrations
The Ruby LSP's implementation of the VS Code test explorer allows handling any Ruby test framework by add-on contributions. The explorer connects to the LSP client to be able to ask questions about test files and determine which groups and examples exist in the codebase. This infrastructure is all custom built with language server custom requests.
While tests are running, the Ruby LSP server hooks into the process with an LSP test reporter to stream events through a TCP socket, so that the explorer is able to show the status of each test (extension is the TCP server and the test process is the client).
vscode/src/testController.tsandvscode/src/streamingRunner.ts: extension side implementation of test explorer and streaming event serverlib/ruby_lsp/test_reporters/lsp_reporter.rb: LSP reporter implementationlib/ruby_lsp/test_reporters/minitest_reporter.rb: Minitest reporter integrationlib/ruby_lsp/test_reporters/test_unit_reporter.rb: Test Unit reporter integrationlib/ruby_lsp/listeners/test_style.rb: Minitest and Test Unit test discovery and command resolution for test style (classes with method definitions)lib/ruby_lsp/listeners/spec_style.rb: Minitest test discovery and command resolution for the spec style (describe, it)
yarn run lint # Lint TypeScript code
yarn run test # Run extension tests