Ruby LSP

repository·main·Indexed 24 days ago

https://github.com/shopify/ruby-lsp

A 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.

Tokens
42.3K
Snippets
83
Records
216
Agent score
83%

What's inside ruby-lsp

  1. Overview of Ruby LSP

    main
    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).
  2. Overview of Ruby LSP features

    main

    Ruby 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.
  3. Ruby LSP Long Term Roadmap

    main

    The 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), rename support, 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 method and extract 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.
  4. How Ruby LSP activation works

    main

    The 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 rbenv or asdf), the extension must explicitly invoke your shell in interactive mode to capture the correct environment.

    1. The extension runs a command using your shell's interactive mode (e.g., zsh -ic).
    2. This loads your configuration files (like ~/.zshrc).
    3. The command executes your Ruby version manager (e.g., rbenv exec ruby) and prints the resulting environment variables as JSON.
    4. The extension reads this JSON to inject the correct Ruby version and gem paths into the NodeJS process.
    5. To avoid requiring users to add ruby-lsp to their project's Gemfile, the extension creates a composed bundle inside a .ruby-lsp directory within your project.
  5. How multi-root workspaces work with Ruby LSP

    main

    Ruby 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-workspace file. You can use files.exclude in 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,
        },
      },
    }
  6. Identify Ruby LSP Code Execution Vectors

    main

    Ruby LSP executes code from your workspace through several mechanisms:

    Bundle Installation

    Ruby LSP automatically performs bundler operations (such as bundle install or bundle update) when starting up or when detecting changes to your Gemfile. 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.rb files.
    • Files matching ruby_lsp/**/addon.rb anywhere in your workspace.

    Add-ons are loaded via require and their activate method is called, allowing them to execute arbitrary Ruby code, including spawning processes or making network requests.

  7. Understand Guessed Types for Completion

    main

    Ruby LSP attempts to identify the type of a receiver based on its identifier to provide better method completion.

    How it works:

    1. It tries to resolve a constant based on the receiver identifier and current nesting (e.g., user inside module Admin matches Admin::User).
    2. 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.a

    Warning: 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.

  8. Experimental feature: Ancestors Hierarchy Request

    main

    The Ancestors Hierarchy Request is an experimental feature that implements the Type Hierarchy Supertypes LSP 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.

  9. Hook Ruby LSP into a private telemetry service

    main

    Ruby 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.TelemetrySender interface, including the following methods:

    • sendEventData(eventName: string, data: EventData): void
    • sendErrorData(error: Error, data?: Record<string, any>): void
    • flush(): 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;
        }),
      );
    }
  10. How the Composed Ruby LSP bundle works

    main

    To integrate seamlessly with project-specific dependencies (like linters, formatters, and test frameworks) without requiring users to manually add ruby-lsp to their project's Gemfile, Ruby LSP uses a composed bundle strategy.

    When you run the ruby-lsp executable, it performs the following lifecycle:

    1. Initialization: The executable is run directly as ruby-lsp (without bundle exec).
    2. Bundle Configuration: It creates a local directory at your_project/.ruby-lsp and generates a new Gemfile inside it. This generated Gemfile includes:
      • The ruby-lsp gem.
      • All gems listed in your project's original Gemfile.
      • Optional gems like debug and ruby-lsp-rails.
    3. Installation & Update: The logic runs bundle install and attempts to auto-update the ruby-lsp gem to ensure you have the latest features and fixes.
    4. 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_PATH and dependencies.

    This approach allows the language server to automatically detect and index declarations from gems without manual user configuration.

  11. Check if a block is provided using `block_given?`

    main

    To avoid LocalJumpError when using yield, use the block_given? method. This method returns true if a block was passed to the current method and false otherwise, 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!