1use std::process::ExitStatus;
2
3use anyhow::Result;
4use gpui::{AppContext, Context, Entity, Task};
5use language::Buffer;
6use project::{TaskSourceKind, WorktreeId};
7use remote::ConnectionState;
8use task::{
9 DebugScenario, ResolvedTask, SaveStrategy, SharedTaskContext, SpawnInTerminal, TaskContext,
10 TaskTemplate,
11};
12use ui::Window;
13use util::TryFutureExt;
14
15use crate::{SaveIntent, Toast, Workspace, notifications::NotificationId};
16
17impl Workspace {
18 pub fn schedule_task(
19 self: &mut Workspace,
20 task_source_kind: TaskSourceKind,
21 task_to_resolve: &TaskTemplate,
…lines 22–71 unchanged (50 lines)
72 inventory.task_scheduled(task_source_kind, resolved_task);
73 })
74 }
75 });
76 }
77
78 if self.terminal_provider.is_some() {
79 let task = cx.spawn_in(window, async move |workspace, cx| {
80 let save_action = match spawn_in_terminal.save {
81 SaveStrategy::All => {
82 let save_all = workspace.update_in(cx, |workspace, window, cx| {
83 let task = workspace.save_all_internal(SaveIntent::SaveAll, window, cx);
84 // Match the type of the other arm by ignoring the bool value returned
85 cx.background_spawn(async { task.await.map(|_| ()) })
86 });
87 save_all.ok()
88 }
89 SaveStrategy::Current => {
90 let save_current = workspace.update_in(cx, |workspace, window, cx| {
91 workspace.save_active_item(SaveIntent::SaveAll, window, cx)
92 });
93 save_current.ok()
94 }
95 SaveStrategy::None => None,
96 };
97 if let Some(save_action) = save_action {
98 save_action.log_err().await;
99 }
100
101 let spawn_task = workspace.update_in(cx, |workspace, window, cx| {
102 workspace
103 .terminal_provider
104 .as_ref()
105 .map(|terminal_provider| {
106 terminal_provider.spawn(spawn_in_terminal, window, cx)
107 })
108 });
109 if let Some(spawn_task) = spawn_task.ok().flatten() {
110 let res = cx.background_spawn(spawn_task).await;
111 match res {
112 Some(Ok(status)) => {
113 if status.success() {
114 log::debug!("Task spawn succeeded");
115 } else {
116 log::debug!("Task spawn failed, code: {:?}", status.code());
117 }
118 }
119 Some(Err(e)) => {
120 log::error!("Task spawn failed: {e:#}");
121 _ = workspace.update(cx, |w, cx| {
122 let id = NotificationId::unique::<ResolvedTask>();
123 w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx);
124 })
125 }
126 None => log::debug!("Task spawn got cancelled"),
127 };
128 }
129 });
130 self.scheduled_tasks.push(task);
131 }
132 }
133
134 pub fn start_debug_session(
…lines 135–162 unchanged (28 lines)
163 } else {
164 Task::ready(None)
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::{
173 TerminalProvider,
174 item::test::{TestItem, TestProjectItem},
175 register_serializable_item,
176 };
177 use gpui::{App, TestAppContext};
178 use parking_lot::Mutex;
179 use project::{FakeFs, Project, TaskSourceKind};
180 use serde_json::json;
181 use std::sync::Arc;
182 use task::TaskTemplate;
183
184 struct Fixture {
185 workspace: Entity<Workspace>,
186 item: Entity<TestItem>,
187 task: ResolvedTask,
188 dirty_before_spawn: Arc<Mutex<Option<bool>>>,
189 }
190
191 #[gpui::test]
192 async fn test_schedule_resolved_task_save_all(cx: &mut TestAppContext) {
193 let (fixture, cx) = create_fixture(cx, SaveStrategy::All).await;
194 fixture.workspace.update_in(cx, |workspace, window, cx| {
195 workspace.schedule_resolved_task(
196 TaskSourceKind::UserInput,
197 fixture.task,
198 false,
199 window,
200 cx,
201 );
202 });
203 cx.executor().run_until_parked();
204
205 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
206 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
207 }
208
209 #[gpui::test]
210 async fn test_schedule_resolved_task_save_current(cx: &mut TestAppContext) {
211 let (fixture, cx) = create_fixture(cx, SaveStrategy::Current).await;
212 // Add a second inactive dirty item
213 let inactive = add_test_item(&fixture.workspace, "file2.txt", false, cx);
214 fixture.workspace.update_in(cx, |workspace, window, cx| {
215 workspace.schedule_resolved_task(
216 TaskSourceKind::UserInput,
217 fixture.task,
218 false,
219 window,
220 cx,
221 );
222 });
223 cx.executor().run_until_parked();
224
225 // The active item (fixture.item) should be saved
226 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(false));
227 assert!(cx.read(|cx| !fixture.item.read(cx).is_dirty));
228 // The inactive item should not be saved
229 assert!(cx.read(|cx| inactive.read(cx).is_dirty));
230 }
231
232 #[gpui::test]
233 async fn test_schedule_resolved_task_save_none(cx: &mut TestAppContext) {
234 let (fixture, cx) = create_fixture(cx, SaveStrategy::None).await;
235 fixture.workspace.update_in(cx, |workspace, window, cx| {
236 workspace.schedule_resolved_task(
237 TaskSourceKind::UserInput,
238 fixture.task,
239 false,
240 window,
241 cx,
242 );
243 });
244 cx.executor().run_until_parked();
245
246 assert_eq!(*fixture.dirty_before_spawn.lock(), Some(true));
247 assert!(cx.read(|cx| fixture.item.read(cx).is_dirty));
248 }
249
250 async fn create_fixture(
251 cx: &mut TestAppContext,
252 save_strategy: SaveStrategy,
253 ) -> (Fixture, &mut gpui::VisualTestContext) {
254 cx.update(|cx| {
255 let settings_store = settings::SettingsStore::test(cx);
256 cx.set_global(settings_store);
257 theme::init(theme::LoadThemes::JustBase, cx);
258 register_serializable_item::<TestItem>(cx);
259 });
260 let fs = FakeFs::new(cx.executor());
261 fs.insert_tree("/root", json!({ "file.txt": "dirty" }))
262 .await;
263 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
264 let (workspace, cx) =
265 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
266
267 // Add a dirty item to the workspace
268 let item = add_test_item(&workspace, "file.txt", true, cx);
269
270 let template = TaskTemplate {
271 label: "test".to_string(),
272 command: "echo".to_string(),
273 save: save_strategy,
274 ..Default::default()
275 };
276 let task = template
277 .resolve_task("test", &task::TaskContext::default())
278 .unwrap();
279 let dirty_before_spawn: Arc<Mutex<Option<bool>>> = Arc::default();
280 let terminal_provider = Box::new(TestTerminalProvider {
281 item: item.clone(),
282 dirty_before_spawn: dirty_before_spawn.clone(),
283 });
284 workspace.update(cx, |workspace, _| {
285 workspace.terminal_provider = Some(terminal_provider);
286 });
287 let fixture = Fixture {
288 workspace,
289 item,
290 task,
291 dirty_before_spawn,
292 };
293 (fixture, cx)
294 }
295
296 fn add_test_item(
297 workspace: &Entity<Workspace>,
298 name: &str,
299 active: bool,
300 cx: &mut gpui::VisualTestContext,
301 ) -> Entity<TestItem> {
302 let item = cx.new(|cx| {
303 TestItem::new(cx)
304 .with_dirty(true)
305 .with_project_items(&[TestProjectItem::new(1, name, cx)])
306 });
307 workspace.update_in(cx, |workspace, window, cx| {
308 let pane = workspace.active_pane().clone();
309 workspace.add_item(pane, Box::new(item.clone()), None, true, active, window, cx);
310 });
311 item
312 }
313
314 struct TestTerminalProvider {
315 item: Entity<TestItem>,
316 dirty_before_spawn: Arc<Mutex<Option<bool>>>,
317 }
318
319 impl TerminalProvider for TestTerminalProvider {
320 fn spawn(
321 &self,
322 _task: task::SpawnInTerminal,
323 _window: &mut ui::Window,
324 cx: &mut App,
325 ) -> Task<Option<Result<ExitStatus>>> {
326 *self.dirty_before_spawn.lock() = Some(cx.read_entity(&self.item, |e, _| e.is_dirty));
327 Task::ready(Some(Ok(ExitStatus::default())))
328 }
329 }
330}
331