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