A fast, lightweight LSP server for Ruby projects written in Go.
Provides go to definition and find references for Ruby codebases with minimal setup and fast startup.
go install github.com/jarredhawkins/goruby-lsp/cmd/goruby-lsp@latestOr build from source:
git clone https://github.com/jarredhawkins/goruby-lsp
cd goruby-lsp
go install ./cmd/goruby-lspRun from your Ruby project root:
cd /path/to/your/rails/app
goruby-lsp| Flag | Description |
|---|---|
--root <path> |
Root path of the Ruby project (defaults to cwd) |
--log <file> |
Log file path (defaults to stderr) |
--debug |
Enable debug logging |
VS Code: Add to .vscode/settings.json:
{
"ruby.languageServer": {
"command": "goruby-lsp"
}
}Neovim (with nvim-lspconfig):
require('lspconfig.configs').goruby_lsp = {
default_config = {
cmd = { 'goruby-lsp' },
filetypes = { 'ruby' },
root_dir = require('lspconfig.util').root_pattern('Gemfile', '.git'),
},
}
require('lspconfig').goruby_lsp.setup({})- textDocument/definition - Jump to class, module, method, and constant definitions
- textDocument/references - Find all usages of a symbol using trigram search
- Live reindexing - File changes are detected via fsnotify and the index updates automatically
This project uses regex-based parsing rather than a proper AST parser like tree-sitter or prism.
| Benefit | Explanation |
|---|---|
| Fast startup | No grammar loading or parser initialization overhead |
| Simple plugins | Adding new patterns is just writing a regex |
| Small binary | Works on most repos straight from binary installation |
| No composed gemfile | Avoids a lot of complexity that comes with using the composed Gemfile |
| Limitation | Impact |
|---|---|
| No AST | Can't resolve scope accurately in complex cases |
| Edge cases | Misses definitions inside heredocs, multiline strings, or unusual formatting |
| No type inference | Can't follow include/extend to find inherited methods |
| Metaprogramming | define_method, class_eval, etc. are invisible |
Compared to the Ruby LSP, I've found this trades latency and false negatives, for speed and false positives. I find this generally fits my workflow better than the ruby-lsp, but ultimately that tradeoff is up to you.
| Use goruby-lsp when... | Use ruby-lsp when... |
|---|---|
| You just need "good enough" navigation | You need completion, hover, diagnostics |
| You want a single binary with no Ruby deps | You don't mind Ruby/gem dependencies |
| You want instant startup | You need accurate type inference |
| You're on a large monorepo | You need metaprogramming support |
- On startup, walks the project tree and indexes all
.rbfiles - Parses Ruby files line-by-line using regex patterns to extract definitions
- Builds an in-memory symbol index and trigram index for fast lookups
- Watches for file changes and incrementally updates the index
| Construct | Example |
|---|---|
| Classes | class MyClass, class MyModule::MyClass < Base |
| Modules | module MyModule |
| Methods | def my_method, def self.class_method |
| Constants | MY_CONST = value |
| Rails relations | belongs_to :user, has_many :posts, has_one :profile |
The parser uses a plugin system—additional patterns (like attr_accessor, Rails DSLs) can be added.
Create a new matcher in internal/plugins/:
package plugins
import (
"regexp"
"github.com/jarredhawkins/goruby-lsp/internal/parser"
"github.com/jarredhawkins/goruby-lsp/internal/types"
)
var attrPattern = regexp.MustCompile(`^\s*attr_(reader|writer|accessor)\s+(.+)`)
type AttrMatcher struct{}
func (m *AttrMatcher) Name() string { return "attr" }
func (m *AttrMatcher) Priority() int { return 85 }
func (m *AttrMatcher) Match(line string, ctx *parser.ParseContext) *parser.MatchResult {
match := attrPattern.FindStringSubmatch(line)
if match == nil {
return nil
}
// Extract symbols from match...
return &parser.MatchResult{Symbols: symbols}
}Then register it in parser.RegisterDefaults().
Unlicense - Public domain. Do whatever you want.