From be2bf98529726ba6845486b283315b5ffb228391 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 16 Apr 2024 21:13:35 +0200 Subject: [PATCH] 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 --- crates/project/src/terminals.rs | 8 +++ crates/terminal/src/terminal.rs | 105 ++++++++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 4784cf6568..12c743dc64 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -55,6 +55,14 @@ impl Project { id: spawn_task.id, full_label: spawn_task.full_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, completion_rx, }), diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0b7c39ced1..63fc4b2793 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -598,6 +598,7 @@ pub struct TaskState { pub id: TaskId, pub full_label: String, pub label: String, + pub command_label: String, pub status: TaskStatus, pub completion_rx: Receiver<()>, } @@ -657,13 +658,7 @@ impl Terminal { AlacTermEvent::Bell => { cx.emit(Event::Bell); } - AlacTermEvent::Exit => match &mut self.task { - Some(task) => { - task.status.register_terminal_exit(); - self.completion_tx.try_send(()).ok(); - } - None => cx.emit(Event::CloseTerminal), - }, + AlacTermEvent::Exit => self.register_task_finished(None, cx), AlacTermEvent::MouseCursorDirty => { //NOOP, Handled in render } @@ -679,10 +674,7 @@ impl Terminal { .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone())); } AlacTermEvent::ChildExit(error_code) => { - if let Some(task) = &mut self.task { - task.status.register_task_exit(*error_code); - self.completion_tx.try_send(()).ok(); - } + self.register_task_finished(Some(*error_code), cx); } } } @@ -1425,6 +1417,97 @@ impl Terminal { } Task::ready(()) } + + fn register_task_finished( + &mut self, + error_code: Option, + 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) -> (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, 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 {