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