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}