feat: redesign status chrome and floating prompts

This commit is contained in:
Valdemar Lorenzen
2026-10-12 12:21:31 +02:00
parent 8c6a050171
commit 52915ffacf
12 changed files with 1333 additions and 373 deletions
+145 -4
View File
@@ -91,7 +91,20 @@ loop do
'tokenModifiers' => ['custom', 'readonly', 'declaration', 'async', 'defaultLibrary']},
'range' => true, 'full' => mode.include?('range') ? false : {'delta' => true}}
end
result(initialize_id, {'capabilities' => capabilities})
response = {'capabilities' => capabilities}
response['serverInfo'] = {'name' => "Fixture λ #{File.basename(log, '.jsonl')}"}
case mode
when 'name-absent' then response.delete('serverInfo')
when 'name-empty' then response['serverInfo']['name'] = ''
when 'name-blank' then response['serverInfo']['name'] = ' '
when 'name-controls' then response['serverInfo']['name'] = "bad\e\nname"
when 'name-edge-controls' then response['serverInfo']['name'] = "\nFixture\n"
when 'name-type' then response['serverInfo']['name'] = 42
when 'initialize-error'
emit({'id' => initialize_id, 'error' => {'code' => -32603, 'message' => 'fixture initialization failed'}})
when 'initialize-invalid' then response['capabilities'] = nil
end
result(initialize_id, response) unless mode == 'initialize-error'
initialized_sent = true
end
if !held.empty? && File.exist?(release) && ['stale', 'hover-stale'].include?(mode)
@@ -300,7 +313,7 @@ impl Fixture {
fn open(&self, mode: &str) -> EditorSession {
let server = self.root.join("server.rb");
fs::write(&server, SERVER).unwrap();
let command = if mode == "spawn" {
let mut command = if mode == "spawn" {
json!([self.root.join("unavailable-language-server")])
} else {
json!([
@@ -314,6 +327,12 @@ impl Fixture {
self.alternate
])
};
if mode == "name-absent" {
command
.as_array_mut()
.unwrap()
.insert(0, json!("/usr/bin/env"));
}
let options = json!({
"extensions": if mode == "irrelevant" { vec!["myl"] } else { vec!["rs"] },
"command": command,
@@ -492,6 +511,106 @@ fn edit_target(session: &mut EditorSession, fixture: &Fixture) -> usize {
id
}
#[test]
fn lsp_ready_names_wait_for_initialization_and_follow_only_the_focused_buffer() {
let fixture = Fixture::new();
let mut session = fixture.open("controlled-gated");
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
until(&mut session, &fixture, |_| {
fixture.method_count("initialize") == 1
});
assert!(session.editor().lsp_names().is_empty());
let unserved = fixture.root.join("notes.txt");
fs::write(&unserved, "unserved\n").unwrap();
session.execute(&format!("edit {}", unserved.display()));
let unserved_id = session.editor().buffer_id();
fixture.release();
until(&mut session, &fixture, |_| {
fixture.method_count("textDocument/didOpen") == 1
});
session.poll();
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
session.execute("buffer 1");
assert_eq!(session.editor().lsp_names(), ["Fixture λ protocol"]);
assert_eq!(session.editor().lsp_label(), "Fixture λ protocol");
let revision = session.editor().text_revision();
let cursor = session.editor().cursor();
session.poll();
assert_eq!(session.editor().text_revision(), revision);
assert_eq!(session.editor().cursor(), cursor);
assert_eq!(session.editor().mode(), Mode::Normal);
session.execute(&format!("buffer {unserved_id}"));
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
session.execute("buffer 1");
assert_eq!(session.editor().lsp_names(), ["Fixture λ protocol"]);
fixture.control(0, 1, json!({"action": "die"}));
until(&mut session, &fixture, |session| {
session.editor().message().contains("server stopped")
});
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
}
#[test]
fn lsp_readiness_alone_requests_a_redraw_without_document_or_message_changes() {
let fixture = Fixture::new();
let mut session = fixture.open("controlled-gated");
until(&mut session, &fixture, |_| {
fixture.method_count("initialize") == 1
});
assert!(session.editor().lsp_names().is_empty());
let message = session.editor().message().to_owned();
let revision = session.editor().text_revision();
let cursor = session.editor().cursor();
fixture.release();
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let changed = session.poll();
if !session.editor().lsp_names().is_empty() {
assert!(changed, "readiness must redraw even without plugin effects");
break;
}
assert!(
Instant::now() < deadline,
"fixture server did not become ready"
);
thread::sleep(Duration::from_millis(2));
}
assert_eq!(session.editor().lsp_names(), ["Fixture λ protocol"]);
assert_eq!(session.editor().message(), message);
assert_eq!(session.editor().text_revision(), revision);
assert_eq!(session.editor().cursor(), cursor);
assert_eq!(session.editor().mode(), Mode::Normal);
assert_eq!(session.editor().diagnostics().count(), 0);
}
#[test]
fn lsp_ready_names_fall_back_to_executable_only_for_missing_or_invalid_server_names() {
for mode in [
"name-absent",
"name-empty",
"name-blank",
"name-controls",
"name-edge-controls",
"name-type",
] {
let fixture = Fixture::new();
let mut session = fixture.open(mode);
until(&mut session, &fixture, |session| {
!session.editor().lsp_names().is_empty()
});
let expected = if mode == "name-absent" { "env" } else { "ruby" };
assert_eq!(session.editor().lsp_names(), [expected], "{mode}");
assert_eq!(session.editor().lsp_label(), expected, "{mode}");
}
}
#[test]
fn lsp_framing_lifecycle_diagnostics_and_unicode_dirty_definition() {
let fixture = Fixture::new();
@@ -885,6 +1004,8 @@ fn lsp_server_failures_and_unsupported_results_leave_native_editor_available() {
"spawn",
"malformed",
"dead",
"initialize-error",
"initialize-invalid",
"unsupported",
"remote",
"bounds",
@@ -892,7 +1013,10 @@ fn lsp_server_failures_and_unsupported_results_leave_native_editor_available() {
] {
let fixture = Fixture::new();
let mut session = fixture.open(mode);
if matches!(mode, "spawn" | "malformed" | "dead") {
if matches!(
mode,
"spawn" | "malformed" | "dead" | "initialize-error" | "initialize-invalid"
) {
until(&mut session, &fixture, |session| {
session.editor().message().contains(if mode == "spawn" {
"failed to start"
@@ -901,6 +1025,8 @@ fn lsp_server_failures_and_unsupported_results_leave_native_editor_available() {
})
});
assert_eq!(session.editor().diagnostics().count(), 0);
assert!(session.editor().lsp_names().is_empty(), "{mode}");
assert_eq!(session.editor().lsp_label(), "", "{mode}");
} else {
until(&mut session, &fixture, |session| {
session.editor().diagnostics().count() == 2
@@ -1285,11 +1411,13 @@ fn publish(
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, |_| {
until(&mut session, &fixture, |session| {
fixture.worker_count(0, "initialize") == 1
&& fixture.worker_count(1, "textDocument/didOpen") == 1
&& session.editor().lsp_names() == ["Fixture λ protocol-1"]
});
assert_eq!(fixture.worker_count(0, "textDocument/didOpen"), 0);
assert_eq!(session.editor().lsp_label(), "Fixture λ protocol-1");
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);
@@ -1298,9 +1426,14 @@ fn lsp_python_stack_merges_in_assignment_order_and_routes_positions_only_to_ty()
fixture.release();
until(&mut session, &fixture, |session| {
session.editor().hover().is_some()
&& session.editor().lsp_names() == ["Fixture λ protocol", "Fixture λ protocol-1"]
});
assert_eq!(fixture.worker_count(0, "textDocument/hover"), 1);
assert_eq!(fixture.worker_count(1, "textDocument/hover"), 0);
assert_eq!(
session.editor().lsp_label(),
"Fixture λ protocol + Fixture λ protocol-1"
);
for worker in 0..2 {
let frames = fixture.worker_frames(worker);
let initialize = frames
@@ -1551,8 +1684,16 @@ fn lsp_python_stack_failures_retire_only_the_offender_in_active_and_parked_buffe
});
}
assert_eq!(diagnostic_messages(&session), ["healthy target"]);
let name = if survivor == 0 {
"Fixture λ protocol"
} else {
"Fixture λ protocol-1"
};
assert_eq!(session.editor().lsp_names(), [name]);
assert_eq!(session.editor().lsp_label(), name);
session.execute("buffer 1");
assert_eq!(diagnostic_messages(&session), ["healthy source"]);
assert_eq!(session.editor().lsp_names(), [name]);
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);