lib.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    sync::Arc,
  4};
  5
  6use ::settings::Settings;
  7use anyhow::Context;
  8use editor::Editor;
  9use gpui::{AppContext, ViewContext, WindowContext};
 10use language::{BasicContextProvider, ContextProvider, Language};
 11use modal::TasksModal;
 12use project::{Location, TaskSourceKind, WorktreeId};
 13use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables};
 14use util::ResultExt;
 15use workspace::Workspace;
 16
 17mod modal;
 18mod settings;
 19
 20pub use modal::Spawn;
 21
 22pub fn init(cx: &mut AppContext) {
 23    settings::TaskSettings::register(cx);
 24    cx.observe_new_views(
 25        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
 26            workspace
 27                .register_action(spawn_task_or_modal)
 28                .register_action(move |workspace, action: &modal::Rerun, cx| {
 29                    if let Some((task_source_kind, mut last_scheduled_task)) =
 30                        workspace.project().update(cx, |project, cx| {
 31                            project.task_inventory().read(cx).last_scheduled_task()
 32                        })
 33                    {
 34                        if action.reevaluate_context {
 35                            let mut original_task = last_scheduled_task.original_task().clone();
 36                            if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 37                                original_task.allow_concurrent_runs = allow_concurrent_runs;
 38                            }
 39                            if let Some(use_new_terminal) = action.use_new_terminal {
 40                                original_task.use_new_terminal = use_new_terminal;
 41                            }
 42                            let task_context = task_context(workspace, cx);
 43                            schedule_task(
 44                                workspace,
 45                                task_source_kind,
 46                                &original_task,
 47                                &task_context,
 48                                false,
 49                                cx,
 50                            )
 51                        } else {
 52                            if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
 53                                if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
 54                                    resolved.allow_concurrent_runs = allow_concurrent_runs;
 55                                }
 56                                if let Some(use_new_terminal) = action.use_new_terminal {
 57                                    resolved.use_new_terminal = use_new_terminal;
 58                                }
 59                            }
 60
 61                            schedule_resolved_task(
 62                                workspace,
 63                                task_source_kind,
 64                                last_scheduled_task,
 65                                false,
 66                                cx,
 67                            );
 68                        }
 69                    } else {
 70                        toggle_modal(workspace, cx);
 71                    };
 72                });
 73        },
 74    )
 75    .detach();
 76}
 77
 78fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
 79    match &action.task_name {
 80        Some(name) => spawn_task_with_name(name.clone(), cx),
 81        None => toggle_modal(workspace, cx),
 82    }
 83}
 84
 85fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) {
 86    let inventory = workspace.project().read(cx).task_inventory().clone();
 87    let workspace_handle = workspace.weak_handle();
 88    let task_context = task_context(workspace, cx);
 89    workspace.toggle_modal(cx, |cx| {
 90        TasksModal::new(inventory, task_context, workspace_handle, cx)
 91    })
 92}
 93
 94fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
 95    cx.spawn(|workspace, mut cx| async move {
 96        let did_spawn = workspace
 97            .update(&mut cx, |workspace, cx| {
 98                let (worktree, language) = active_item_selection_properties(workspace, cx);
 99                let tasks = workspace.project().update(cx, |project, cx| {
100                    project.task_inventory().update(cx, |inventory, cx| {
101                        inventory.list_tasks(language, worktree, cx)
102                    })
103                });
104                let (task_source_kind, target_task) =
105                    tasks.into_iter().find(|(_, task)| task.label == name)?;
106                let task_context = task_context(workspace, cx);
107                schedule_task(
108                    workspace,
109                    task_source_kind,
110                    &target_task,
111                    &task_context,
112                    false,
113                    cx,
114                );
115                Some(())
116            })
117            .ok()
118            .flatten()
119            .is_some();
120        if !did_spawn {
121            workspace
122                .update(&mut cx, |workspace, cx| {
123                    spawn_task_or_modal(workspace, &Spawn::default(), cx);
124                })
125                .ok();
126        }
127    })
128    .detach();
129}
130
131fn active_item_selection_properties(
132    workspace: &Workspace,
133    cx: &mut WindowContext,
134) -> (Option<WorktreeId>, Option<Arc<Language>>) {
135    let active_item = workspace.active_item(cx);
136    let worktree_id = active_item
137        .as_ref()
138        .and_then(|item| item.project_path(cx))
139        .map(|path| path.worktree_id);
140    let language = active_item
141        .and_then(|active_item| active_item.act_as::<Editor>(cx))
142        .and_then(|editor| {
143            editor.update(cx, |editor, cx| {
144                let selection = editor.selections.newest::<usize>(cx);
145                let (buffer, buffer_position, _) = editor
146                    .buffer()
147                    .read(cx)
148                    .point_to_buffer_offset(selection.start, cx)?;
149                buffer.read(cx).language_at(buffer_position)
150            })
151        });
152    (worktree_id, language)
153}
154
155fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
156    fn task_context_impl(workspace: &Workspace, cx: &mut WindowContext<'_>) -> Option<TaskContext> {
157        let cwd = task_cwd(workspace, cx).log_err().flatten();
158        let editor = workspace
159            .active_item(cx)
160            .and_then(|item| item.act_as::<Editor>(cx))?;
161
162        let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| {
163            let selection = editor.selections.newest::<usize>(cx);
164            let (buffer, _, _) = editor
165                .buffer()
166                .read(cx)
167                .point_to_buffer_offset(selection.start, cx)?;
168            let snapshot = editor.snapshot(cx);
169            Some((selection, buffer, snapshot))
170        })?;
171        let language_context_provider = buffer
172            .read(cx)
173            .language()
174            .and_then(|language| language.context_provider())
175            .unwrap_or_else(|| Arc::new(BasicContextProvider));
176        let selection_range = selection.range();
177        let start = editor_snapshot
178            .display_snapshot
179            .buffer_snapshot
180            .anchor_after(selection_range.start)
181            .text_anchor;
182        let end = editor_snapshot
183            .display_snapshot
184            .buffer_snapshot
185            .anchor_after(selection_range.end)
186            .text_anchor;
187        let worktree_abs_path = buffer
188            .read(cx)
189            .file()
190            .map(|file| WorktreeId::from_usize(file.worktree_id()))
191            .and_then(|worktree_id| {
192                workspace
193                    .project()
194                    .read(cx)
195                    .worktree_for_id(worktree_id, cx)
196                    .map(|worktree| worktree.read(cx).abs_path())
197            });
198        let location = Location {
199            buffer,
200            range: start..end,
201        };
202        let task_variables = combine_task_variables(
203            worktree_abs_path.as_deref(),
204            location,
205            language_context_provider.as_ref(),
206            cx,
207        )
208        .log_err()?;
209        Some(TaskContext {
210            cwd,
211            task_variables,
212        })
213    }
214
215    task_context_impl(workspace, cx).unwrap_or_default()
216}
217
218fn combine_task_variables(
219    worktree_abs_path: Option<&Path>,
220    location: Location,
221    context_provider: &dyn ContextProvider,
222    cx: &mut WindowContext<'_>,
223) -> anyhow::Result<TaskVariables> {
224    if context_provider.is_basic() {
225        context_provider
226            .build_context(worktree_abs_path, &location, cx)
227            .context("building basic provider context")
228    } else {
229        let mut basic_context = BasicContextProvider
230            .build_context(worktree_abs_path, &location, cx)
231            .context("building basic default context")?;
232        basic_context.extend(
233            context_provider
234                .build_context(worktree_abs_path, &location, cx)
235                .context("building provider context ")?,
236        );
237        Ok(basic_context)
238    }
239}
240
241fn schedule_task(
242    workspace: &Workspace,
243    task_source_kind: TaskSourceKind,
244    task_to_resolve: &TaskTemplate,
245    task_cx: &TaskContext,
246    omit_history: bool,
247    cx: &mut ViewContext<'_, Workspace>,
248) {
249    if let Some(spawn_in_terminal) =
250        task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
251    {
252        schedule_resolved_task(
253            workspace,
254            task_source_kind,
255            spawn_in_terminal,
256            omit_history,
257            cx,
258        );
259    }
260}
261
262fn schedule_resolved_task(
263    workspace: &Workspace,
264    task_source_kind: TaskSourceKind,
265    mut resolved_task: ResolvedTask,
266    omit_history: bool,
267    cx: &mut ViewContext<'_, Workspace>,
268) {
269    if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
270        if !omit_history {
271            resolved_task.resolved = Some(spawn_in_terminal.clone());
272            workspace.project().update(cx, |project, cx| {
273                project.task_inventory().update(cx, |inventory, _| {
274                    inventory.task_scheduled(task_source_kind, resolved_task);
275                })
276            });
277        }
278        cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
279    }
280}
281
282fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
283    let project = workspace.project().read(cx);
284    let available_worktrees = project
285        .worktrees()
286        .filter(|worktree| {
287            let worktree = worktree.read(cx);
288            worktree.is_visible()
289                && worktree.is_local()
290                && worktree.root_entry().map_or(false, |e| e.is_dir())
291        })
292        .collect::<Vec<_>>();
293    let cwd = match available_worktrees.len() {
294        0 => None,
295        1 => Some(available_worktrees[0].read(cx).abs_path()),
296        _ => {
297            let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
298                available_worktrees.into_iter().find_map(|worktree| {
299                    let worktree = worktree.read(cx);
300                    if worktree.contains_entry(entry_id) {
301                        Some(worktree.abs_path())
302                    } else {
303                        None
304                    }
305                })
306            });
307            anyhow::ensure!(
308                cwd_for_active_entry.is_some(),
309                "Cannot determine task cwd for multiple worktrees"
310            );
311            cwd_for_active_entry
312        }
313    };
314    Ok(cwd.map(|path| path.to_path_buf()))
315}
316
317#[cfg(test)]
318mod tests {
319    use std::sync::Arc;
320
321    use editor::Editor;
322    use gpui::{Entity, TestAppContext};
323    use language::{BasicContextProvider, Language, LanguageConfig};
324    use project::{FakeFs, Project};
325    use serde_json::json;
326    use task::{TaskContext, TaskVariables, VariableName};
327    use ui::VisualContext;
328    use workspace::{AppState, Workspace};
329
330    use crate::task_context;
331
332    #[gpui::test]
333    async fn test_default_language_context(cx: &mut TestAppContext) {
334        init_test(cx);
335        let fs = FakeFs::new(cx.executor());
336        fs.insert_tree(
337            "/dir",
338            json!({
339                ".zed": {
340                    "tasks.json": r#"[
341                            {
342                                "label": "example task",
343                                "command": "echo",
344                                "args": ["4"]
345                            },
346                            {
347                                "label": "another one",
348                                "command": "echo",
349                                "args": ["55"]
350                            },
351                        ]"#,
352                },
353                "a.ts": "function this_is_a_test() { }",
354                "rust": {
355                                    "b.rs": "use std; fn this_is_a_rust_file() { }",
356                }
357
358            }),
359        )
360        .await;
361
362        let rust_language = Arc::new(
363            Language::new(
364                LanguageConfig::default(),
365                Some(tree_sitter_rust::language()),
366            )
367            .with_outline_query(
368                r#"(function_item
369            "fn" @context
370            name: (_) @name) @item"#,
371            )
372            .unwrap()
373            .with_context_provider(Some(Arc::new(BasicContextProvider))),
374        );
375
376        let typescript_language = Arc::new(
377            Language::new(
378                LanguageConfig::default(),
379                Some(tree_sitter_typescript::language_typescript()),
380            )
381            .with_outline_query(
382                r#"(function_declaration
383                    "async"? @context
384                    "function" @context
385                    name: (_) @name
386                    parameters: (formal_parameters
387                      "(" @context
388                      ")" @context)) @item"#,
389            )
390            .unwrap()
391            .with_context_provider(Some(Arc::new(BasicContextProvider))),
392        );
393        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
394        let worktree_id = project.update(cx, |project, cx| {
395            project.worktrees().next().unwrap().read(cx).id()
396        });
397        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
398
399        let buffer1 = workspace
400            .update(cx, |this, cx| {
401                this.project()
402                    .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
403            })
404            .await
405            .unwrap();
406        buffer1.update(cx, |this, cx| {
407            this.set_language(Some(typescript_language), cx)
408        });
409        let editor1 = cx.new_view(|cx| Editor::for_buffer(buffer1, Some(project.clone()), cx));
410        let buffer2 = workspace
411            .update(cx, |this, cx| {
412                this.project().update(cx, |this, cx| {
413                    this.open_buffer((worktree_id, "rust/b.rs"), cx)
414                })
415            })
416            .await
417            .unwrap();
418        buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
419        let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx));
420        workspace.update(cx, |this, cx| {
421            this.add_item_to_center(Box::new(editor1.clone()), cx);
422            this.add_item_to_center(Box::new(editor2.clone()), cx);
423            assert_eq!(this.active_item(cx).unwrap().item_id(), editor2.entity_id());
424            assert_eq!(
425                task_context(this, cx),
426                TaskContext {
427                    cwd: Some("/dir".into()),
428                    task_variables: TaskVariables::from_iter([
429                        (VariableName::File, "/dir/rust/b.rs".into()),
430                        (VariableName::WorktreeRoot, "/dir".into()),
431                        (VariableName::Row, "1".into()),
432                        (VariableName::Column, "1".into()),
433                    ])
434                }
435            );
436            // And now, let's select an identifier.
437            editor2.update(cx, |this, cx| {
438                this.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
439            });
440            assert_eq!(
441                task_context(this, cx),
442                TaskContext {
443                    cwd: Some("/dir".into()),
444                    task_variables: TaskVariables::from_iter([
445                        (VariableName::File, "/dir/rust/b.rs".into()),
446                        (VariableName::WorktreeRoot, "/dir".into()),
447                        (VariableName::Row, "1".into()),
448                        (VariableName::Column, "15".into()),
449                        (VariableName::SelectedText, "is_i".into()),
450                        (VariableName::Symbol, "this_is_a_rust_file".into()),
451                    ])
452                }
453            );
454
455            // Now, let's switch the active item to .ts file.
456            this.activate_item(&editor1, cx);
457            assert_eq!(
458                task_context(this, cx),
459                TaskContext {
460                    cwd: Some("/dir".into()),
461                    task_variables: TaskVariables::from_iter([
462                        (VariableName::File, "/dir/a.ts".into()),
463                        (VariableName::WorktreeRoot, "/dir".into()),
464                        (VariableName::Row, "1".into()),
465                        (VariableName::Column, "1".into()),
466                        (VariableName::Symbol, "this_is_a_test".into()),
467                    ])
468                }
469            );
470        });
471    }
472
473    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
474        cx.update(|cx| {
475            let state = AppState::test(cx);
476            file_icons::init((), cx);
477            language::init(cx);
478            crate::init(cx);
479            editor::init(cx);
480            workspace::init_settings(cx);
481            Project::init_settings(cx);
482            state
483        })
484    }
485}