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
+667 -5
View File
@@ -69,8 +69,10 @@ initialized_sent = false
held = []
definition_count = 0
source_uri = uri(source)
documents = {}
control_id = nil
loop do
if initialize_id && !initialized_sent && (!['gated', 'hover-gated'].include?(mode) || File.exist?(release))
if initialize_id && !initialized_sent && (!['gated', 'hover-gated', 'controlled-gated'].include?(mode) || File.exist?(release))
sync = case mode
when 'numeric' then 1
when 'none' then {'openClose' => false, 'change' => 0, 'save' => false}
@@ -78,8 +80,8 @@ loop do
else {'openClose' => true, 'change' => 1, 'save' => {'includeText' => true}}
end
result(initialize_id, {'capabilities' => {'positionEncoding' => 'utf-16',
'textDocumentSync' => sync, 'definitionProvider' => mode == 'unsupported' ? false : {'workDoneProgress' => false},
'hoverProvider' => mode == 'hover-unsupported' ? false : {'workDoneProgress' => false}}})
'textDocumentSync' => sync, 'definitionProvider' => ['unsupported', 'controlled-unsupported'].include?(mode) ? false : {'workDoneProgress' => false},
'hoverProvider' => ['hover-unsupported', 'controlled-unsupported'].include?(mode) ? false : {'workDoneProgress' => false}}})
initialized_sent = true
end
if !held.empty? && File.exist?(release) && ['stale', 'hover-stale'].include?(mode)
@@ -89,6 +91,30 @@ loop do
held.clear
emit({'method' => 'window/showMessage', 'params' => {'type' => 3, 'message' => 'stale response complete'}})
end
control = release + '.control'
if File.exist?(control)
command = JSON.parse(File.read(control))
if command['id'] != control_id
control_id = command.fetch('id')
case command.fetch('action')
when 'publish', 'clear'
document = documents.fetch(command.fetch('uri', source_uri))
params = {'uri' => document['uri'], 'version' => command.fetch('version', document['version']),
'diagnostics' => command['action'] == 'clear' ? [] :
[{'range' => range(0, 2, 4), 'severity' => 2, 'message' => command.fetch('message')}]}
emit({'method' => 'textDocument/publishDiagnostics', 'params' => params})
when 'die'
exit! 7
when 'malformed'
STDOUT.write("Content-Length: nope\r\n\r\n")
else
raise 'unknown fixture action'
end
# The response proves the worker consumed every preceding publication.
emit({'id' => "control-#{control_id}", 'method' => 'workspace/configuration',
'params' => {'items' => []}})
end
end
next unless IO.select([STDIN], nil, nil, 0.01)
headers = {}
loop do
@@ -114,7 +140,8 @@ loop do
emit({'id' => 0, 'method' => '$/unsupported', 'params' => {}})
when 'textDocument/didOpen'
document = frame.fetch('params').fetch('textDocument')
if document['uri'] == source_uri
documents[document['uri']] = document
if document['uri'] == source_uri && !mode.start_with?('controlled')
if mode == 'malformed'
STDOUT.write("Content-Length: nope\r\n\r\n")
elsif mode == 'dead'
@@ -125,7 +152,8 @@ loop do
end
when 'textDocument/didChange'
document = frame.fetch('params').fetch('textDocument')
if document['uri'] == source_uri && !['malformed', 'dead'].include?(mode)
documents[document['uri']] = document
if document['uri'] == source_uri && !['malformed', 'dead'].include?(mode) && !mode.start_with?('controlled')
diagnostics(document, 'obsolete must drop', old: true)
diagnostics(document, "revision #{document['version']}")
end
@@ -258,6 +286,103 @@ impl Fixture {
.unwrap()
}
fn worker_log(&self, worker: usize) -> PathBuf {
if worker == 0 {
self.log.clone()
} else {
self.root.join(format!("protocol-{worker}.jsonl"))
}
}
fn worker_release(&self, worker: usize) -> PathBuf {
if worker == 0 {
self.release.clone()
} else {
self.root.join(format!("release-{worker}"))
}
}
fn worker_frames(&self, worker: usize) -> Vec<Value> {
fs::read_to_string(self.worker_log(worker))
.unwrap_or_default()
.lines()
.filter_map(|line| serde_json::from_str(line).ok())
.collect()
}
fn worker_count(&self, worker: usize, method: &str) -> usize {
self.worker_frames(worker)
.iter()
.filter(|frame| frame["method"] == method)
.count()
}
fn open_stack(&self, modes: [&str; 2], overlap: bool) -> EditorSession {
self.open_stack_at(modes, overlap, &self.source)
}
fn open_stack_at(
&self,
modes: [&str; 2],
overlap: bool,
path: &std::path::Path,
) -> EditorSession {
let server = self.root.join("server.rb");
fs::write(&server, SERVER).unwrap();
let command = |worker: usize, mode: &str| {
if mode == "spawn" {
json!([self.root.join("unavailable-language-server")])
} else {
json!([
"ruby",
server,
mode,
self.worker_log(worker),
self.worker_release(worker),
self.source,
self.target,
self.alternate
])
}
};
let servers: Vec<_> = modes
.into_iter()
.enumerate()
.map(|(worker, mode)| json!({"command": command(worker, mode)}))
.collect();
let mut languages = json!({"python": {"extensions": ["rs"], "servers": servers}});
if overlap {
languages["zzz-other"] =
json!({"extensions": ["rs"], "command": command(2, "controlled")});
}
let config = format!(
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n JSON.parse({}).each {{ |language, options| config.lsp[language] = options }}\n config.bind 'K', config.core.lsp.hover\nend\n",
serde_json::to_string(&languages.to_string()).unwrap(),
);
let config_path = self.root.join("config.rb");
fs::write(&config_path, config).unwrap();
EditorSession::open(
Some(path.to_path_buf()),
vec!["text".into(), "diagnostics".into()],
Some(&config_path),
)
.unwrap()
}
fn control(&self, worker: usize, id: u64, mut command: Value) {
command["id"] = json!(id);
let path = self.worker_release(worker).with_extension("control");
let temporary = path.with_extension("pending");
fs::write(&temporary, command.to_string()).unwrap();
fs::rename(temporary, path).unwrap();
}
fn acknowledged(&self, worker: usize, id: u64) -> bool {
self.worker_frames(worker)
.iter()
.any(|frame| frame["id"] == format!("control-{id}") && frame.get("result").is_some())
}
fn frames(&self) -> Vec<Value> {
fs::read_to_string(&self.log)
.unwrap_or_default()
@@ -959,3 +1084,540 @@ fn lsp_hover_absent_unsupported_and_invalid_results_preserve_native_editing() {
assert_eq!(fixture.method_count("textDocument/definition"), 0);
}
}
fn diagnostic_messages(session: &EditorSession) -> Vec<&str> {
session
.editor()
.diagnostics()
.map(|diagnostic| diagnostic.message.as_str())
.collect()
}
fn wait_protocol(fixture: &Fixture, predicate: impl Fn() -> bool) {
let deadline = Instant::now() + Duration::from_secs(5);
while !predicate() {
assert!(
Instant::now() < deadline,
"fixture protocol gate did not complete: {:?}",
fixture.frames()
);
thread::sleep(Duration::from_millis(2));
}
// Deliberately no session.poll(): outputs may be queued across a rename/close.
}
fn publish(
session: &mut EditorSession,
fixture: &Fixture,
worker: usize,
id: u64,
command: Value,
expected: &[&str],
) {
fixture.control(worker, id, command);
until(session, fixture, |session| {
fixture.acknowledged(worker, id) && diagnostic_messages(session) == expected
});
}
#[test]
fn lsp_python_stack_merges_in_assignment_order_and_routes_positions_only_to_ty() {
let fixture = Fixture::new();
let mut session = fixture.open_stack(["controlled-gated", "controlled"], true);
until(&mut session, &fixture, |_| {
fixture.worker_count(0, "initialize") == 1
&& fixture.worker_count(1, "textDocument/didOpen") == 1
});
assert_eq!(fixture.worker_count(0, "textDocument/didOpen"), 0);
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);
assert_eq!(session.editor().lines(), &["A😀e\u{301}Z!", ""]);
session.execute("lsp.hover");
fixture.release();
until(&mut session, &fixture, |session| {
session.editor().hover().is_some()
});
assert_eq!(fixture.worker_count(0, "textDocument/hover"), 1);
assert_eq!(fixture.worker_count(1, "textDocument/hover"), 0);
for worker in 0..2 {
let frames = fixture.worker_frames(worker);
let initialize = frames
.iter()
.find(|frame| frame["method"] == "initialize")
.unwrap();
assert!(
initialize["params"]["capabilities"]["textDocument"]
.get("diagnostic")
.is_none()
);
let open = frames
.iter()
.find(|frame| frame["method"] == "textDocument/didOpen")
.unwrap();
assert_eq!(open["params"]["textDocument"]["languageId"], "python");
}
publish(
&mut session,
&fixture,
1,
1,
json!({"action": "publish", "message": "ruff"}),
&["ruff"],
);
publish(
&mut session,
&fixture,
0,
1,
json!({"action": "publish", "message": "ty"}),
&["ty", "ruff"],
);
publish(
&mut session,
&fixture,
0,
2,
json!({"action": "publish", "message": "ty replaced"}),
&["ty replaced", "ruff"],
);
publish(
&mut session,
&fixture,
1,
2,
json!({"action": "clear"}),
&["ty replaced"],
);
publish(
&mut session,
&fixture,
1,
3,
json!({"action": "publish", "message": "duplicate"}),
&["ty replaced", "duplicate"],
);
publish(
&mut session,
&fixture,
0,
3,
json!({"action": "publish", "message": "duplicate"}),
&["duplicate", "duplicate"],
);
publish(
&mut session,
&fixture,
0,
4,
json!({"action": "clear"}),
&["duplicate"],
);
publish(
&mut session,
&fixture,
1,
4,
json!({"action": "clear"}),
&[],
);
publish(
&mut session,
&fixture,
0,
5,
json!({"action": "publish", "message": "ty first"}),
&["ty first"],
);
publish(
&mut session,
&fixture,
1,
5,
json!({"action": "publish", "message": "ruff second"}),
&["ty first", "ruff second"],
);
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().lines(), &["😀e\u{301}disk", ""]);
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 1 });
assert_eq!(fixture.worker_count(0, "textDocument/definition"), 1);
assert_eq!(fixture.worker_count(1, "textDocument/definition"), 0);
session.execute("buffer 1");
assert_eq!(diagnostic_messages(&session), ["ty first", "ruff second"]);
session.handle(Input::Char('A'));
session.handle(Input::Char('?'));
session.handle(Input::Escape);
assert!(diagnostic_messages(&session).is_empty());
until(&mut session, &fixture, |_| {
(0..2).all(|worker| {
fixture.worker_frames(worker).iter().any(|frame| {
frame["method"] == "textDocument/didChange"
&& frame["params"]["contentChanges"][0]["text"] == "A😀e\u{301}Z!?\n"
})
})
});
publish(
&mut session,
&fixture,
1,
6,
json!({"action": "publish", "version": 1, "message": "obsolete"}),
&[],
);
publish(
&mut session,
&fixture,
1,
7,
json!({"action": "publish", "message": "new lint"}),
&["new lint"],
);
publish(
&mut session,
&fixture,
0,
6,
json!({"action": "clear"}),
&["new lint"],
);
session.execute("w");
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didSave") == 1)
});
session.execute("bdelete 1");
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didClose") == 1)
});
assert_eq!(session.editor().buffer_id(), target_id);
assert!(
!fixture.worker_log(2).exists(),
"overlapping losing language must never start"
);
drop(session);
for worker in 0..2 {
assert_eq!(fixture.worker_count(worker, "shutdown"), 1);
assert_eq!(fixture.worker_count(worker, "exit"), 1);
}
}
#[test]
fn lsp_python_stack_failures_retire_only_the_offender_in_active_and_parked_buffers() {
for failing in 0..2 {
for failure in ["spawn", "malformed", "die"] {
let fixture = Fixture::new();
fs::write(&fixture.target, "A😀e\u{301}Z\n").unwrap();
let mut modes = ["controlled"; 2];
if failure == "spawn" {
modes[failing] = "spawn";
}
let mut session = fixture.open_stack(modes, true);
let survivor = 1 - failing;
until(&mut session, &fixture, |session| {
fixture.worker_count(survivor, "textDocument/didOpen") == 1
&& if failure == "spawn" {
session.editor().message().contains("failed to start")
} else {
fixture.worker_count(failing, "textDocument/didOpen") == 1
}
});
publish(
&mut session,
&fixture,
survivor,
1,
json!({"action": "publish", "message": "healthy source"}),
&["healthy source"],
);
if failure != "spawn" {
let expected = if failing == 0 {
["failed source", "healthy source"]
} else {
["healthy source", "failed source"]
};
publish(
&mut session,
&fixture,
failing,
1,
json!({"action": "publish", "message": "failed source"}),
&expected,
);
}
session.execute(&format!("edit {}", fixture.target.display()));
until(&mut session, &fixture, |_| {
fixture.worker_count(survivor, "textDocument/didOpen") == 2
&& (failure == "spawn"
|| fixture.worker_count(failing, "textDocument/didOpen") == 2)
});
let target_uri = fixture
.worker_frames(survivor)
.into_iter()
.filter(|frame| frame["method"] == "textDocument/didOpen")
.nth(1)
.unwrap()["params"]["textDocument"]["uri"]
.clone();
publish(
&mut session,
&fixture,
survivor,
2,
json!({"action": "publish", "uri": target_uri, "message": "healthy target"}),
&["healthy target"],
);
if failure != "spawn" {
let expected = if failing == 0 {
["failed target", "healthy target"]
} else {
["healthy target", "failed target"]
};
publish(
&mut session,
&fixture,
failing,
2,
json!({"action": "publish", "uri": target_uri, "message": "failed target"}),
&expected,
);
fixture.control(failing, 3, json!({"action": failure}));
until(&mut session, &fixture, |session| {
session.editor().message().contains("server stopped")
&& diagnostic_messages(session) == ["healthy target"]
});
}
assert_eq!(diagnostic_messages(&session), ["healthy target"]);
session.execute("buffer 1");
assert_eq!(diagnostic_messages(&session), ["healthy source"]);
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);
assert_eq!(session.editor().lines(), &["A😀e\u{301}Z!", ""]);
assert!(diagnostic_messages(&session).is_empty());
until(&mut session, &fixture, |_| {
fixture.worker_count(survivor, "textDocument/didChange") == 1
});
publish(
&mut session,
&fixture,
survivor,
3,
json!({"action": "publish", "version": 1, "message": "stale healthy"}),
&[],
);
publish(
&mut session,
&fixture,
survivor,
4,
json!({"action": "publish", "message": "healthy edit"}),
&["healthy edit"],
);
if failing == 0 {
session.execute("lsp.definition");
until(&mut session, &fixture, |session| {
session.editor().message().contains("no running server")
});
assert_eq!(fixture.worker_count(1, "textDocument/definition"), 0);
} else {
session.execute("lsp.hover");
until(&mut session, &fixture, |session| {
session.editor().hover().is_some()
});
}
assert!(!fixture.worker_log(2).exists());
}
}
}
#[test]
fn lsp_python_stack_discards_queued_same_revision_diagnostics_after_rename_and_close() {
let fixture = Fixture::new();
let mut session = fixture.open_stack(["controlled", "controlled"], false);
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didOpen") == 1)
});
publish(
&mut session,
&fixture,
0,
1,
json!({"action": "publish", "message": "before rename"}),
&["before rename"],
);
let revision = session.editor().context().revision;
for worker in 0..2 {
fixture.control(
worker,
2,
json!({"action": "publish", "message": "queued old path"}),
);
}
wait_protocol(&fixture, || {
(0..2).all(|worker| fixture.acknowledged(worker, 2))
});
let renamed = fixture.root.join("renamed.rs");
session.execute(&format!("w {}", renamed.display()));
assert_eq!(session.editor().context().revision, revision);
until(&mut session, &fixture, |_| {
(0..2).all(|worker| {
fixture.worker_count(worker, "textDocument/didClose") == 1
&& fixture.worker_count(worker, "textDocument/didOpen") == 2
})
});
assert!(diagnostic_messages(&session).is_empty());
let old_uri = fixture
.worker_frames(0)
.into_iter()
.find(|frame| frame["method"] == "textDocument/didOpen")
.unwrap()["params"]["textDocument"]["uri"]
.clone();
let new_uri = fixture
.worker_frames(0)
.into_iter()
.filter(|frame| frame["method"] == "textDocument/didOpen")
.nth(1)
.unwrap()["params"]["textDocument"]["uri"]
.clone();
publish(
&mut session,
&fixture,
0,
3,
json!({"action": "publish", "uri": old_uri, "message": "closed old path"}),
&[],
);
publish(
&mut session,
&fixture,
1,
3,
json!({"action": "publish", "uri": new_uri, "message": "new path"}),
&["new path"],
);
fixture.control(
1,
4,
json!({"action": "publish", "uri": new_uri, "message": "queued closed buffer"}),
);
wait_protocol(&fixture, || fixture.acknowledged(1, 4));
session.execute("bdelete 1");
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didClose") == 2)
});
assert!(!session.editor().has_buffer(1));
assert!(diagnostic_messages(&session).is_empty());
}
#[test]
fn lsp_python_stack_aggregate_message_limit_preserves_the_healthy_contribution() {
let fixture = Fixture::new();
let mut session = fixture.open_stack(["controlled", "controlled"], false);
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didOpen") == 1)
});
let large = "x".repeat(2 * 1024 * 1024 + 1);
fixture.control(0, 1, json!({"action": "publish", "message": large}));
until(&mut session, &fixture, |session| {
fixture.acknowledged(0, 1) && session.editor().diagnostics().count() == 1
});
assert_eq!(
session.editor().diagnostics().next().unwrap().message.len(),
large.len()
);
fixture.control(1, 1, json!({"action": "publish", "message": large}));
until(&mut session, &fixture, |session| {
session
.editor()
.message()
.contains("combined diagnostic messages exceed 4 MiB")
});
assert_eq!(session.editor().diagnostics().count(), 1);
assert_eq!(
session.editor().diagnostics().next().unwrap().message.len(),
large.len()
);
publish(
&mut session,
&fixture,
0,
2,
json!({"action": "publish", "message": "healthy replacement"}),
&["healthy replacement"],
);
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);
assert_eq!(session.editor().lines(), &["A😀e\u{301}Z!", ""]);
}
#[test]
fn lsp_python_stack_seeds_dirty_unserved_documents_before_lazy_startup() {
let mut fixture = Fixture::new();
fixture.target = fixture.root.join("target.txt");
fs::write(&fixture.target, "disk\n").unwrap();
let mut session = fixture.open_stack_at(["controlled", "controlled"], false, &fixture.target);
session.execute("lsp.diagnostics");
until(&mut session, &fixture, |session| {
session.editor().message().contains("no running server")
});
assert!(!fixture.worker_log(0).exists());
assert!(!fixture.worker_log(1).exists());
session.handle(Input::Char('i'));
session.handle(Input::Paste("😀e\u{301}".into()));
session.handle(Input::Escape);
session.execute(&format!("edit {}", fixture.source.display()));
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didOpen") == 1)
});
session.execute("lsp.definition");
until(&mut session, &fixture, |session| {
session.editor().buffer_id() == 1
});
assert_eq!(session.editor().lines(), &["😀e\u{301}disk", ""]);
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 1 });
assert!(session.editor().dirty());
assert_eq!(fs::read_to_string(&fixture.target).unwrap(), "disk\n");
assert_eq!(fixture.worker_count(0, "textDocument/definition"), 1);
assert_eq!(fixture.worker_count(1, "textDocument/definition"), 0);
for worker in 0..2 {
assert_eq!(
fixture.worker_count(worker, "textDocument/didOpen"),
1,
"the unserved target is cached, not opened on the wire"
);
}
}
#[test]
fn lsp_python_stack_does_not_fall_back_to_ruff_when_ty_lacks_position_capabilities() {
let fixture = Fixture::new();
let mut session = fixture.open_stack(["controlled-unsupported", "controlled"], false);
until(&mut session, &fixture, |_| {
(0..2).all(|worker| fixture.worker_count(worker, "textDocument/didOpen") == 1)
});
publish(
&mut session,
&fixture,
1,
1,
json!({"action": "publish", "message": "lint remains available"}),
&["lint remains available"],
);
for command in ["lsp.definition", "lsp.hover"] {
let previous_message = session.editor().message().to_owned();
session.execute(command);
until(&mut session, &fixture, |session| {
session.editor().message() != previous_message
});
assert_eq!(session.editor().buffer_id(), 1);
assert_eq!(diagnostic_messages(&session), ["lint remains available"]);
}
for worker in 0..2 {
assert_eq!(fixture.worker_count(worker, "textDocument/definition"), 0);
assert_eq!(fixture.worker_count(worker, "textDocument/hover"), 0);
}
}