Show task summary in its terminal after it stops running (#10615)

Based on https://github.com/alacritty/alacritty/issues/7795

Unknown error code commands (now includes the interrupted ones):

![image](https://github.com/zed-industries/zed/assets/2690773/801868bc-081c-453c-a353-233d4397bda9)

Successful command:

![image](https://github.com/zed-industries/zed/assets/2690773/874377c7-c967-4a6f-8a89-ec7bf398a8b3)

Unsuccessful command:

![image](https://github.com/zed-industries/zed/assets/2690773/6c99dc5d-d324-41e9-a71b-5d0bf705de27)

The "design", including wordings and special characters, is not final,
suggestions are welcome.
The main idea was to somehow distinguish the appended lines without
occupying extra vertical space.

Release Notes:

- Added task summary output into corresponding terminal tabs
This commit is contained in:
Kirill Bulatov 2024-04-16 21:13:35 +02:00 committed by GitHub
parent 4eb1e65fbb
commit be2bf98529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 102 additions and 11 deletions

View File

@ -55,6 +55,14 @@ impl Project {
id: spawn_task.id, id: spawn_task.id,
full_label: spawn_task.full_label, full_label: spawn_task.full_label,
label: spawn_task.label, label: spawn_task.label,
command_label: spawn_task.args.iter().fold(
spawn_task.command.clone(),
|mut command_label, new_arg| {
command_label.push(' ');
command_label.push_str(new_arg);
command_label
},
),
status: TaskStatus::Running, status: TaskStatus::Running,
completion_rx, completion_rx,
}), }),

View File

@ -598,6 +598,7 @@ pub struct TaskState {
pub id: TaskId, pub id: TaskId,
pub full_label: String, pub full_label: String,
pub label: String, pub label: String,
pub command_label: String,
pub status: TaskStatus, pub status: TaskStatus,
pub completion_rx: Receiver<()>, pub completion_rx: Receiver<()>,
} }
@ -657,13 +658,7 @@ impl Terminal {
AlacTermEvent::Bell => { AlacTermEvent::Bell => {
cx.emit(Event::Bell); cx.emit(Event::Bell);
} }
AlacTermEvent::Exit => match &mut self.task { AlacTermEvent::Exit => self.register_task_finished(None, cx),
Some(task) => {
task.status.register_terminal_exit();
self.completion_tx.try_send(()).ok();
}
None => cx.emit(Event::CloseTerminal),
},
AlacTermEvent::MouseCursorDirty => { AlacTermEvent::MouseCursorDirty => {
//NOOP, Handled in render //NOOP, Handled in render
} }
@ -679,10 +674,7 @@ impl Terminal {
.push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone())); .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
} }
AlacTermEvent::ChildExit(error_code) => { AlacTermEvent::ChildExit(error_code) => {
if let Some(task) = &mut self.task { self.register_task_finished(Some(*error_code), cx);
task.status.register_task_exit(*error_code);
self.completion_tx.try_send(()).ok();
}
} }
} }
} }
@ -1425,6 +1417,97 @@ impl Terminal {
} }
Task::ready(()) Task::ready(())
} }
fn register_task_finished(
&mut self,
error_code: Option<i32>,
cx: &mut ModelContext<'_, Terminal>,
) {
self.completion_tx.try_send(()).ok();
let task = match &mut self.task {
Some(task) => task,
None => {
if error_code.is_none() {
cx.emit(Event::CloseTerminal);
}
return;
}
};
if task.status != TaskStatus::Running {
return;
}
match error_code {
Some(error_code) => {
task.status.register_task_exit(error_code);
}
None => {
task.status.register_terminal_exit();
}
};
let (task_line, command_line) = task_summary(task, error_code);
// SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once,
// after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned
// when Zed task finishes and no more output is made.
// After the task summary is output once, no more text is appended to the terminal.
unsafe { append_text_to_term(&mut self.term.lock(), &[&task_line, &command_line]) };
}
}
const TASK_DELIMITER: &str = "";
fn task_summary(task: &TaskState, error_code: Option<i32>) -> (String, String) {
let escaped_full_label = task.full_label.replace("\r\n", "\r").replace('\n', "\r");
let task_line = match error_code {
Some(0) => {
format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished successfully")
}
Some(error_code) => {
format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished with non-zero error code: {error_code}")
}
None => {
format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished")
}
};
let escaped_command_label = task.command_label.replace("\r\n", "\r").replace('\n', "\r");
let command_line = format!("{TASK_DELIMITER}Command: '{escaped_command_label}'");
(task_line, command_line)
}
/// Appends a stringified task summary to the terminal, after its output.
///
/// SAFETY: This function should only be called after terminal's PTY is no longer alive.
/// New text being added to the terminal here, uses "less public" APIs,
/// which are not maintaining the entire terminal state intact.
///
///
/// The library
///
/// * does not increment inner grid cursor's _lines_ on `input` calls
/// (but displaying the lines correctly and incrementing cursor's columns)
///
/// * ignores `\n` and \r` character input, requiring the `newline` call instead
///
/// * does not alter grid state after `newline` call
/// so its `bottommost_line` is always the the same additions, and
/// the cursor's `point` is not updated to the new line and column values
///
/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic.
/// Still, concequent `append_text_to_term` invocations are possible and display the contents correctly.
///
/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations,
/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone.
/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized.
/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts.
unsafe fn append_text_to_term(term: &mut Term<ZedListener>, text_lines: &[&str]) {
term.newline();
term.grid_mut().cursor.point.column = Column(0);
for line in text_lines {
for c in line.chars() {
term.input(c);
}
term.newline();
term.grid_mut().cursor.point.column = Column(0);
}
} }
impl Drop for Terminal { impl Drop for Terminal {