feat: add Solargraph as the built-in Ruby LSP preset

This commit is contained in:
2026-10-09 20:08:15 +02:00
parent 788638f34e
commit 099c15771f
6 changed files with 32 additions and 15 deletions
+4 -1
View File
@@ -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.
+1
View File
@@ -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: {}
+5 -4
View File
@@ -10,9 +10,10 @@ pub fn resolve(value: Value) -> Result<Vec<ServerConfig>, 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<Vec<ServerConfig>, 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"
+1
View File
@@ -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)";
+19 -8
View File
@@ -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}"
);
}
+2 -2
View File
@@ -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,
),
] {