Ruby LSP
repository·main·Indexed 24 days ago
https://github.com/shopify/ruby-lspA language server implementation for Ruby that provides editor features such as autocompletion, code navigation, and documentation via the Language Server Protocol. It includes an official VS Code extension, a CLI for server management and troubleshooting, and an experimental add-on system for extending functionality through gems.
What's inside ruby-lsp
- Ruby LSP is an implementation of the Language Server Protocol (LSP) for the Ruby programming language. It is designed to provide modern, cross-editor features such as code completion, navigation, and documentation, aiming to improve the overall Ruby developer experience (DX).
Overview of Ruby LSP features
mainRuby LSP implements the Language Server Protocol for Ruby to provide rich editor features. Key capabilities include:
- Code Intelligence: Semantic highlighting, symbol search, code outline, go to definition, showing documentation on hover, and completion.
- Code Quality: RuboCop diagnostics (errors and warnings) and formatting (on save or on type) using RuboCop or Syntax Tree.
- Project Navigation: Fuzzy search for declarations anywhere in the workspace.
- Testing & Debugging: Debugging support and running/debugging tests via the VS Code UI.
- Rails Integration: Running Rails generators directly from the UI.
Ruby LSP Long Term Roadmap
mainThe Ruby LSP roadmap outlines major milestones for the project. Key areas of focus include:
Core Language Server Features
- Navigation & Discovery: Adding support for
find references(methods, instance variables, local variables),renamesupport,show type hierarchy, and navigation between related code (e.g., from tests to implementations). - Code Intelligence: Full method support for definition, hover, and completion; improving accuracy via class/module hierarchies; and supporting intrinsics for methods like
new. - Refactoring: Adding code actions such as
extract to methodandextract to class/module. - Formatting: Adding range formatting support for compatible formatters.
- Template Support: Adding ERB support.
Architecture & Performance
- Add-on Ecosystem: Stabilizing APIs for add-ons to allow gems to enhance base features, enabling automatic add-on detection, and allowing add-ons to support arbitrary file types.
- Type Checking: Exploring connections to typechecker add-ons (like Sorbet or Steep) to improve accuracy, using the default Ruby LSP functionality as a fallback.
- Indexing & Speed: Improving indexing speed via gem index caching and lazy Prism AST allocations; developing strategies to index native extensions or C code (e.g., Ruby Core classes).
- Environment: Making Ruby environment activation more flexible and less coupled with shells.
Developer Experience
- VS Code Integration: Showing an index view in the VS Code extension to browse indexed gems.
- Navigation & Discovery: Adding support for
How Ruby LSP activation works
mainThe Ruby LSP extension runs inside the VS Code NodeJS runtime. Because NodeJS does not automatically inherit environment variables set in your shell (like those from
rbenvorasdf), the extension must explicitly invoke your shell in interactive mode to capture the correct environment.- The extension runs a command using your shell's interactive mode (e.g.,
zsh -ic). - This loads your configuration files (like
~/.zshrc). - The command executes your Ruby version manager (e.g.,
rbenv exec ruby) and prints the resulting environment variables as JSON. - The extension reads this JSON to inject the correct Ruby version and gem paths into the NodeJS process.
- To avoid requiring users to add
ruby-lspto their project'sGemfile, the extension creates a composed bundle inside a.ruby-lspdirectory within your project.
- The extension runs a command using your shell's interactive mode (e.g.,
How multi-root workspaces work with Ruby LSP
mainRuby LSP supports VS Code multi-root workspaces by spawning a separate language server instance for each workspace root. This allows each workspace to use a different Ruby version and a different set of gems, which would be impossible in a single process.
To ensure proper functionality, you must define the workspace folders in a
.code-workspacefile. You can usefiles.excludein the workspace settings to prevent the same files from appearing twice in the explorer if one directory is a sub-directory of another.{ "folders": [ { "name": "rails", "path": ".", }, { "name": "react", "path": "frontend", }, ], "settings": { "files.exclude": { "frontend": true, }, }, }Identify Ruby LSP Code Execution Vectors
mainRuby LSP executes code from your workspace through several mechanisms:
Bundle Installation
Ruby LSP automatically performs bundler operations (such as
bundle installorbundle update) when starting up or when detecting changes to yourGemfile. This results in:- Execution of any Ruby code contained within your
Gemfile. - Installation of gems, which may include native extensions that execute during installation.
- Execution of any post-install hooks defined by gems.
Add-ons / Plugins
Ruby LSP automatically discovers and loads add-ons from:
- Gems in your bundle that contain
ruby_lsp/**/addon.rbfiles. - Files matching
ruby_lsp/**/addon.rbanywhere in your workspace.
Add-ons are loaded via
requireand theiractivatemethod is called, allowing them to execute arbitrary Ruby code, including spawning processes or making network requests.- Execution of any Ruby code contained within your
Understand Guessed Types for Completion
mainRuby LSP attempts to identify the type of a receiver based on its identifier to provide better method completion.
How it works:
- It tries to resolve a constant based on the receiver identifier and current nesting (e.g.,
userinsidemodule AdminmatchesAdmin::User). - If nesting doesn't resolve it, it falls back to matching the first unqualified type name found in the project.
Usage for exploration: You can use this to quickly explore methods in a class by typing the lowercase name of the class as an identifier:
pathname.a integer.a file.aWarning: This is an experimental feature. Do not rename variables solely to improve type guessing; readability should always come first. It can be easily fooled if a variable name does not match its actual type.
- It tries to resolve a constant based on the receiver identifier and current nesting (e.g.,
Experimental feature: Ancestors Hierarchy Request
mainThe Ancestors Hierarchy Request is an experimental feature that implements the
Type Hierarchy SupertypesLSP request. It allows developers to:- Visualize the inheritance hierarchy of classes and modules.
- Quickly navigate through the inheritance chain.
Because it is experimental, behavior regarding singleton classes and the inclusion of modules versus pure classes is subject to change based on feedback and LSP specification clarifications.
Hook Ruby LSP into a private telemetry service
mainRuby LSP does not collect telemetry by default but allows integration with private metrics services. To enable this, you must create a separate VS Code extension that registers the command
getTelemetrySenderObject.This command must return an object that implements the
vscode.TelemetrySenderinterface, including the following methods:sendEventData(eventName: string, data: EventData): voidsendErrorData(error: Error, data?: Record<string, any>): voidflush(): Promise<void>(optional)
// Your private VS Code extension class Telemetry implements vscode.TelemetrySender { constructor() { // Initialize some API service or whatever is needed to collect metrics } sendEventData(eventName: string, data: EventData): void { // Send events to some API or accumulate them to be sent in batch when `flush` is invoked by VS Code } sendErrorData(error: Error, data?: Record<string, any> | undefined): void { // Send errors to some API or accumulate them to be sent in batch when `flush` is invoked by VS Code } async flush() { // Optional function to flush accumulated events and errors } } export async function activate(context: vscode.ExtensionContext) { const telemetry = new Telemetry(); await telemetry.activate(); // Register the command that the Ruby LSP will search for to hook into context.subscriptions.push( vscode.commands.registerCommand("getTelemetrySenderObject", () => { return telemetry; }), ); }How the Composed Ruby LSP bundle works
mainTo integrate seamlessly with project-specific dependencies (like linters, formatters, and test frameworks) without requiring users to manually add
ruby-lspto their project'sGemfile, Ruby LSP uses a composed bundle strategy.When you run the
ruby-lspexecutable, it performs the following lifecycle:- Initialization: The executable is run directly as
ruby-lsp(withoutbundle exec). - Bundle Configuration: It creates a local directory at
your_project/.ruby-lspand generates a newGemfileinside it. This generated Gemfile includes:- The
ruby-lspgem. - All gems listed in your project's original
Gemfile. - Optional gems like
debugandruby-lsp-rails.
- The
- Installation & Update: The logic runs
bundle installand attempts to auto-update theruby-lspgem to ensure you have the latest features and fixes. - Process Replacement: Once the environment is ready, the original process is replaced by a new command:
BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp. This ensures the language server runs with full access to your project's$LOAD_PATHand dependencies.
This approach allows the language server to automatically detect and index declarations from gems without manual user configuration.
- Initialization: The executable is run directly as
Experimental feature: Copilot chat participant
mainRuby LSP includes a Copilot chat participant. It has built-in knowledge of Ruby and Rails commands, assisting users in building these commands efficiently within the Copilot chat interface.Check if a block is provided using `block_given?`
mainTo avoid
LocalJumpErrorwhen usingyield, use theblock_given?method. This method returnstrueif a block was passed to the current method andfalseotherwise, allowing you to handle the absence of a block gracefully.def foo if block_given? result = yield(10) puts result else puts "No block passed!" end end foo do |a| a * 2 end # => 20 foo # => No block passed!