diff --git a/Cargo.lock b/Cargo.lock index 120669f037..a0daee9305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5523,6 +5523,7 @@ version = "0.1.0" dependencies = [ "anyhow", "fs", + "futures 0.3.28", "gpui", "language", ] diff --git a/assets/settings/default.json b/assets/settings/default.json index cc724657c0..be47ac9c8c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -199,7 +199,8 @@ // "arguments": ["--stdin-filepath", "{buffer_path}"] // } // } - "formatter": "language_server", + // TODO kb description + "formatter": "auto", // How to soft-wrap long lines of text. This setting can take // three values: // diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 1d95db9b6c..bb5d6387e0 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -85,7 +85,7 @@ pub struct RemoveOptions { pub ignore_if_not_exists: bool, } -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct Metadata { pub inode: u64, pub mtime: SystemTime, @@ -229,11 +229,12 @@ impl Fs for RealFs { } else { symlink_metadata }; + let file_type_metadata = metadata.file_type(); Ok(Some(Metadata { inode: metadata.ino(), mtime: metadata.modified().unwrap(), is_symlink, - is_dir: metadata.file_type().is_dir(), + is_dir: file_type_metadata.is_dir(), })) } diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml index c2e4b2a3cc..821cde7b3a 100644 --- a/crates/prettier/Cargo.toml +++ b/crates/prettier/Cargo.toml @@ -12,7 +12,7 @@ gpui = { path = "../gpui" } fs = { path = "../fs" } anyhow.workspace = true - +futures.workspace = true [dev-dependencies] language = { path = "../language", features = ["test-support"] } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index ecd4376477..a38f8f8651 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -1,6 +1,8 @@ +use std::collections::VecDeque; pub use std::path::{Path, PathBuf}; pub use std::sync::Arc; +use anyhow::Context; use fs::Fs; use gpui::ModelHandle; use language::{Buffer, Diff}; @@ -11,6 +13,12 @@ pub struct Prettier { pub struct NodeRuntime; +#[derive(Debug)] +pub struct LocateStart { + pub worktree_root_path: Arc, + pub starting_path: Arc, +} + impl Prettier { // This was taken from the prettier-vscode extension. pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ @@ -28,8 +36,107 @@ impl Prettier { ".editorconfig", ]; - pub async fn locate(starting_path: Option<&Path>, fs: Arc) -> PathBuf { - todo!() + pub async fn locate( + starting_path: Option, + fs: Arc, + ) -> anyhow::Result { + let paths_to_check = match starting_path { + Some(starting_path) => { + let worktree_root = starting_path + .worktree_root_path + .components() + .into_iter() + .take_while(|path_component| { + path_component.as_os_str().to_str() != Some("node_modules") + }) + .collect::(); + + if worktree_root != starting_path.worktree_root_path.as_ref() { + vec![worktree_root] + } else { + let (worktree_root_metadata, start_path_metadata) = if starting_path + .starting_path + .as_ref() + == Path::new("") + { + let worktree_root_data = + fs.metadata(&worktree_root).await.with_context(|| { + format!( + "FS metadata fetch for worktree root path {worktree_root:?}", + ) + })?; + (worktree_root_data.unwrap_or_else(|| { + panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}") + }), None) + } else { + let full_starting_path = worktree_root.join(&starting_path.starting_path); + let (worktree_root_data, start_path_data) = futures::try_join!( + fs.metadata(&worktree_root), + fs.metadata(&full_starting_path), + ) + .with_context(|| { + format!("FS metadata fetch for starting path {full_starting_path:?}",) + })?; + ( + worktree_root_data.unwrap_or_else(|| { + panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}") + }), + start_path_data, + ) + }; + + match start_path_metadata { + Some(start_path_metadata) => { + anyhow::ensure!(worktree_root_metadata.is_dir, + "For non-empty start path, worktree root {starting_path:?} should be a directory"); + anyhow::ensure!( + !start_path_metadata.is_dir, + "For non-empty start path, it should not be a directory {starting_path:?}" + ); + anyhow::ensure!( + !start_path_metadata.is_symlink, + "For non-empty start path, it should not be a symlink {starting_path:?}" + ); + + let file_to_format = starting_path.starting_path.as_ref(); + let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]); + let mut current_path = worktree_root; + for path_component in file_to_format.components().into_iter() { + current_path = current_path.join(path_component); + paths_to_check.push_front(current_path.clone()); + if path_component.as_os_str().to_str() == Some("node_modules") { + break; + } + } + paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it + Vec::from(paths_to_check) + } + None => { + anyhow::ensure!( + !worktree_root_metadata.is_dir, + "For empty start path, worktree root should not be a directory {starting_path:?}" + ); + anyhow::ensure!( + !worktree_root_metadata.is_symlink, + "For empty start path, worktree root should not be a symlink {starting_path:?}" + ); + worktree_root + .parent() + .map(|path| vec![path.to_path_buf()]) + .unwrap_or_default() + } + } + } + } + None => Vec::new(), + }; + + if dbg!(paths_to_check).is_empty() { + // TODO kb return the default prettier, how, without state? + } else { + // TODO kb now check all paths to check for prettier + } + Ok(PathBuf::new()) } pub async fn start(prettier_path: &Path, node: Arc) -> anyhow::Result { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d24fe17380..6538a26540 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -50,7 +50,7 @@ use lsp::{ }; use lsp_command::*; use postage::watch; -use prettier::{NodeRuntime, Prettier}; +use prettier::{LocateStart, NodeRuntime, Prettier}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -8189,14 +8189,61 @@ impl Project { ) -> Option, Arc>>>>> { let buffer_file = File::from_dyn(buffer.read(cx).file()); let buffer_path = buffer_file.map(|file| Arc::clone(file.path())); + let worktree_path = buffer_file + .as_ref() + .map(|file| file.worktree.read(cx).abs_path()); let worktree_id = buffer_file.map(|file| file.worktree_id(cx)); // TODO kb return None if config opted out of prettier + if true { + let fs = Arc::clone(&self.fs); + let buffer_path = buffer_path.clone(); + let worktree_path = worktree_path.clone(); + cx.spawn(|_, _| async move { + let prettier_path = Prettier::locate( + worktree_path + .zip(buffer_path) + .map(|(worktree_root_path, starting_path)| { + dbg!(LocateStart { + worktree_root_path, + starting_path, + }) + }), + fs, + ) + .await + .unwrap(); + dbg!(prettier_path); + }) + .detach(); + return None; + } let task = cx.spawn(|this, mut cx| async move { let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs)); // TODO kb can we have a cache for this instead? - let prettier_path = Prettier::locate(buffer_path.as_deref(), fs).await; + let prettier_path = match cx + .background() + .spawn(Prettier::locate( + worktree_path + .zip(buffer_path) + .map(|(worktree_root_path, starting_path)| LocateStart { + worktree_root_path, + starting_path, + }), + fs, + )) + .await + { + Ok(path) => path, + Err(e) => { + return Task::Ready(Some(Result::Err(Arc::new( + e.context("determining prettier path for worktree {worktree_path:?}"), + )))) + .shared(); + } + }; + if let Some(existing_prettier) = this.update(&mut cx, |project, _| { project .prettier_instances