Prepare prettier file lookup code infra

Kirill Bulatov created

Change summary

Cargo.lock                      |   1 
assets/settings/default.json    |   3 
crates/fs/src/fs.rs             |   5 
crates/prettier/Cargo.toml      |   2 
crates/prettier/src/prettier.rs | 111 ++++++++++++++++++++++++++++++++++
crates/project/src/project.rs   |  51 +++++++++++++++
6 files changed, 165 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5523,6 +5523,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "fs",
+ "futures 0.3.28",
  "gpui",
  "language",
 ]

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:
   //

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(),
         }))
     }
 

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"] }

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<Path>,
+    pub starting_path: Arc<Path>,
+}
+
 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<dyn Fs>) -> PathBuf {
-        todo!()
+    pub async fn locate(
+        starting_path: Option<LocateStart>,
+        fs: Arc<dyn Fs>,
+    ) -> anyhow::Result<PathBuf> {
+        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::<PathBuf>();
+
+                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<NodeRuntime>) -> anyhow::Result<Self> {

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<Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
         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