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