1use ::settings::Settings;
2use editor::{tasks::task_context, Editor};
3use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
4use modal::TasksModal;
5use project::{Location, WorktreeId};
6use task::TaskId;
7use workspace::tasks::schedule_task;
8use workspace::{tasks::schedule_resolved_task, Workspace};
9
10mod modal;
11mod settings;
12
13pub use modal::{Rerun, Spawn};
14use zed_actions::TaskSpawnTarget;
15
16pub fn init(cx: &mut AppContext) {
17 settings::TaskSettings::register(cx);
18 cx.observe_new_views(
19 |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
20 workspace
21 .register_action(spawn_task_or_modal)
22 .register_action(move |workspace, action: &modal::Rerun, cx| {
23 if let Some((task_source_kind, mut last_scheduled_task)) = workspace
24 .project()
25 .read(cx)
26 .task_store()
27 .read(cx)
28 .task_inventory()
29 .and_then(|inventory| {
30 inventory.read(cx).last_scheduled_task(
31 action
32 .task_id
33 .as_ref()
34 .map(|id| TaskId(id.clone()))
35 .as_ref(),
36 )
37 })
38 {
39 if action.reevaluate_context {
40 let mut original_task = last_scheduled_task.original_task().clone();
41 if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
42 original_task.allow_concurrent_runs = allow_concurrent_runs;
43 }
44 if let Some(use_new_terminal) = action.use_new_terminal {
45 original_task.use_new_terminal = use_new_terminal;
46 }
47 let context_task = task_context(workspace, cx);
48 cx.spawn(|workspace, mut cx| async move {
49 let task_context = context_task.await;
50 workspace
51 .update(&mut cx, |workspace, cx| {
52 schedule_task(
53 workspace,
54 task_source_kind,
55 &original_task,
56 &task_context,
57 Default::default(),
58 false,
59 cx,
60 )
61 })
62 .ok()
63 })
64 .detach()
65 } else {
66 if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
67 if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
68 resolved.allow_concurrent_runs = allow_concurrent_runs;
69 }
70 if let Some(use_new_terminal) = action.use_new_terminal {
71 resolved.use_new_terminal = use_new_terminal;
72 }
73 }
74
75 schedule_resolved_task(
76 workspace,
77 task_source_kind,
78 last_scheduled_task,
79 false,
80 cx,
81 );
82 }
83 } else {
84 toggle_modal(workspace, cx).detach();
85 };
86 });
87 },
88 )
89 .detach();
90}
91
92fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
93 match &action.task_name {
94 Some(name) => spawn_task_with_name(name.clone(), action.target.unwrap_or_default(), cx)
95 .detach_and_log_err(cx),
96 None => toggle_modal(workspace, cx).detach(),
97 }
98}
99
100fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
101 let task_store = workspace.project().read(cx).task_store().clone();
102 let workspace_handle = workspace.weak_handle();
103 let can_open_modal = workspace.project().update(cx, |project, cx| {
104 project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
105 });
106 if can_open_modal {
107 let context_task = task_context(workspace, cx);
108 cx.spawn(|workspace, mut cx| async move {
109 let task_context = context_task.await;
110 workspace
111 .update(&mut cx, |workspace, cx| {
112 workspace.toggle_modal(cx, |cx| {
113 TasksModal::new(task_store.clone(), task_context, workspace_handle, cx)
114 })
115 })
116 .ok();
117 })
118 } else {
119 AsyncTask::ready(())
120 }
121}
122
123fn spawn_task_with_name(
124 name: String,
125 task_target: TaskSpawnTarget,
126 cx: &mut ViewContext<Workspace>,
127) -> AsyncTask<anyhow::Result<()>> {
128 cx.spawn(|workspace, mut cx| async move {
129 let context_task =
130 workspace.update(&mut cx, |workspace, cx| task_context(workspace, cx))?;
131 let task_context = context_task.await;
132 let tasks = workspace.update(&mut cx, |workspace, cx| {
133 let Some(task_inventory) = workspace
134 .project()
135 .read(cx)
136 .task_store()
137 .read(cx)
138 .task_inventory()
139 .cloned()
140 else {
141 return Vec::new();
142 };
143 let (worktree, location) = active_item_selection_properties(workspace, cx);
144 let (file, language) = location
145 .map(|location| {
146 let buffer = location.buffer.read(cx);
147 (
148 buffer.file().cloned(),
149 buffer.language_at(location.range.start),
150 )
151 })
152 .unwrap_or_default();
153 task_inventory
154 .read(cx)
155 .list_tasks(file, language, worktree, cx)
156 })?;
157
158 let did_spawn = workspace
159 .update(&mut cx, |workspace, cx| {
160 let (task_source_kind, target_task) =
161 tasks.into_iter().find(|(_, task)| task.label == name)?;
162 schedule_task(
163 workspace,
164 task_source_kind,
165 &target_task,
166 &task_context,
167 task_target,
168 false,
169 cx,
170 );
171 Some(())
172 })?
173 .is_some();
174 if !did_spawn {
175 workspace
176 .update(&mut cx, |workspace, cx| {
177 spawn_task_or_modal(workspace, &Spawn::default(), cx);
178 })
179 .ok();
180 }
181
182 Ok(())
183 })
184}
185
186fn active_item_selection_properties(
187 workspace: &Workspace,
188 cx: &mut WindowContext,
189) -> (Option<WorktreeId>, Option<Location>) {
190 let active_item = workspace.active_item(cx);
191 let worktree_id = active_item
192 .as_ref()
193 .and_then(|item| item.project_path(cx))
194 .map(|path| path.worktree_id);
195 let location = active_item
196 .and_then(|active_item| active_item.act_as::<Editor>(cx))
197 .and_then(|editor| {
198 editor.update(cx, |editor, cx| {
199 let selection = editor.selections.newest_anchor();
200 let multi_buffer = editor.buffer().clone();
201 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
202 let (buffer_snapshot, buffer_offset) =
203 multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
204 let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
205 let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
206 Some(Location {
207 buffer,
208 range: buffer_anchor..buffer_anchor,
209 })
210 })
211 });
212 (worktree_id, location)
213}
214
215#[cfg(test)]
216mod tests {
217 use std::{collections::HashMap, sync::Arc};
218
219 use editor::Editor;
220 use gpui::{Entity, TestAppContext};
221 use language::{Language, LanguageConfig};
222 use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project};
223 use serde_json::json;
224 use task::{TaskContext, TaskVariables, VariableName};
225 use ui::VisualContext;
226 use workspace::{AppState, Workspace};
227
228 use crate::task_context;
229
230 #[gpui::test]
231 async fn test_default_language_context(cx: &mut TestAppContext) {
232 init_test(cx);
233 let fs = FakeFs::new(cx.executor());
234 fs.insert_tree(
235 "/dir",
236 json!({
237 ".zed": {
238 "tasks.json": r#"[
239 {
240 "label": "example task",
241 "command": "echo",
242 "args": ["4"]
243 },
244 {
245 "label": "another one",
246 "command": "echo",
247 "args": ["55"]
248 },
249 ]"#,
250 },
251 "a.ts": "function this_is_a_test() { }",
252 "rust": {
253 "b.rs": "use std; fn this_is_a_rust_file() { }",
254 }
255
256 }),
257 )
258 .await;
259 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
260 let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
261 let rust_language = Arc::new(
262 Language::new(
263 LanguageConfig::default(),
264 Some(tree_sitter_rust::LANGUAGE.into()),
265 )
266 .with_outline_query(
267 r#"(function_item
268 "fn" @context
269 name: (_) @name) @item"#,
270 )
271 .unwrap()
272 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
273 worktree_store.clone(),
274 )))),
275 );
276
277 let typescript_language = Arc::new(
278 Language::new(
279 LanguageConfig::default(),
280 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
281 )
282 .with_outline_query(
283 r#"(function_declaration
284 "async"? @context
285 "function" @context
286 name: (_) @name
287 parameters: (formal_parameters
288 "(" @context
289 ")" @context)) @item"#,
290 )
291 .unwrap()
292 .with_context_provider(Some(Arc::new(BasicContextProvider::new(
293 worktree_store.clone(),
294 )))),
295 );
296
297 let worktree_id = project.update(cx, |project, cx| {
298 project.worktrees(cx).next().unwrap().read(cx).id()
299 });
300 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
301
302 let buffer1 = workspace
303 .update(cx, |this, cx| {
304 this.project()
305 .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
306 })
307 .await
308 .unwrap();
309 buffer1.update(cx, |this, cx| {
310 this.set_language(Some(typescript_language), cx)
311 });
312 let editor1 = cx.new_view(|cx| Editor::for_buffer(buffer1, Some(project.clone()), cx));
313 let buffer2 = workspace
314 .update(cx, |this, cx| {
315 this.project().update(cx, |this, cx| {
316 this.open_buffer((worktree_id, "rust/b.rs"), cx)
317 })
318 })
319 .await
320 .unwrap();
321 buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
322 let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx));
323
324 let first_context = workspace
325 .update(cx, |workspace, cx| {
326 workspace.add_item_to_center(Box::new(editor1.clone()), cx);
327 workspace.add_item_to_center(Box::new(editor2.clone()), cx);
328 assert_eq!(
329 workspace.active_item(cx).unwrap().item_id(),
330 editor2.entity_id()
331 );
332 task_context(workspace, cx)
333 })
334 .await;
335 assert_eq!(
336 first_context,
337 TaskContext {
338 cwd: Some("/dir".into()),
339 task_variables: TaskVariables::from_iter([
340 (VariableName::File, "/dir/rust/b.rs".into()),
341 (VariableName::Filename, "b.rs".into()),
342 (VariableName::RelativeFile, "rust/b.rs".into()),
343 (VariableName::Dirname, "/dir/rust".into()),
344 (VariableName::Stem, "b".into()),
345 (VariableName::WorktreeRoot, "/dir".into()),
346 (VariableName::Row, "1".into()),
347 (VariableName::Column, "1".into()),
348 ]),
349 project_env: HashMap::default(),
350 }
351 );
352
353 // And now, let's select an identifier.
354 editor2.update(cx, |editor, cx| {
355 editor.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
356 });
357
358 assert_eq!(
359 workspace
360 .update(cx, |workspace, cx| { task_context(workspace, cx) })
361 .await,
362 TaskContext {
363 cwd: Some("/dir".into()),
364 task_variables: TaskVariables::from_iter([
365 (VariableName::File, "/dir/rust/b.rs".into()),
366 (VariableName::Filename, "b.rs".into()),
367 (VariableName::RelativeFile, "rust/b.rs".into()),
368 (VariableName::Dirname, "/dir/rust".into()),
369 (VariableName::Stem, "b".into()),
370 (VariableName::WorktreeRoot, "/dir".into()),
371 (VariableName::Row, "1".into()),
372 (VariableName::Column, "15".into()),
373 (VariableName::SelectedText, "is_i".into()),
374 (VariableName::Symbol, "this_is_a_rust_file".into()),
375 ]),
376 project_env: HashMap::default(),
377 }
378 );
379
380 assert_eq!(
381 workspace
382 .update(cx, |workspace, cx| {
383 // Now, let's switch the active item to .ts file.
384 workspace.activate_item(&editor1, true, true, cx);
385 task_context(workspace, cx)
386 })
387 .await,
388 TaskContext {
389 cwd: Some("/dir".into()),
390 task_variables: TaskVariables::from_iter([
391 (VariableName::File, "/dir/a.ts".into()),
392 (VariableName::Filename, "a.ts".into()),
393 (VariableName::RelativeFile, "a.ts".into()),
394 (VariableName::Dirname, "/dir".into()),
395 (VariableName::Stem, "a".into()),
396 (VariableName::WorktreeRoot, "/dir".into()),
397 (VariableName::Row, "1".into()),
398 (VariableName::Column, "1".into()),
399 (VariableName::Symbol, "this_is_a_test".into()),
400 ]),
401 project_env: HashMap::default(),
402 }
403 );
404 }
405
406 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
407 cx.update(|cx| {
408 let state = AppState::test(cx);
409 file_icons::init((), cx);
410 language::init(cx);
411 crate::init(cx);
412 editor::init(cx);
413 workspace::init_settings(cx);
414 Project::init_settings(cx);
415 TaskStore::init(None);
416 state
417 })
418 }
419}