Add prettier tests infrastructure

Kirill Bulatov created

Change summary

crates/editor/src/editor_tests.rs       | 66 ++++++++++++++++++++++++++
crates/language/src/language.rs         |  6 ++
crates/node_runtime/src/node_runtime.rs | 25 +++++++--
crates/prettier/Cargo.toml              |  3 +
crates/prettier/src/prettier.rs         | 54 ++++++++++++++++++---
crates/project/Cargo.toml               |  2 
6 files changed, 139 insertions(+), 17 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -19,8 +19,8 @@ use gpui::{
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
-    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
-    Override, Point,
+    BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
+    LanguageRegistry, Override, Point,
 };
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -7815,6 +7815,68 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
     });
 }
 
+#[gpui::test]
+async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
+    });
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+
+    let _ = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            enabled_formatters: vec![BundledFormatter::Prettier {
+                parser_name: Some("test_parser"),
+                plugin_names: vec!["test_plugin"],
+            }],
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    // TODO kb have to specify some test node runtime
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+    });
+    format.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project, FormatTrigger::Manual, cx)
+    });
+    cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+    cx.foreground().start_waiting();
+    format.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/language/src/language.rs 🔗

@@ -498,6 +498,7 @@ pub struct FakeLspAdapter {
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub disk_based_diagnostics_sources: Vec<String>,
+    pub enabled_formatters: Vec<BundledFormatter>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -1760,6 +1761,7 @@ impl Default for FakeLspAdapter {
             disk_based_diagnostics_progress_token: None,
             initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
+            enabled_formatters: Vec::new(),
         }
     }
 }
@@ -1816,6 +1818,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
     async fn initialization_options(&self) -> Option<Value> {
         self.initialization_options.clone()
     }
+
+    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+        self.enabled_formatters.clone()
+    }
 }
 
 fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

crates/node_runtime/src/node_runtime.rs 🔗

@@ -230,19 +230,32 @@ impl FakeNodeRuntime {
 
 #[async_trait::async_trait]
 impl NodeRuntime for FakeNodeRuntime {
-    async fn binary_path(&self) -> Result<PathBuf> {
-        unreachable!()
+    async fn binary_path(&self) -> anyhow::Result<PathBuf> {
+        // TODO kb move away into a separate type + a Project's setter (for test code)
+        Ok(PathBuf::from("prettier_fake_node"))
     }
 
     async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
         unreachable!()
     }
 
-    async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
-        unreachable!()
+    async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
+        if name == "prettier" {
+            Ok("0.0.1".to_string())
+        } else {
+            unreachable!()
+        }
     }
 
-    async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> {
-        unreachable!()
+    async fn npm_install_packages(
+        &self,
+        _: &Path,
+        packages: &[(&str, &str)],
+    ) -> anyhow::Result<()> {
+        if packages == [("prettier", "0.0.1")] {
+            Ok(())
+        } else {
+            unreachable!()
+        }
     }
 }

crates/prettier/Cargo.toml 🔗

@@ -6,6 +6,9 @@ edition = "2021"
 [lib]
 path = "src/prettier.rs"
 
+[features]
+test-support = []
+
 [dependencies]
 client = { path = "../client" }
 collections = { path = "../collections"}

crates/prettier/src/prettier.rs 🔗

@@ -11,7 +11,7 @@ use language::language_settings::language_settings;
 use language::proto::deserialize_diff;
 use language::{Buffer, BundledFormatter, Diff};
 use lsp::request::Request;
-use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
+use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use util::paths::DEFAULT_PRETTIER_DIR;
@@ -19,6 +19,8 @@ use util::paths::DEFAULT_PRETTIER_DIR;
 pub enum Prettier {
     Local(Local),
     Remote(Remote),
+    #[cfg(any(test, feature = "test-support"))]
+    Test(TestPrettier),
 }
 
 pub struct Local {
@@ -35,6 +37,13 @@ pub struct Remote {
     client: Arc<Client>,
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub struct TestPrettier {
+    worktree_id: Option<usize>,
+    prettier_dir: PathBuf,
+    default: bool,
+}
+
 #[derive(Debug)]
 pub struct LocateStart {
     pub worktree_root_path: Arc<Path>,
@@ -180,6 +189,22 @@ impl Prettier {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub async fn start(
+        worktree_id: Option<usize>,
+        _: LanguageServerId,
+        prettier_dir: PathBuf,
+        _: Arc<dyn NodeRuntime>,
+        _: AsyncAppContext,
+    ) -> anyhow::Result<Self> {
+        Ok(Self::Test(TestPrettier {
+            worktree_id,
+            default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
+            prettier_dir,
+        }))
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
     pub async fn start(
         worktree_id: Option<usize>,
         server_id: LanguageServerId,
@@ -400,6 +425,12 @@ impl Prettier {
                 .diff
                 .map(deserialize_diff)
                 .context("missing diff after prettier diff invocation"),
+            Self::Test(_) => Ok(buffer
+                .read_with(cx, |buffer, cx| {
+                    let formatted_text = buffer.text() + "\nformatted by test prettier";
+                    buffer.diff(formatted_text, cx)
+                })
+                .await),
         }
     }
 
@@ -427,34 +458,39 @@ impl Prettier {
                     )
                 })
                 .context("prettier invoke clear cache"),
+            Self::Test(_) => Ok(()),
         }
     }
 
     pub fn server(&self) -> Option<&Arc<LanguageServer>> {
         match self {
-            Prettier::Local(local) => Some(&local.server),
-            Prettier::Remote(_) => None,
+            Self::Local(local) => Some(&local.server),
+            Self::Remote(_) => None,
+            Self::Test(_) => None,
         }
     }
 
     pub fn is_default(&self) -> bool {
         match self {
-            Prettier::Local(local) => local.default,
-            Prettier::Remote(_) => false,
+            Self::Local(local) => local.default,
+            Self::Remote(_) => false,
+            Self::Test(test_prettier) => test_prettier.default,
         }
     }
 
     pub fn prettier_dir(&self) -> &Path {
         match self {
-            Prettier::Local(local) => &local.prettier_dir,
-            Prettier::Remote(remote) => &remote.prettier_dir,
+            Self::Local(local) => &local.prettier_dir,
+            Self::Remote(remote) => &remote.prettier_dir,
+            Self::Test(test_prettier) => &test_prettier.prettier_dir,
         }
     }
 
     pub fn worktree_id(&self) -> Option<usize> {
         match self {
-            Prettier::Local(local) => local.worktree_id,
-            Prettier::Remote(remote) => remote.worktree_id,
+            Self::Local(local) => local.worktree_id,
+            Self::Remote(remote) => remote.worktree_id,
+            Self::Test(test_prettier) => test_prettier.worktree_id,
         }
     }
 }

crates/project/Cargo.toml 🔗

@@ -15,6 +15,7 @@ test-support = [
     "language/test-support",
     "settings/test-support",
     "text/test-support",
+    "prettier/test-support",
 ]
 
 [dependencies]
@@ -75,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
+prettier = { path = "../prettier", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 git2.workspace = true