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