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