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