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