lib.rs

  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}