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