feat: migrate LSP to opt-in native language servers
This commit is contained in:
+215
-3
@@ -19,6 +19,7 @@ const INVOKE_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub plugins: Vec<PluginConfig>,
|
||||
pub lsp: Vec<crate::lsp::ServerConfig>,
|
||||
pub bindings: Vec<Binding>,
|
||||
pub leader: String,
|
||||
pub file_explorer_side: String,
|
||||
@@ -52,6 +53,7 @@ impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plugins: Vec::new(),
|
||||
lsp: Vec::new(),
|
||||
bindings: Vec::new(),
|
||||
leader: "space".into(),
|
||||
file_explorer_side: "right".into(),
|
||||
@@ -71,6 +73,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
match response {
|
||||
Response::Config {
|
||||
plugins,
|
||||
lsp,
|
||||
bindings,
|
||||
leader,
|
||||
file_explorer_side,
|
||||
@@ -92,6 +95,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
});
|
||||
}
|
||||
config.bindings = bindings;
|
||||
config.lsp = lsp;
|
||||
config.leader = leader;
|
||||
config.file_explorer_side = file_explorer_side;
|
||||
config.file_picker_view = file_picker_view;
|
||||
@@ -375,6 +379,8 @@ enum Response {
|
||||
Config {
|
||||
version: u8,
|
||||
plugins: Vec<PluginConfig>,
|
||||
#[serde(deserialize_with = "crate::lsp_config::deserialize")]
|
||||
lsp: Vec<crate::lsp::ServerConfig>,
|
||||
bindings: Vec<Binding>,
|
||||
leader: String,
|
||||
file_explorer_side: String,
|
||||
@@ -879,6 +885,7 @@ end
|
||||
let defaults = Config::default();
|
||||
assert_eq!(defaults.leader, "space");
|
||||
assert!(defaults.plugins.is_empty() && defaults.bindings.is_empty());
|
||||
assert!(defaults.lsp.is_empty());
|
||||
let path = fixture.file("config.rb", "Vamp.configure { |_| }");
|
||||
assert_eq!(load_config(&path).unwrap().leader, "space");
|
||||
for (source, key) in [
|
||||
@@ -900,14 +907,219 @@ end
|
||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||
}
|
||||
for frame in [
|
||||
r#"{"type":"config","version":1,"plugins":[],"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system"}"#,
|
||||
] {
|
||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_configuration_is_disabled_unless_explicitly_enabled() {
|
||||
let fixture = Fixture::new();
|
||||
for source in [
|
||||
"Vamp.configure { |_| }",
|
||||
r#"Vamp.configure do |config|
|
||||
config.lsp["rust"] = false
|
||||
config.lsp["python"] = false
|
||||
config.lsp["not-a-preset"] = false
|
||||
end"#,
|
||||
] {
|
||||
let path = fixture.file("config.rb", source);
|
||||
let config = load_config(&path).unwrap();
|
||||
assert!(config.lsp.is_empty());
|
||||
assert!(config.plugins.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_presets_custom_languages_and_overrides_reach_native_configuration() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
r#"Vamp.configure do |config|
|
||||
config.lsp["rust"] = true
|
||||
config.lsp["python"] = 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")]
|
||||
{
|
||||
let server = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == language)
|
||||
.unwrap();
|
||||
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!({}));
|
||||
}
|
||||
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
r#"Vamp.configure do |config|
|
||||
config.lsp["rust"] = {
|
||||
extensions: ["rs", "ron"],
|
||||
initialization_options: {cargo: {features: ["α"]}},
|
||||
settings: {"rust-analyzer" => {check: {command: "clippy"}}}
|
||||
}
|
||||
config.lsp["python"] = {command: ["custom pylsp", "--stdio"]}
|
||||
config.lsp["my-language.c++"] = {
|
||||
command: ["custom server", "--flag", ""],
|
||||
extensions: ["custom", "لغة"],
|
||||
settings: {nested: {enabled: true, limit: 3, unset: nil}}
|
||||
}
|
||||
end"#,
|
||||
);
|
||||
let config = load_config(&path).unwrap();
|
||||
assert_eq!(config.lsp.len(), 3);
|
||||
let rust = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == "rust")
|
||||
.unwrap();
|
||||
assert_eq!(rust.command, ["rust-analyzer"]);
|
||||
assert_eq!(rust.extensions, ["rs", "ron"]);
|
||||
assert_eq!(
|
||||
rust.initialization_options,
|
||||
serde_json::json!({"cargo": {"features": ["α"]}})
|
||||
);
|
||||
assert_eq!(
|
||||
rust.settings,
|
||||
serde_json::json!({"rust-analyzer": {"check": {"command": "clippy"}}})
|
||||
);
|
||||
let python = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == "python")
|
||||
.unwrap();
|
||||
assert_eq!(python.command, ["custom pylsp", "--stdio"]);
|
||||
assert_eq!(python.extensions, ["py"]);
|
||||
assert_eq!(python.initialization_options, serde_json::json!({}));
|
||||
assert_eq!(python.settings, serde_json::json!({}));
|
||||
let custom = config
|
||||
.lsp
|
||||
.iter()
|
||||
.find(|server| server.language_id == "my-language.c++")
|
||||
.unwrap();
|
||||
assert_eq!(custom.command, ["custom server", "--flag", ""]);
|
||||
assert_eq!(custom.extensions, ["custom", "لغة"]);
|
||||
assert_eq!(custom.initialization_options, serde_json::json!({}));
|
||||
assert_eq!(
|
||||
custom.settings,
|
||||
serde_json::json!({"nested": {"enabled": true, "limit": 3, "unset": null}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_configuration_rejects_invalid_keys_values_and_json_options() {
|
||||
let fixture = Fixture::new();
|
||||
for assignment in [
|
||||
r#"config.lsp[:rust] = true"#,
|
||||
r#"config.lsp[7] = false"#,
|
||||
r#"config.lsp[""] = false"#,
|
||||
r#"config.lsp["bad\nkey"] = false"#,
|
||||
r#"config.lsp["\xFF".b] = false"#,
|
||||
r#"config.lsp["rust"] = nil"#,
|
||||
r#"config.lsp["rust"] = []"#,
|
||||
r#"config.lsp["rust"] = "yes""#,
|
||||
r#"config.lsp["custom"] = {}"#,
|
||||
r#"config.lsp["custom"] = {command: ["server"]}"#,
|
||||
r#"config.lsp["custom"] = {extensions: ["custom"]}"#,
|
||||
r#"config.lsp["rust"] = {command: "server"}"#,
|
||||
r#"config.lsp["rust"] = {command: []}"#,
|
||||
r#"config.lsp["rust"] = {command: [""]}"#,
|
||||
r#"config.lsp["rust"] = {command: ["server", 7]}"#,
|
||||
r#"config.lsp["rust"] = {command: ["server", "bad\0argument"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: "rs"}"#,
|
||||
r#"config.lsp["rust"] = {extensions: []}"#,
|
||||
r#"config.lsp["rust"] = {extensions: [""]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: [7]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: [".rs"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["path/rs"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["path\\rs"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["r s"]}"#,
|
||||
r#"config.lsp["rust"] = {extensions: ["r\u0001s"]}"#,
|
||||
r#"config.lsp["rust"] = {initialization_options: []}"#,
|
||||
r#"config.lsp["rust"] = {settings: nil}"#,
|
||||
r#"config.lsp["rust"] = {settings: {bad: Object.new}}"#,
|
||||
r#"config.lsp["rust"] = {settings: {bad: Float::NAN}}"#,
|
||||
r#"config.lsp["rust"] = {settings: {7 => true}}"#,
|
||||
r#"config.lsp["rust"] = {unknown: true}"#,
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"invalid.rb",
|
||||
&format!("Vamp.configure {{ |config| {assignment} }}"),
|
||||
);
|
||||
assert!(load_config(&path).is_err(), "{assignment}");
|
||||
}
|
||||
let path = fixture.file(
|
||||
"invalid.rb",
|
||||
r#"Vamp.configure { |config| config.lsp["unlisted"] = true }"#,
|
||||
);
|
||||
let error = load_config(&path).unwrap_err();
|
||||
assert!(
|
||||
error.contains("unlisted") && error.contains("preset"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_wire_configuration_is_validated_before_being_consumed() {
|
||||
let mut frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"clipboard_mode": "system"
|
||||
});
|
||||
assert!(matches!(
|
||||
serde_json::from_value::<Response>(frame.clone()).unwrap(),
|
||||
Response::Config { lsp, .. } if lsp.is_empty()
|
||||
));
|
||||
frame["lsp"] = serde_json::json!({"rust": true});
|
||||
assert!(matches!(
|
||||
serde_json::from_value::<Response>(frame.clone()).unwrap(),
|
||||
Response::Config { lsp, .. }
|
||||
if lsp.len() == 1 && lsp[0].command == ["rust-analyzer"]
|
||||
));
|
||||
for value in [
|
||||
serde_json::json!(null),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({"": false}),
|
||||
serde_json::json!({"bad\u{0085}key": false}),
|
||||
serde_json::json!({"rust": null}),
|
||||
serde_json::json!({"unknown": true}),
|
||||
serde_json::json!({"custom": {}}),
|
||||
serde_json::json!({"rust": {"command": ["tool", "bad\0argument"]}}),
|
||||
serde_json::json!({"rust": {"extensions": ["bad\nextension"]}}),
|
||||
serde_json::json!({"rust": {"initialization_options": []}}),
|
||||
serde_json::json!({"rust": {"settings": "not-an-object"}}),
|
||||
serde_json::json!({"rust": {"unexpected": true}}),
|
||||
] {
|
||||
frame["lsp"] = value;
|
||||
assert!(
|
||||
serde_json::from_value::<Response>(frame.clone()).is_err(),
|
||||
"{frame}"
|
||||
);
|
||||
}
|
||||
let languages: serde_json::Map<String, serde_json::Value> = (0..17)
|
||||
.map(|index| {
|
||||
(
|
||||
format!("custom-{index}"),
|
||||
serde_json::json!({"command": ["server"], "extensions": ["custom"]}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
frame["lsp"] = serde_json::Value::Object(languages);
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
|
||||
frame.as_object_mut().unwrap().remove("lsp");
|
||||
assert!(serde_json::from_value::<Response>(frame).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_hang_callback_failure_and_fresh_host_recovery() {
|
||||
let fixture = Fixture::new();
|
||||
|
||||
Reference in New Issue
Block a user