feat: migrate LSP to opt-in native language servers

This commit is contained in:
Valdemar Lorenzen
2026-10-09 13:12:38 +02:00
parent 92679ecbe1
commit 30aa70672e
12 changed files with 2407 additions and 658 deletions
+102 -9
View File
@@ -2,7 +2,7 @@
use std::{
fs,
path::{Path, PathBuf},
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
thread,
time::{Duration, Instant},
@@ -17,7 +17,7 @@ use vamp::{
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
// A real subprocess speaking framed LSP, not a plugin echoing generic effects.
// A real subprocess speaking framed LSP to the native client.
// Files are explicit test-controlled gates; no wall-clock ordering assumptions.
const SERVER: &str = r#"
require 'json'
@@ -213,13 +213,14 @@ impl Fixture {
self.alternate
])
};
let options = json!({"servers": [{
"language_id": "rust", "extensions": ["rs"], "command": command,
}]});
let plugin = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/plugins/lsp.rb");
let options = json!({
"extensions": if mode == "irrelevant" { vec!["myl"] } else { vec!["rs"] },
"command": command,
"initialization_options": {"fixture": true},
"settings": {"rust-analyzer": {"cargo": {"allFeatures": true}}},
});
let config = format!(
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.plugin {}, options: JSON.parse({})\nend\n",
serde_json::to_string(&plugin).unwrap(),
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.lsp['fixture-language'] = JSON.parse({})\nend\n",
serde_json::to_string(&options.to_string()).unwrap(),
);
let config_path = self.root.join("config.rb");
@@ -323,6 +324,10 @@ fn lsp_framing_lifecycle_diagnostics_and_unicode_dirty_definition() {
initialize["params"]["capabilities"]["general"]["positionEncodings"],
json!(["utf-16"])
);
assert_eq!(
initialize["params"]["initializationOptions"],
json!({"fixture": true})
);
assert_eq!(
initialize["params"]["capabilities"]["textDocument"]["publishDiagnostics"]["versionSupport"],
true
@@ -340,6 +345,10 @@ fn lsp_framing_lifecycle_diagnostics_and_unicode_dirty_definition() {
.position(|frame| frame["method"] == "initialized")
.unwrap();
assert!(initialized_index < open_index);
assert_eq!(
frames[open_index]["params"]["textDocument"]["languageId"],
"fixture-language"
);
assert_eq!(
frames[open_index]["params"]["textDocument"]["text"],
"A😀e\u{301}Z\n"
@@ -368,7 +377,7 @@ fn lsp_framing_lifecycle_diagnostics_and_unicode_dirty_definition() {
.iter()
.find(|frame| frame["id"] == "settings")
.unwrap()["result"],
json!([null, null])
json!([{"cargo": {"allFeatures": true}}, null])
);
assert_eq!(
frames.iter().find(|frame| frame["id"] == 0).unwrap()["error"]["code"],
@@ -698,3 +707,87 @@ fn lsp_optional_configuration_numeric_sync_and_unversioned_diagnostics() {
assert_eq!(fixture.method_count("textDocument/didChange"), 0);
assert_eq!(fixture.method_count("textDocument/didSave"), 0);
}
#[test]
fn lsp_definition_reuses_dirty_targets_without_an_enabled_target_language() {
let mut fixture = Fixture::new();
let target = fixture.target.with_extension("txt");
fs::rename(&fixture.target, &target).unwrap();
fixture.target = target;
let mut session = fixture.open("basic");
until(&mut session, &fixture, |session| {
session.editor().diagnostics().count() == 2
});
let target_id = edit_target(&mut session, &fixture);
session.execute("lsp_definition");
until(&mut session, &fixture, |session| {
session.editor().buffer_id() == target_id
});
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 1 });
assert_eq!(session.editor().lines(), &["😀e\u{301}disk", ""]);
assert!(session.editor().dirty());
assert_eq!(fs::read_to_string(&fixture.target).unwrap(), "disk\n");
assert_eq!(fixture.method_count("textDocument/didOpen"), 1);
}
#[test]
fn lsp_definition_does_not_move_a_caret_that_has_moved_since_the_request() {
let fixture = Fixture::new();
let mut session = fixture.open("stale");
until(&mut session, &fixture, |session| {
session.editor().diagnostics().count() == 2
});
session.execute("lsp_definition");
until(&mut session, &fixture, |_| {
fixture.method_count("textDocument/definition") == 1
});
session.handle(Input::Right);
fixture.release();
until(&mut session, &fixture, |session| {
session
.editor()
.message()
.contains("stale response complete")
});
assert_eq!(session.editor().buffer_id(), 1);
assert_eq!(session.editor().buffer_count(), 1);
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 1 });
}
#[test]
fn lsp_only_starts_an_explicitly_configured_server_when_a_matching_buffer_opens() {
let fixture = Fixture::new();
let mut session = fixture.open("irrelevant");
session.execute("lsp_diagnostics");
until(&mut session, &fixture, |session| {
session.editor().message().contains("no running server")
});
assert!(!fixture.log.exists());
let custom = fixture.root.join("custom.myl");
fs::write(&custom, "custom language\n").unwrap();
session.execute(&format!("edit {}", custom.display()));
until(&mut session, &fixture, |_| {
fixture.method_count("textDocument/didOpen") == 1
});
let frames = fixture.frames();
let open = frames
.iter()
.find(|frame| frame["method"] == "textDocument/didOpen")
.unwrap();
assert_eq!(
open["params"]["textDocument"]["languageId"],
"fixture-language"
);
assert_eq!(open["params"]["textDocument"]["text"], "custom language\n");
}
#[test]
fn lsp_session_shutdown_finishes_the_server_protocol_before_returning() {
let fixture = Fixture::new();
let mut session = fixture.open("basic");
until(&mut session, &fixture, |session| {
session.editor().diagnostics().count() == 2
});
drop(session);
assert_eq!(fixture.method_count("shutdown"), 1);
}