diff --git a/crates/vim/src/projections.rs b/crates/vim/src/projections.rs index 26aa7ca0b7894df1594524c6265292f705092e09..14ab14ac348e42d4586186cf9461c50e6b124057 100644 --- a/crates/vim/src/projections.rs +++ b/crates/vim/src/projections.rs @@ -14,17 +14,24 @@ // - [ ] Implement `:a` to open alternate file // - [ ] Implement `:as` to open alternate file in split // - [ ] Implement `:av` to open alternate file in vertical split -// - [ ] Implement actually updating the state from the `projections.json` file +// - [X] Implement actually updating the state from the `projections.json` file // - [ ] Make this work with excerpts in multibuffers use crate::Vim; +use anyhow::Result; use editor::Editor; use gpui::Context; use gpui::Window; use gpui::actions; +use project::Fs; use project::ProjectItem; use project::ProjectPath; use regex::Regex; +use serde::Deserialize; +use settings::parse_json_with_comments; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; use util::rel_path::RelPath; #[derive(Debug)] @@ -33,6 +40,13 @@ struct Projection { target: String, } +#[derive(Deserialize, Debug)] +struct ProjectionValue { + alternate: String, +} + +type ProjectionsConfig = HashMap; + impl Projection { fn new(source: &str, target: &str) -> Self { // Replace the `*` character in the source string, if such a character @@ -50,8 +64,6 @@ impl Projection { } /// Determines whether the provided path matches this projection's source. - /// TODO!: We'll likely want to update this to use `ProjectPath` instead of - /// `&str`. fn matches(&self, path: &str) -> bool { self.source.is_match(path) } @@ -82,6 +94,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::open_projection); } +async fn load_projections(root_path: &Path, fs: Arc) -> Result { + let projections_path = root_path.join(".zed").join("projections.json"); + + let content = fs.load(&projections_path).await?; + let config = parse_json_with_comments::(&content)?; + + Ok(config) +} + impl Vim { pub fn open_projection( &mut self, @@ -89,44 +110,106 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - // Implementation for opening a projection - dbg!("[vim] attempting to open projection..."); self.update_editor(cx, |_vim, editor, cx| { - let project_path = editor + let current_file_path = editor .buffer() .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).project_path(cx)); - // User is editing an empty buffer, can't even find a projection. - if project_path.is_none() { + // User is editing an empty buffer, can't find a projection. + let Some(current_file_path) = current_file_path else { return; - } - - if let Some(project_path) = project_path - && let Some(workspace) = editor.workspace() - { - dbg!(&project_path); - if project_path.path.as_unix_str() - == "lib/phx_new_web/controllers/page_controller.ex" - { - dbg!("[vim] opening projection..."); - workspace - .update(cx, |workspace, cx| { - let worktree_id = project_path.worktree_id; - let mut project_path = ProjectPath::root_path(worktree_id); - project_path.path = RelPath::unix( - "test/phx_new_web/controllers/page_controller_test.exs", - ) - .unwrap() - .into_arc(); - dbg!(&project_path); - - workspace.open_path(project_path, None, true, window, cx) - }) - .detach(); - } - } + }; + + let Some(workspace) = editor.workspace() else { + return; + }; + + let Some(project) = editor.project() else { + return; + }; + + // Extract data we need before going async + let worktree_id = current_file_path.worktree_id; + let current_path = current_file_path.path.clone(); + let fs = project.read(cx).fs().clone(); + + // Get the worktree to extract its root path + let worktree = project.read(cx).worktree_for_id(worktree_id, cx); + + let Some(worktree) = worktree else { + return; + }; + + let root_path = worktree.read(cx).abs_path(); + + workspace.update(cx, |_workspace, cx| { + cx.spawn_in(window, async move |workspace, cx| { + // Load the projections configuration + let config = match load_projections(&root_path, fs).await { + Ok(config) => { + log::info!("Loaded projections config: {:?}", config); + config + } + Err(err) => { + log::warn!("Failed to load projections: {:?}", err); + return; + } + }; + + // Convert config to Projection instances and find a match + let current_path_str = current_path.as_unix_str(); + log::info!("Looking for projection for path: {}", current_path_str); + let mut alternate_path: Option = None; + + for (source_pattern, projection_value) in config.iter() { + log::debug!( + "Trying pattern '{}' -> '{}'", + source_pattern, + projection_value.alternate + ); + let projection = + Projection::new(source_pattern, &projection_value.alternate); + + if projection.matches(current_path_str) { + let alt = projection.alternate(current_path_str); + log::info!("Found match! Alternate path: {}", alt); + alternate_path = Some(alt); + break; + } + } + + // If we found an alternate, open it + if let Some(alternate_path) = alternate_path { + let alternate_rel_path = match RelPath::unix(&alternate_path) { + Ok(path) => path, + Err(_) => return, + }; + + let alternate_project_path = ProjectPath { + worktree_id, + path: alternate_rel_path.into_arc(), + }; + + let result = workspace.update_in(cx, |workspace, window, cx| { + workspace.open_path(alternate_project_path, None, true, window, cx) + }); + + match result { + Ok(task) => { + task.detach(); + } + Err(err) => { + log::error!("Failed to open alternate file: {:?}", err); + } + } + } else { + log::info!("No alternate projection found for: {}", current_path_str); + } + }) + .detach(); + }); }); } } @@ -134,7 +217,13 @@ impl Vim { #[cfg(test)] mod tests { use super::Projection; + use super::load_projections; use gpui::TestAppContext; + use project::FakeFs; + use project::Project; + use serde_json::json; + use settings::SettingsStore; + use util::path; #[gpui::test] async fn test_matches(_cx: &mut TestAppContext) { @@ -158,4 +247,42 @@ mod tests { let path = "lib/app/module.ex"; assert_eq!(projection.alternate(path), "test/app/module_test.exs"); } + + #[gpui::test] + async fn test_load_projections(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + ".zed": { + "projections.json": r#"{ + "src/main/java/*.java": {"alternate": "src/test/java/{}.java"}, + "src/test/java/*.java": {"alternate": "src/main/java/{}.java"} + }"# + } + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let worktree = project.read_with(cx, |project, _cx| project.worktrees(_cx).next().unwrap()); + + let root_path = worktree.read_with(cx, |wt, _| wt.abs_path()); + let config = load_projections(&root_path, fs).await.unwrap(); + + assert_eq!(config.len(), 2); + assert_eq!( + config.get("src/main/java/*.java").unwrap().alternate, + "src/test/java/{}.java" + ); + assert_eq!( + config.get("src/test/java/*.java").unwrap().alternate, + "src/main/java/{}.java" + ); + } }