Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(watch): display which tasks will be rerun #8960

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/turborepo-lib/src/run/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ impl WatchClient {
.build(&signal_handler, telemetry)
.await?;

if let Some(sender) = &self.ui_sender {
let task_names = run.engine.tasks_with_command(&run.pkg_dep_graph);
sender
.restart_tasks(task_names)
.map_err(|err| Error::UISend(err.to_string()))?;
}

Ok(run.run(self.ui_sender.clone(), true).await?)
}
ChangedPackages::All => {
Expand Down
163 changes: 120 additions & 43 deletions crates/turborepo-ui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use ratatui::{
widgets::TableState,
Frame, Terminal,
};
use tracing::debug;
use tracing::{debug, trace};

const PANE_SIZE_RATIO: f32 = 3.0 / 4.0;
const FRAMERATE: Duration = Duration::from_millis(3);
Expand Down Expand Up @@ -178,18 +178,6 @@ impl<W> App<W> {
let running = planned.start();
self.tasks_by_status.running.push(running);

found_task = true;
} else if let Some(finished_idx) = self
.tasks_by_status
.finished
.iter()
.position(|finished| finished.name() == task)
{
let _finished = self.tasks_by_status.finished.remove(finished_idx);
self.tasks_by_status
.running
.push(Task::new(task.to_owned()).start());

found_task = true;
}

Expand All @@ -202,18 +190,7 @@ impl<W> App<W> {
.output_logs = Some(output_logs);

// If user hasn't interacted, keep highlighting top-most task in list.
if !self.has_user_scrolled {
return Ok(());
}

if let Some(new_index_to_highlight) = self
.tasks_by_status
.task_names_in_displayed_order()
.position(|running| running == highlighted_task)
{
self.selected_task_index = new_index_to_highlight;
self.scroll.select(Some(new_index_to_highlight));
}
self.select_task(&highlighted_task)?;

Ok(())
}
Expand All @@ -235,11 +212,7 @@ impl<W> App<W> {
.running
.iter()
.position(|running| running.name() == task)
.ok_or_else(|| {
debug!("could not find '{task}' to finish");
println!("{:#?}", highlighted_task);
Error::TaskNotFound { name: task.into() }
})?;
.ok_or_else(|| Error::TaskNotFound { name: task.into() })?;

let running = self.tasks_by_status.running.remove(running_idx);
self.tasks_by_status.finished.push(running.finish(result));
Expand All @@ -249,20 +222,8 @@ impl<W> App<W> {
.ok_or_else(|| Error::TaskNotFound { name: task.into() })?
.task_result = Some(result);

// If user hasn't interacted, keep highlighting top-most task in list.
if !self.has_user_scrolled {
return Ok(());
}

// Find the highlighted task from before the list movement in the new list.
if let Some(new_index_to_highlight) = self
.tasks_by_status
.task_names_in_displayed_order()
.position(|running| running == highlighted_task.as_str())
{
self.selected_task_index = new_index_to_highlight;
self.scroll.select(Some(new_index_to_highlight));
}
self.select_task(&highlighted_task)?;

Ok(())
}
Expand All @@ -286,6 +247,7 @@ impl<W> App<W> {
#[tracing::instrument(skip(self))]
pub fn update_tasks(&mut self, tasks: Vec<String>) {
debug!("updating task list: {tasks:?}");
let highlighted_task = self.active_task().to_owned();
// Make sure all tasks have a terminal output
for task in &tasks {
self.tasks
Expand All @@ -303,6 +265,30 @@ impl<W> App<W> {
running: Default::default(),
finished: Default::default(),
};

// Task that was selected may have been removed, go back to top if this happens
if self.select_task(&highlighted_task).is_err() {
trace!("{highlighted_task} was removed from list");
self.reset_scroll();
}
}

#[tracing::instrument(skip(self))]
pub fn restart_tasks(&mut self, tasks: Vec<String>) {
debug!("tasks to reset: {tasks:?}");
let highlighted_task = self.active_task().to_owned();
// Make sure all tasks have a terminal output
for task in &tasks {
self.tasks
.entry(task.clone())
.or_insert_with(|| TerminalOutput::new(self.pane_rows, self.pane_cols, None));
}

self.tasks_by_status
.restart_tasks(tasks.iter().map(|s| s.as_str()));

self.select_task(&highlighted_task)
.expect("should find task after restart");
}

/// Persist all task output to the after closing the TUI
Expand Down Expand Up @@ -362,6 +348,33 @@ impl<W> App<W> {
};
super::copy_to_clipboard(&text);
}

fn select_task(&mut self, task_name: &str) -> Result<(), Error> {
if !self.has_user_scrolled {
return Ok(());
}

let Some(new_index_to_highlight) = self
.tasks_by_status
.task_names_in_displayed_order()
.position(|task| task == task_name)
else {
return Err(Error::TaskNotFound {
name: task_name.to_owned(),
});
};
self.selected_task_index = new_index_to_highlight;
self.scroll.select(Some(new_index_to_highlight));

Ok(())
}

/// Resets scroll state
pub fn reset_scroll(&mut self) {
self.has_user_scrolled = false;
self.scroll.select(Some(0));
self.selected_task_index = 0;
}
}

impl<W: Write> App<W> {
Expand Down Expand Up @@ -579,6 +592,9 @@ fn update(
Event::CopySelection => {
app.copy_selection();
}
Event::RestartTasks { tasks } => {
app.update_tasks(tasks);
}
}
Ok(None)
}
Expand Down Expand Up @@ -692,6 +708,7 @@ mod test {
);

// Restart b
app.restart_tasks(vec!["b".to_string()]);
app.start_task("b", OutputLogs::Full).unwrap();
assert_eq!(
(
Expand All @@ -703,6 +720,7 @@ mod test {
);

// Restart a
app.restart_tasks(vec!["a".to_string()]);
app.start_task("a", OutputLogs::Full).unwrap();
assert_eq!(
(
Expand Down Expand Up @@ -822,4 +840,63 @@ mod test {
);
assert!(app.tasks.get("b").unwrap().status.is_none());
}

#[test]
fn test_restarting_task_no_scroll() {
let mut app: App<()> = App::new(
100,
100,
vec!["a".to_string(), "b".to_string(), "c".to_string()],
);
assert_eq!(app.scroll.selected(), Some(0), "selected a");
assert_eq!(app.tasks_by_status.task_name(0), "a", "selected a");
app.start_task("a", OutputLogs::None).unwrap();
app.start_task("b", OutputLogs::None).unwrap();
app.start_task("c", OutputLogs::None).unwrap();
app.finish_task("b", TaskResult::Success).unwrap();
app.finish_task("c", TaskResult::Success).unwrap();
app.finish_task("a", TaskResult::Success).unwrap();

assert_eq!(app.scroll.selected(), Some(0), "selected b");
assert_eq!(app.tasks_by_status.task_name(0), "b", "selected b");

app.restart_tasks(vec!["c".to_string()]);

assert_eq!(
app.tasks_by_status
.task_name(app.scroll.selected().unwrap()),
"c",
"selected c"
);
}

#[test]
fn test_restarting_task() {
let mut app: App<()> = App::new(
100,
100,
vec!["a".to_string(), "b".to_string(), "c".to_string()],
);
app.next();
assert_eq!(app.scroll.selected(), Some(1), "selected b");
assert_eq!(app.tasks_by_status.task_name(1), "b", "selected b");
app.start_task("a", OutputLogs::None).unwrap();
app.start_task("b", OutputLogs::None).unwrap();
app.start_task("c", OutputLogs::None).unwrap();
app.finish_task("b", TaskResult::Success).unwrap();
app.finish_task("c", TaskResult::Success).unwrap();
app.finish_task("a", TaskResult::Success).unwrap();

assert_eq!(app.scroll.selected(), Some(0), "selected b");
assert_eq!(app.tasks_by_status.task_name(0), "b", "selected b");

app.restart_tasks(vec!["c".to_string()]);

assert_eq!(
app.tasks_by_status
.task_name(app.scroll.selected().unwrap()),
"b",
"selected b"
);
}
}
3 changes: 3 additions & 0 deletions crates/turborepo-ui/src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub enum Event {
},
Mouse(crossterm::event::MouseEvent),
CopySelection,
RestartTasks {
tasks: Vec<String>,
},
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
Expand Down
5 changes: 5 additions & 0 deletions crates/turborepo-ui/src/tui/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ impl AppSender {
pub fn update_tasks(&self, tasks: Vec<String>) -> Result<(), mpsc::SendError<Event>> {
self.primary.send(Event::UpdateTasks { tasks })
}

/// Restart the list of tasks displayed in the TUI
pub fn restart_tasks(&self, tasks: Vec<String>) -> Result<(), mpsc::SendError<Event>> {
self.primary.send(Event::RestartTasks { tasks })
}
}

impl AppReceiver {
Expand Down
37 changes: 36 additions & 1 deletion crates/turborepo-ui/src/tui/task.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![allow(dead_code)]
use std::time::Instant;
use std::{collections::HashSet, mem, time::Instant};

use super::event::TaskResult;

Expand Down Expand Up @@ -73,6 +73,13 @@ impl Task<Running> {
pub fn start(&self) -> Instant {
self.state.start
}

pub fn restart(self) -> Task<Planned> {
Task {
name: self.name,
state: Planned,
}
}
}

impl Task<Finished> {
Expand All @@ -87,6 +94,13 @@ impl Task<Finished> {
pub fn result(&self) -> TaskResult {
self.state.result
}

pub fn restart(self) -> Task<Planned> {
Task {
name: self.name,
state: Planned,
}
}
}

pub struct TaskNamesByStatus {
Expand Down Expand Up @@ -138,4 +152,25 @@ impl TasksByStatus {
.map(|task| task.to_string())
.collect()
}

pub fn restart_tasks<'a>(&mut self, tasks: impl Iterator<Item = &'a str>) {
let tasks_to_restart = tasks.collect::<HashSet<_>>();

let (restarted_running, keep_running): (Vec<_>, Vec<_>) = mem::take(&mut self.running)
.into_iter()
.partition(|task| tasks_to_restart.contains(task.name()));
self.running = keep_running;

let (restarted_finished, keep_finished): (Vec<_>, Vec<_>) = mem::take(&mut self.finished)
.into_iter()
.partition(|task| tasks_to_restart.contains(task.name()));
self.finished = keep_finished;
self.planned.extend(
restarted_running
.into_iter()
.map(|task| task.restart())
.chain(restarted_finished.into_iter().map(|task| task.restart())),
);
self.planned.sort_unstable();
}
}
Loading