From 099c15771fa0ae6142609f685938aac0d7d138c3 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 9 Oct 2026 20:08:15 +0200 Subject: [PATCH] feat: add Solargraph as the built-in Ruby LSP preset --- README.md | 5 ++++- examples/config.rb | 1 + src/lsp_config.rs | 9 +++++---- src/main.rs | 1 + src/ruby.rs | 27 +++++++++++++++++++-------- src/session.rs | 4 ++-- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 32d5169..c5215d4 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ LSP runs in Rust, independently of Ruby plugin hosts. **Every language server is ```ruby Vamp.configure do |config| config.lsp["rust"] = true # rust-analyzer, extensions: ["rs"] + config.lsp["ruby"] = true # solargraph stdio, extensions: ["rb"] config.lsp["python"] = false # disabled (also the default when absent) config.lsp["my-language"] = { @@ -267,10 +268,12 @@ Vamp.configure do |config| end ``` -Keys are arbitrary nonempty strings, used as the protocol's `languageId`; they need no Tree-sitter grammar or hardcoded language enum. Absent/`false` entries are disabled. `true` enables a built-in preset: currently `"rust"` uses `rust-analyzer`/`rs`, and `"python"` uses `pylsp`/`py`. Unknown keys need a configuration object with nonempty `command` argv and `extensions` (without leading dots). Objects also explicitly enable servers; known presets supply omitted command/extensions. For example, `config.lsp["rust"] = { command: ["/custom/path/rust-analyzer"] }` overrides the executable while retaining `rs`. +Keys are arbitrary nonempty strings, used as the protocol's `languageId`; they need no Tree-sitter grammar or hardcoded language enum. Absent/`false` entries are disabled. `true` enables a built-in preset: `"rust"` uses `rust-analyzer`/`rs`, `"python"` uses `pylsp`/`py`, and `"ruby"` uses `solargraph stdio`/`rb`. Unknown keys need a configuration object with nonempty `command` argv and `extensions` (without leading dots). Objects also explicitly enable servers; known presets supply omitted command/extensions. For example, `config.lsp["rust"] = { command: ["/custom/path/rust-analyzer"] }` overrides the executable while retaining `rs`. Only `command`, `extensions`, `initialization_options`, and `settings` are accepted. Both option/settings values must be JSON-compatible objects and default to `{}`. Invalid declarations reject configuration with a notice rather than activating a guessed server. Server commands are executable/argument arrays, never shell commands; use an absolute path if the executable is not on PATH. No server installation or restart is automatic. +Install Solargraph yourself with `gem install solargraph` in the project's Ruby environment. The Ruby preset launches `["solargraph", "stdio"]`, following [Solargraph's stdio integration](https://solargraph.org/guides/language-server), and uses [YARD annotations](https://solargraph.org/guides/yard) for type information. Explicit `command` overrides replace the entire argv: for a bundled installation, use `config.lsp["ruby"] = { command: ["bundle", "exec", "solargraph", "stdio"] }`; Vamp retains the `rb` extension but does not append `stdio` to your override. + Servers start lazily only when a matching buffer opens. Extension matching is case-sensitive; if enabled languages overlap, the first language in sorted key order wins. One worker runs per language configuration/project root, with at most 16 enabled languages/server-project keys. Start vamp from your project root: its startup working directory is the workspace; automatic root discovery is not implemented. The native module initializes each server, sends `initialization_options` as `initializationOptions`, answers `workspace/configuration` from `settings` (including dotted sections; missing sections return `null`), honors open/change/save capabilities, synchronizes in-memory text including hidden dirty buffers, and sends close events. Incremental servers receive whole-document range replacements. Process/pipe I/O and requests stay on bounded background workers; input and idle diagnostic redraw do not wait for server responses. diff --git a/examples/config.rb b/examples/config.rb index 0dc1e4f..5009187 100644 --- a/examples/config.rb +++ b/examples/config.rb @@ -15,6 +15,7 @@ Vamp.configure do |config| config.plugin "plugins/greeting.rb" # Native LSP is disabled by default; install a server and explicitly opt in. # config.lsp["rust"] = true # built-in rust-analyzer preset + # config.lsp["ruby"] = true # built-in Solargraph stdio preset (gem install solargraph) # config.lsp["my-language"] = { # command: ["my-language-server", "--stdio"], extensions: ["myl"], # initialization_options: {}, settings: {} diff --git a/src/lsp_config.rs b/src/lsp_config.rs index 4ddfb71..3326068 100644 --- a/src/lsp_config.rs +++ b/src/lsp_config.rs @@ -10,9 +10,10 @@ pub fn resolve(value: Value) -> Result, String> { if language_id.is_empty() || language_id.chars().any(char::is_control) { return Err("LSP language keys must not be empty or contain control characters".into()); } - let preset = match language_id.as_str() { - "rust" => Some(("rust-analyzer", "rs")), - "python" => Some(("pylsp", "py")), + let preset: Option<(&[&str], &str)> = match language_id.as_str() { + "rust" => Some((&["rust-analyzer"], "rs")), + "python" => Some((&["pylsp"], "py")), + "ruby" => Some((&["solargraph", "stdio"], "rb")), _ => None, }; let mut options = match value { @@ -45,7 +46,7 @@ pub fn resolve(value: Value) -> Result, String> { let command = match options.remove("command") { Some(value) => command(value, &language_id)?, None => match preset { - Some((command, _)) => vec![command.into()], + Some((command, _)) => command.iter().map(|argument| (*argument).into()).collect(), None => { return Err(format!( "LSP language {language_id:?} requires command argv" diff --git a/src/main.rs b/src/main.rs index 9aa5d49..697ca8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,7 @@ Ruby config: $XDG_CONFIG_HOME/vamp/config.rb or ~/.config/vamp/config.rb Set VAMP_RUBY to choose the Ruby executable. Native LSP is disabled by default: config.lsp[\"rust\"] = true enables a preset +Presets: rust -> rust-analyzer, python -> pylsp, ruby -> solargraph stdio Custom languages: config.lsp[\"my-language\"] = { command: [...], extensions: [...] } LSP commands: :lsp.definition, :lsp.diagnostics, :lsp.hover Hover binding: config.bind \"K\", config.core.lsp.hover (no default key)"; diff --git a/src/ruby.rs b/src/ruby.rs index 8f31465..5cc276a 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -969,19 +969,21 @@ end"#, r#"Vamp.configure do |config| config.lsp["rust"] = true config.lsp["python"] = true + config.lsp["ruby"] = true end"#, ); let config = load_config(&path).unwrap(); - assert_eq!(config.lsp.len(), 2); - for (language, command, extension) in - [("rust", "rust-analyzer", "rs"), ("python", "pylsp", "py")] - { + for (language, command, extension) in [ + ("rust", &["rust-analyzer"][..], "rs"), + ("python", &["pylsp"][..], "py"), + ("ruby", &["solargraph", "stdio"][..], "rb"), + ] { let server = config .lsp .iter() .find(|server| server.language_id == language) .unwrap(); - assert_eq!(server.command, [command]); + assert_eq!(server.command, command); assert_eq!(server.extensions, [extension]); assert_eq!(server.initialization_options, serde_json::json!({})); assert_eq!(server.settings, serde_json::json!({})); @@ -996,6 +998,7 @@ end"#, settings: {"rust-analyzer" => {check: {command: "clippy"}}} } config.lsp["python"] = {command: ["custom pylsp", "--stdio"]} + config.lsp["ruby"] = {command: ["bundle", "exec", "solargraph", "stdio"]} config.lsp["my-language.c++"] = { command: ["custom server", "--flag", ""], extensions: ["custom", "لغة"], @@ -1004,7 +1007,6 @@ end"#, end"#, ); let config = load_config(&path).unwrap(); - assert_eq!(config.lsp.len(), 3); let rust = config .lsp .iter() @@ -1029,6 +1031,13 @@ end"#, assert_eq!(python.extensions, ["py"]); assert_eq!(python.initialization_options, serde_json::json!({})); assert_eq!(python.settings, serde_json::json!({})); + let ruby = config + .lsp + .iter() + .find(|server| server.language_id == "ruby") + .unwrap(); + assert_eq!(ruby.command, ["bundle", "exec", "solargraph", "stdio"]); + assert_eq!(ruby.extensions, ["rb"]); let custom = config .lsp .iter() @@ -1087,12 +1096,14 @@ end"#, } let path = fixture.file( "invalid.rb", - r#"Vamp.configure { |config| config.lsp["ruby"] = true }"#, + r#"Vamp.configure { |config| config.lsp["unsupported-language"] = true }"#, ); let error = load_config(&path).unwrap_err(); assert!(!error.contains("Malformed Ruby protocol frame"), "{error}"); assert!( - error.contains("\"ruby\"") && error.contains("command") && error.contains("extensions"), + error.contains("\"unsupported-language\"") + && error.contains("command") + && error.contains("extensions"), "{error}" ); } diff --git a/src/session.rs b/src/session.rs index a4e6d59..9e8b995 100644 --- a/src/session.rs +++ b/src/session.rs @@ -2355,8 +2355,8 @@ end for (config, reason, dismiss) in [ ("raise 'broken config'", "broken config", Input::Enter), ( - "Vamp.configure { |config| config.lsp['ruby'] = true }", - "ruby", + "Vamp.configure { |config| config.lsp['unsupported-language'] = true }", + "unsupported-language", Input::Escape, ), ] {