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