fix(tasks): show a progress title's executable path as the program name (#1061)

rust-analyzer titles its check with the full command line, starting with the absolute path of cargo, so the status bar and Background Tasks popup showed /Users/…/Application Support/…/bin/cargo check … truncated before the useful part. LSP $/progress and DAP progressStart titles now go through ide_core::task::display_title, which turns a leading absolute executable path (with spaces, Windows drive/UNC, .exe/.cmd/.bat) into the program name.

Fixes #1060
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-04 02:09:13 +08:00
1 джерело 8efa7f8ece
коміт 2f75628c87
4 files changed
+83 -2

No files matched your search

+57
Переглянути файл
@@ -44,6 +44,39 @@ pub fn cancel(id: TaskId) -> bool {
}
}
/// A progress title a tool reported, made readable: a leading absolute executable path
/// (rust-analyzer reports `/…/bin/cargo check`) shows as its program name (`cargo check`).
pub fn display_title(title: &str) -> String {
let is_sep = |c: char| c == '/' || c == '\\';
let b = title.as_bytes();
let absolute = title.starts_with(is_sep)
|| (b.len() > 2 && b[0].is_ascii_alphabetic() && b[1] == b':' && is_sep(b[2] as char));
if !absolute {
return title.to_owned();
}
// The path runs on through spaces (`Application Support/…`) while the next word still
// continues it; the first word without a separator, an option or a new path ends it.
let mut end = title.find(' ').unwrap_or(title.len());
while end < title.len() {
let rest = &title[end + 1..];
let word = rest.split(' ').next().unwrap_or_default();
if word.is_empty() || !word.contains(is_sep) || word.starts_with(is_sep) || word.starts_with('-') {
break;
}
end += 1 + word.len();
}
let (path, rest) = title.split_at(end);
let name = path.rsplit(is_sep).next().unwrap_or(path);
let name = [".exe", ".cmd", ".bat"]
.iter()
.find_map(|ext| {
let cut = name.len().checked_sub(ext.len())?;
(name.is_char_boundary(cut) && name[cut..].eq_ignore_ascii_case(ext)).then(|| &name[..cut])
})
.unwrap_or(name);
if name.is_empty() { title.to_owned() } else { format!("{name}{rest}") }
}
#[cfg(test)]
mod tests {
use super::*;
@@ -71,4 +104,28 @@ mod tests {
assert!(!cancel(other), "an ended task is not cancelled");
assert_eq!(hits.load(Ordering::SeqCst), 1);
}
#[test]
fn display_title_shows_a_leading_executable_path_as_its_name() {
for (title, shown) in [
("/Users/thomas/Library/Application Support/mbx/bin/cargo check", "cargo check"),
("/home/me/.cargo/bin/cargo clippy (#2)", "cargo clippy (#2)"),
(
"/opt/my tools/bin/cargo check --manifest-path /x/Cargo.toml",
"cargo check --manifest-path /x/Cargo.toml",
),
(r"C:\Users\me\.cargo\bin\cargo.exe check", "cargo check"),
(r"C:\Program Files\Rust\bin\cargo.EXE check", "cargo check"),
(r"\\server\share\tools\x.cmd build", "x build"),
("/usr/bin/make", "make"),
("cargo check", "cargo check"),
("Indexing", "Indexing"),
("Building compile-time-deps", "Building compile-time-deps"),
("Loading /src/app (2/3)", "Loading /src/app (2/3)"),
("/", "/"),
("", ""),
] {
assert_eq!(display_title(title), shown, "{title:?}");
}
}
}
+1 -1
Переглянути файл
@@ -405,7 +405,7 @@ async fn on_event(mgr: &Arc<DebugManager>, s: &Arc<Session>, event: &str, mut bo
let mut progress = s.progress.lock();
let id =
if done { progress.remove(&pid) } else { Some(*progress.entry(pid).or_insert_with(TaskId::next)) };
(id, str_("title").unwrap_or_else(|| s.name.clone()))
(id, str_("title").map(|t| ide_core::task::display_title(&t)).unwrap_or_else(|| s.name.clone()))
};
let Some(id) = id else { return };
s.owner.sink.emit(IdeEvent::Task {
+1 -1
Переглянути файл
@@ -59,7 +59,7 @@ impl ClientHandler {
.get("title")
.and_then(Value::as_str)
.filter(|t| !t.is_empty())
.map(str::to_owned)
.map(ide_core::task::display_title)
.unwrap_or_else(|| self.server.name.clone());
let task = TaskId::next();
let cancel = (cancellable == Some(true)).then(|| self.cancel_handle(task, &params["token"]));
+24
Переглянути файл
@@ -788,6 +788,30 @@ async fn progress_tasks_and_busy_status() {
assert!(tasks[2].3);
}
#[tokio::test(flavor = "multi_thread")]
async fn progress_titled_with_an_executable_path_shows_the_program_name() {
let h = harness(&[("mock", incremental_caps())]);
h.open("a.mk", "x");
h.wait_status("mock", ServerStatus::Ready).await;
let peer = h.launcher.state("mock").peer();
let title = "/Users/thomas/Library/Application Support/mbx/bin/cargo check";
peer.notify("$/progress", json!({ "token": "fc", "value": { "kind": "begin", "title": title } })).unwrap();
peer.notify("$/progress", json!({ "token": "fc", "value": { "kind": "report", "message": "tauri_plugin (lib)" } }))
.unwrap();
peer.notify("$/progress", json!({ "token": "fc", "value": { "kind": "end" } })).unwrap();
eventually(|| h.events.lock().iter().any(|e| matches!(e, IdeEvent::Task { done: true, .. }))).await;
let titles: Vec<String> = h
.events
.lock()
.iter()
.filter_map(|e| match e {
IdeEvent::Task { title, .. } => Some(title.clone()),
_ => None,
})
.collect();
assert_eq!(titles, ["cargo check"; 3]);
}
#[tokio::test(flavor = "multi_thread")]
async fn cancelling_a_cancellable_progress_task_asks_the_server_to_stop() {
let h = harness(&[("mock", incremental_caps())]);