feat: preconfigure Python with Astral ty and Ruff

This commit is contained in:
2026-10-09 21:11:07 +02:00
parent 2c3c73e5b9
commit 548711980c
9 changed files with 1251 additions and 127 deletions
+226 -5
View File
@@ -981,9 +981,9 @@ end"#,
end"#,
);
let config = load_config(&path).unwrap();
assert_eq!(config.lsp.len(), 4);
for (language, command, extension) in [
("rust", &["rust-analyzer"][..], "rs"),
("python", &["pylsp"][..], "py"),
("ruby", &["solargraph", "stdio"][..], "rb"),
] {
let server = config
@@ -996,6 +996,25 @@ end"#,
assert_eq!(server.initialization_options, serde_json::json!({}));
assert_eq!(server.settings, serde_json::json!({}));
}
let python: Vec<_> = config
.lsp
.iter()
.filter(|server| server.language_id == "python")
.collect();
assert_eq!(python.len(), 2);
for (server, command, initialization_options) in [
(
python[0],
["ty", "server"],
serde_json::json!({"experimental": {"useUv": "off"}}),
),
(python[1], ["ruff", "server"], serde_json::json!({})),
] {
assert_eq!(server.command, command);
assert_eq!(server.extensions, ["py"]);
assert_eq!(server.initialization_options, initialization_options);
assert_eq!(server.settings, serde_json::json!({}));
}
let path = fixture.file(
"config.rb",
@@ -1005,7 +1024,7 @@ end"#,
initialization_options: {cargo: {features: ["α"]}},
settings: {"rust-analyzer" => {check: {command: "clippy"}}}
}
config.lsp["python"] = {command: ["custom pylsp", "--stdio"]}
config.lsp["python"] = {command: ["custom-python-server", "--stdio"]}
config.lsp["ruby"] = {command: ["bundle", "exec", "solargraph", "stdio"]}
config.lsp["my-language.c++"] = {
command: ["custom server", "--flag", ""],
@@ -1035,10 +1054,18 @@ end"#,
.iter()
.find(|server| server.language_id == "python")
.unwrap();
assert_eq!(python.command, ["custom pylsp", "--stdio"]);
assert_eq!(python.command, ["custom-python-server", "--stdio"]);
assert_eq!(python.extensions, ["py"]);
assert_eq!(python.initialization_options, serde_json::json!({}));
assert_eq!(python.settings, serde_json::json!({}));
assert_eq!(
config
.lsp
.iter()
.filter(|server| server.language_id == "python")
.count(),
1
);
let ruby = config
.lsp
.iter()
@@ -1060,6 +1087,182 @@ end"#,
);
}
#[test]
fn lsp_python_common_options_and_ordered_server_overrides() {
let fixture = Fixture::new();
let path = fixture.file(
"config.rb",
r#"Vamp.configure do |config|
config.lsp["python"] = {
extensions: ["py", "pyi"],
initialization_options: {diagnosticMode: "workspace"},
settings: {shared: true}
}
end"#,
);
let config = load_config(&path).unwrap();
assert_eq!(config.lsp.len(), 2);
for (server, command) in config
.lsp
.iter()
.zip([["ty", "server"], ["ruff", "server"]])
{
assert_eq!(server.language_id, "python");
assert_eq!(server.command, command);
assert_eq!(server.extensions, ["py", "pyi"]);
assert_eq!(
server.initialization_options,
serde_json::json!({"diagnosticMode": "workspace"})
);
assert_eq!(server.settings, serde_json::json!({"shared": true}));
}
let path = fixture.file(
"config.rb",
r#"Vamp.configure do |config|
config.lsp["python"] = {servers: [
{command: ["type-server", "--stdio"], initialization_options: {experimental: {useUv: "off"}}, settings: {first: true}},
{command: ["lint-server"], initialization_options: {settings: {lint: {enable: true}}}}
]}
config.lsp["custom"] = {extensions: ["custom"], servers: [
{command: ["custom-first"]}, {command: ["custom-second"], settings: {second: true}}
]}
end"#,
);
let config = load_config(&path).unwrap();
for (language, commands, extensions) in [
(
"python",
[&["type-server", "--stdio"][..], &["lint-server"][..]],
["py"],
),
(
"custom",
[&["custom-first"][..], &["custom-second"][..]],
["custom"],
),
] {
let servers: Vec<_> = config
.lsp
.iter()
.filter(|server| server.language_id == language)
.collect();
assert_eq!(servers.len(), 2);
for (server, command) in servers.iter().zip(commands) {
assert_eq!(server.command, command);
assert_eq!(server.extensions, extensions);
}
}
let python: Vec<_> = config
.lsp
.iter()
.filter(|server| server.language_id == "python")
.collect();
assert_eq!(
python[0].initialization_options,
serde_json::json!({"experimental": {"useUv": "off"}})
);
assert_eq!(python[0].settings, serde_json::json!({"first": true}));
assert_eq!(
python[1].initialization_options,
serde_json::json!({"settings": {"lint": {"enable": true}}})
);
assert_eq!(python[1].settings, serde_json::json!({}));
let custom: Vec<_> = config
.lsp
.iter()
.filter(|server| server.language_id == "custom")
.collect();
assert_eq!(custom[0].initialization_options, serde_json::json!({}));
assert_eq!(custom[1].settings, serde_json::json!({"second": true}));
let path = fixture.file(
"config.rb",
r#"Vamp.configure { |config| config.lsp["python"] = {settings: {shared: true}} }"#,
);
let config = load_config(&path).unwrap();
assert_eq!(
config.lsp[0].initialization_options,
serde_json::json!({"experimental": {"useUv": "off"}})
);
assert_eq!(config.lsp[1].initialization_options, serde_json::json!({}));
}
#[test]
fn lsp_server_lists_reject_invalid_shapes_and_ambiguous_options_locally() {
let fixture = Fixture::new();
for (options, error) in [
("{servers: nil}", "nonempty array"),
("{servers: {command: ['server']}}", "nonempty array"),
("{servers: []}", "nonempty array"),
(
"{servers: [true]}",
"servers[0] must be a configuration object",
),
("{servers: [{}]}", "servers[0] requires command argv"),
("{servers: [{command: []}]}", "servers[0] command must be"),
(
"{servers: [{command: ['server'], extensions: ['py']}]}",
"servers[0] has unknown option",
),
(
"{servers: [{command: ['server'], servers: []}]}",
"servers[0] has unknown option",
),
(
"{servers: [{command: ['server'], initialization_options: []}]}",
"servers[0] initialization_options must be",
),
(
"{servers: [{command: ['server']}, {command: ['other'], settings: nil}]}",
"servers[1] settings must be",
),
(
"{servers: [{command: ['server']}], command: ['other']}",
"cannot be combined",
),
(
"{servers: [{command: ['server']}], initialization_options: {}}",
"cannot be combined",
),
(
"{servers: [{command: ['server']}], settings: {}}",
"cannot be combined",
),
] {
let path = fixture.file(
"config.rb",
&format!("Vamp.configure {{ |config| config.lsp[\"python\"] = {options} }}"),
);
let actual = load_config(&path).unwrap_err();
assert!(
actual.starts_with("Ruby config: LSP language \"python\""),
"{actual}"
);
assert!(actual.contains(error), "{options}: {actual}");
}
}
#[test]
fn lsp_server_limit_is_enforced_at_configuration_startup() {
let fixture = Fixture::new();
for count in [16, 17] {
let path = fixture.file(
"config.rb",
&format!(
"Vamp.configure {{ |config| config.lsp[\"python\"] = {{servers: Array.new({count}) {{ {{command: [\"server\"]}} }} }} }}"
),
);
let result = load_config(&path);
if count == 16 {
assert_eq!(result.unwrap().lsp.len(), 16);
} else {
assert_eq!(
result.unwrap_err(),
"Ruby config: LSP configuration supports at most 16 configured servers"
);
}
}
}
#[test]
fn lsp_configuration_rejects_invalid_keys_values_and_json_options() {
let fixture = Fixture::new();
@@ -1074,6 +1277,7 @@ end"#,
r#"config.lsp["rust"] = "yes""#,
r#"config.lsp["custom"] = {}"#,
r#"config.lsp["custom"] = {command: ["server"]}"#,
r#"config.lsp["custom"] = {servers: [{command: ["server"]}]}"#,
r#"config.lsp["custom"] = {extensions: ["custom"]}"#,
r#"config.lsp["rust"] = {command: "server"}"#,
r#"config.lsp["rust"] = {command: []}"#,
@@ -1146,6 +1350,7 @@ end"#,
serde_json::json!({"": false}),
serde_json::json!({"bad\u{0085}key": false}),
serde_json::json!({"rust": null}),
serde_json::json!({"python": []}),
serde_json::json!({"unknown": true}),
serde_json::json!({"custom": {}}),
serde_json::json!({"rust": {"command": ["tool", "bad\0argument"]}}),
@@ -1153,6 +1358,11 @@ end"#,
serde_json::json!({"rust": {"initialization_options": []}}),
serde_json::json!({"rust": {"settings": "not-an-object"}}),
serde_json::json!({"rust": {"unexpected": true}}),
serde_json::json!({"python": {"servers": []}}),
serde_json::json!({"python": {"servers": [null]}}),
serde_json::json!({"python": {"servers": [{}]}}),
serde_json::json!({"python": {"servers": [{"command": ["server"], "extensions": ["py"]}]}}),
serde_json::json!({"python": {"servers": [{"command": ["server"]}], "settings": {}}}),
] {
frame["lsp"] = value;
let Response::Config { lsp, .. } =
@@ -1162,7 +1372,7 @@ end"#,
};
assert!(crate::lsp_config::resolve(lsp).is_err(), "{frame}");
}
let languages: serde_json::Map<String, serde_json::Value> = (0..17)
let mut languages: serde_json::Map<String, serde_json::Value> = (0..14)
.map(|index| {
(
format!("custom-{index}"),
@@ -1170,13 +1380,24 @@ end"#,
)
})
.collect();
languages.insert("python".into(), serde_json::json!(true));
frame["lsp"] = serde_json::Value::Object(languages);
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert!(crate::lsp_config::resolve(lsp).is_err());
assert_eq!(crate::lsp_config::resolve(lsp).unwrap().len(), 16);
frame["lsp"]["ruby"] = serde_json::json!(true);
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert_eq!(
crate::lsp_config::resolve(lsp).unwrap_err(),
"LSP configuration supports at most 16 configured servers"
);
frame.as_object_mut().unwrap().remove("lsp");
assert!(serde_json::from_value::<Response>(frame).is_err());
}