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