lib.rs

  1use ::settings::Settings;
  2use editor::{tasks::task_context, Editor};
  3use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
  4use modal::TasksModal;
  5use project::{Location, WorktreeId};
  6use task::TaskId;
  7use workspace::tasks::schedule_task;
  8use workspace::{tasks::schedule_resolved_task, Workspace};
  9
 10mod modal;
 11mod settings;
 12
 13pub use modal::{Rerun, Spawn};
 14use zed_actions::TaskSpawnTarget;
 15
 16pub fn init(cx: &mut AppContext) {
 17    settings::TaskSettings::register(cx);
 18    cx.observe_new_views(
 19        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
 20            workspace
 21                .register_action(spawn_task_or_modal)
 22                .register_action(move |workspace, action: &modal::Rerun, cx| {
 23                    if let Some((task_source_kind, mut last_scheduled_task)) = workspace
 24                        .project()
 25                        .read(cx)
 26                        .task_store()
 27                        .read(cx)
 28                        .task_inventory()
 29                        .and_then(|inventory| {
 30                            inventory.read(cx).last_scheduled_task(
 31                                action
 32                                    .task_id
 33                                    .as_ref()
 34                                    .map(|id| TaskId(id.clone()))
 35                                    .as_ref(),
 36                            )
 37                        })
 38                    {
 39                        if action.reevaluate_context {
 40                            let mut original_task = last_scheduled_task.original_task().clone();
 41                            if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 42                                original_task.allow_concurrent_runs = allow_concurrent_runs;
 43                            }
 44                            if let Some(use_new_terminal) = action.use_new_terminal {
 45                                original_task.use_new_terminal = use_new_terminal;
 46                            }
 47                            let context_task = task_context(workspace, cx);
 48                            cx.spawn(|workspace, mut cx| async move {
 49                                let task_context = context_task.await;
 50                                workspace
 51                                    .update(&mut cx, |workspace, cx| {
 52                                        schedule_task(
 53                                            workspace,
 54                                            task_source_kind,
 55                                            &original_task,
 56                                            &task_context,
 57                                            Default::default(),
 58                                            false,
 59                                            cx,
 60                                        )
 61                                    })
 62                                    .ok()
 63                            })
 64                            .detach()
 65                        } else {
 66                            if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
 67                                if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 68                                    resolved.allow_concurrent_runs = allow_concurrent_runs;
 69                                }
 70                                if let Some(use_new_terminal) = action.use_new_terminal {
 71                                    resolved.use_new_terminal = use_new_terminal;
 72                                }
 73                            }
 74
 75                            schedule_resolved_task(
 76                                workspace,
 77                                task_source_kind,
 78                                last_scheduled_task,
 79                                false,
 80                                cx,
 81                            );
 82                        }
 83                    } else {
 84                        toggle_modal(workspace, cx).detach();
 85                    };
 86                });
 87        },
 88    )
 89    .detach();
 90}
 91
 92fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
 93    match &action.task_name {
 94        Some(name) => spawn_task_with_name(name.clone(), action.target.unwrap_or_default(), cx)
 95            .detach_and_log_err(cx),
 96        None => toggle_modal(workspace, cx).detach(),
 97    }
 98}
 99
100fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
101    let task_store = workspace.project().read(cx).task_store().clone();
102    let workspace_handle = workspace.weak_handle();
103    let can_open_modal = workspace.project().update(cx, |project, cx| {
104        project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
105    });
106    if can_open_modal {
107        let context_task = task_context(workspace, cx);
108        cx.spawn(|workspace, mut cx| async move {
109            let task_context = context_task.await;
110            workspace
111                .update(&mut cx, |workspace, cx| {
112                    workspace.toggle_modal(cx, |cx| {
113                        TasksModal::new(task_store.clone(), task_context, workspace_handle, cx)
114                    })
115                })
116                .ok();
117        })
118    } else {
119        AsyncTask::ready(())
120    }
121}
122
123fn spawn_task_with_name(
124    name: String,
125    task_target: TaskSpawnTarget,
126    cx: &mut ViewContext<Workspace>,
127) -> AsyncTask<anyhow::Result<()>> {
128    cx.spawn(|workspace, mut cx| async move {
129        let context_task =
130            workspace.update(&mut cx, |workspace, cx| task_context(workspace, cx))?;
131        let task_context = context_task.await;
132        let tasks = workspace.update(&mut cx, |workspace, cx| {
133            let Some(task_inventory) = workspace
134                .project()
135                .read(cx)
136                .task_store()
137                .read(cx)
138                .task_inventory()
139                .cloned()
140            else {
141                return Vec::new();
142            };
143            let (worktree, location) = active_item_selection_properties(workspace, cx);
144            let (file, language) = location
145                .map(|location| {
146                    let buffer = location.buffer.read(cx);
147                    (
148                        buffer.file().cloned(),
149                        buffer.language_at(location.range.start),
150                    )
151                })
152                .unwrap_or_default();
153            task_inventory
154                .read(cx)
155                .list_tasks(file, language, worktree, cx)
156        })?;
157
158        let did_spawn = workspace
159            .update(&mut cx, |workspace, cx| {
160                let (task_source_kind, target_task) =
161                    tasks.into_iter().find(|(_, task)| task.label == name)?;
162                schedule_task(
163                    workspace,
164                    task_source_kind,
165                    &target_task,
166                    &task_context,
167                    task_target,
168                    false,
169                    cx,
170                );
171                Some(())
172            })?
173            .is_some();
174        if !did_spawn {
175            workspace
176                .update(&mut cx, |workspace, cx| {
177                    spawn_task_or_modal(workspace, &Spawn::default(), cx);
178                })
179                .ok();
180        }
181
182        Ok(())
183    })
184}
185
186fn active_item_selection_properties(
187    workspace: &Workspace,
188    cx: &mut WindowContext,
189) -> (Option<WorktreeId>, Option<Location>) {
190    let active_item = workspace.active_item(cx);
191    let worktree_id = active_item
192        .as_ref()
193        .and_then(|item| item.project_path(cx))
194        .map(|path| path.worktree_id);
195    let location = active_item
196        .and_then(|active_item| active_item.act_as::<Editor>(cx))
197        .and_then(|editor| {
198            editor.update(cx, |editor, cx| {
199                let selection = editor.selections.newest_anchor();
200                let multi_buffer = editor.buffer().clone();
201                let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
202                let (buffer_snapshot, buffer_offset) =
203                    multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
204                let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
205                let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
206                Some(Location {
207                    buffer,
208                    range: buffer_anchor..buffer_anchor,
209                })
210            })
211        });
212    (worktree_id, location)
213}
214
215#[cfg(test)]
216mod tests {
217    use std::{collections::HashMap, sync::Arc};
218
219    use editor::Editor;
220    use gpui::{Entity, TestAppContext};
221    use language::{Language, LanguageConfig};
222    use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project};
223    use serde_json::json;
224    use task::{TaskContext, TaskVariables, VariableName};
225    use ui::VisualContext;
226    use workspace::{AppState, Workspace};
227
228    use crate::task_context;
229
230    #[gpui::test]
231    async fn test_default_language_context(cx: &mut TestAppContext) {
232        init_test(cx);
233        let fs = FakeFs::new(cx.executor());
234        fs.insert_tree(
235            "/dir",
236            json!({
237                ".zed": {
238                    "tasks.json": r#"[
239                            {
240                                "label": "example task",
241                                "command": "echo",
242                                "args": ["4"]
243                            },
244                            {
245                                "label": "another one",
246                                "command": "echo",
247                                "args": ["55"]
248                            },
249                        ]"#,
250                },
251                "a.ts": "function this_is_a_test() { }",
252                "rust": {
253                                    "b.rs": "use std; fn this_is_a_rust_file() { }",
254                }
255
256            }),
257        )
258        .await;
259        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
260        let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
261        let rust_language = Arc::new(
262            Language::new(
263                LanguageConfig::default(),
264                Some(tree_sitter_rust::LANGUAGE.into()),
265            )
266            .with_outline_query(
267                r#"(function_item
268            "fn" @context
269            name: (_) @name) @item"#,
270            )
271            .unwrap()
272            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
273                worktree_store.clone(),
274            )))),
275        );
276
277        let typescript_language = Arc::new(
278            Language::new(
279                LanguageConfig::default(),
280                Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
281            )
282            .with_outline_query(
283                r#"(function_declaration
284                    "async"? @context
285                    "function" @context
286                    name: (_) @name
287                    parameters: (formal_parameters
288                      "(" @context
289                      ")" @context)) @item"#,
290            )
291            .unwrap()
292            .with_context_provider(Some(Arc::new(BasicContextProvider::new(
293                worktree_store.clone(),
294            )))),
295        );
296
297        let worktree_id = project.update(cx, |project, cx| {
298            project.worktrees(cx).next().unwrap().read(cx).id()
299        });
300        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
301
302        let buffer1 = workspace
303            .update(cx, |this, cx| {
304                this.project()
305                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
306            })
307            .await
308            .unwrap();
309        buffer1.update(cx, |this, cx| {
310            this.set_language(Some(typescript_language), cx)
311        });
312        let editor1 = cx.new_view(|cx| Editor::for_buffer(buffer1, Some(project.clone()), cx));
313        let buffer2 = workspace
314            .update(cx, |this, cx| {
315                this.project().update(cx, |this, cx| {
316                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
317                })
318            })
319            .await
320            .unwrap();
321        buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
322        let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx));
323
324        let first_context = workspace
325            .update(cx, |workspace, cx| {
326                workspace.add_item_to_center(Box::new(editor1.clone()), cx);
327                workspace.add_item_to_center(Box::new(editor2.clone()), cx);
328                assert_eq!(
329                    workspace.active_item(cx).unwrap().item_id(),
330                    editor2.entity_id()
331                );
332                task_context(workspace, cx)
333            })
334            .await;
335        assert_eq!(
336            first_context,
337            TaskContext {
338                cwd: Some("/dir".into()),
339                task_variables: TaskVariables::from_iter([
340                    (VariableName::File, "/dir/rust/b.rs".into()),
341                    (VariableName::Filename, "b.rs".into()),
342                    (VariableName::RelativeFile, "rust/b.rs".into()),
343                    (VariableName::Dirname, "/dir/rust".into()),
344                    (VariableName::Stem, "b".into()),
345                    (VariableName::WorktreeRoot, "/dir".into()),
346                    (VariableName::Row, "1".into()),
347                    (VariableName::Column, "1".into()),
348                ]),
349                project_env: HashMap::default(),
350            }
351        );
352
353        // And now, let's select an identifier.
354        editor2.update(cx, |editor, cx| {
355            editor.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
356        });
357
358        assert_eq!(
359            workspace
360                .update(cx, |workspace, cx| { task_context(workspace, cx) })
361                .await,
362            TaskContext {
363                cwd: Some("/dir".into()),
364                task_variables: TaskVariables::from_iter([
365                    (VariableName::File, "/dir/rust/b.rs".into()),
366                    (VariableName::Filename, "b.rs".into()),
367                    (VariableName::RelativeFile, "rust/b.rs".into()),
368                    (VariableName::Dirname, "/dir/rust".into()),
369                    (VariableName::Stem, "b".into()),
370                    (VariableName::WorktreeRoot, "/dir".into()),
371                    (VariableName::Row, "1".into()),
372                    (VariableName::Column, "15".into()),
373                    (VariableName::SelectedText, "is_i".into()),
374                    (VariableName::Symbol, "this_is_a_rust_file".into()),
375                ]),
376                project_env: HashMap::default(),
377            }
378        );
379
380        assert_eq!(
381            workspace
382                .update(cx, |workspace, cx| {
383                    // Now, let's switch the active item to .ts file.
384                    workspace.activate_item(&editor1, true, true, cx);
385                    task_context(workspace, cx)
386                })
387                .await,
388            TaskContext {
389                cwd: Some("/dir".into()),
390                task_variables: TaskVariables::from_iter([
391                    (VariableName::File, "/dir/a.ts".into()),
392                    (VariableName::Filename, "a.ts".into()),
393                    (VariableName::RelativeFile, "a.ts".into()),
394                    (VariableName::Dirname, "/dir".into()),
395                    (VariableName::Stem, "a".into()),
396                    (VariableName::WorktreeRoot, "/dir".into()),
397                    (VariableName::Row, "1".into()),
398                    (VariableName::Column, "1".into()),
399                    (VariableName::Symbol, "this_is_a_test".into()),
400                ]),
401                project_env: HashMap::default(),
402            }
403        );
404    }
405
406    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
407        cx.update(|cx| {
408            let state = AppState::test(cx);
409            file_icons::init((), cx);
410            language::init(cx);
411            crate::init(cx);
412            editor::init(cx);
413            workspace::init_settings(cx);
414            Project::init_settings(cx);
415            TaskStore::init(None);
416            state
417        })
418    }
419}