diff --git a/Cargo.lock b/Cargo.lock index 72ee771f5d..f95db3d354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1501,6 +1501,7 @@ dependencies = [ "log", "lsp", "nanoid", + "node_runtime", "parking_lot 0.11.2", "pretty_assertions", "project", @@ -5517,6 +5518,26 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettier" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "fs", + "futures 0.3.28", + "gpui", + "language", + "log", + "lsp", + "node_runtime", + "serde", + "serde_derive", + "serde_json", + "util", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -5629,8 +5650,10 @@ dependencies = [ "lazy_static", "log", "lsp", + "node_runtime", "parking_lot 0.11.2", "postage", + "prettier", "pretty_assertions", "rand 0.8.5", "regex", @@ -9986,6 +10009,7 @@ dependencies = [ "lazy_static", "log", "menu", + "node_runtime", "parking_lot 0.11.2", "postage", "project", diff --git a/Cargo.toml b/Cargo.toml index 7dae3bd81f..25aec39cdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "crates/plugin", "crates/plugin_macros", "crates/plugin_runtime", + "crates/prettier", "crates/project", "crates/project_panel", "crates/project_symbols", diff --git a/assets/settings/default.json b/assets/settings/default.json index cc724657c0..1611d80e2f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -199,7 +199,12 @@ // "arguments": ["--stdin-filepath", "{buffer_path}"] // } // } - "formatter": "language_server", + // 3. Format code using Zed's Prettier integration: + // "formatter": "prettier" + // 4. Default. Format files using Zed's Prettier integration (if applicable), + // or falling back to formatting via language server: + // "formatter": "auto" + "formatter": "auto", // How to soft-wrap long lines of text. This setting can take // three values: // @@ -429,6 +434,16 @@ "tab_size": 2 } }, + // Zed's Prettier integration settings. + // If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if + // project has no other Prettier installed. + "prettier": { + // Use regular Prettier json configuration: + // "trailingComma": "es5", + // "tabWidth": 4, + // "semi": false, + // "singleQuote": true + }, // LSP Specific settings. "lsp": { // Specify the LSP name as a key here. diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8fd1cd4380..b91f0e1a5f 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] } git = { path = "../git", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } +node_runtime = { path = "../node_runtime" } project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 4008a941dd..d6d449fd47 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -15,12 +15,14 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, - tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, + tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter, + Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; -use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; +use project::{ + search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, +}; use rand::prelude::*; use serde_json::json; use settings::SettingsStore; @@ -4407,8 +4409,6 @@ async fn test_formatting_buffer( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - use project::FormatTrigger; - let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -4511,6 +4511,134 @@ async fn test_formatting_buffer( ); } +#[gpui::test(iterations = 10)] +async fn test_prettier_formatting_buffer( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let test_plugin = "test_plugin"; + let mut fake_language_servers = 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 language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + + // Here we insert a fake tree with a directory that exists on disk. This is needed + // because later we'll invoke a command, which requires passing a working directory + // that points to a valid location on disk. + let directory = env::current_dir().unwrap(); + let buffer_text = "let one = \"two\""; + client_a + .fs() + .insert_tree(&directory, json!({ "a.rs": buffer_text })) + .await; + let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; + let prettier_format_suffix = project_a.update(cx_a, |project, _| { + let suffix = project.enable_test_prettier(&[test_plugin]); + project.languages().add(language); + suffix + }); + let buffer_a = cx_a + .background() + .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let buffer_b = cx_b + .background() + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .await + .unwrap(); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |file| { + file.defaults.formatter = Some(Formatter::Auto); + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |file| { + file.defaults.formatter = Some(Formatter::LanguageServer); + }); + }); + }); + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.handle_request::(|_, _| async move { + panic!( + "Unexpected: prettier should be preferred since it's enabled and language supports it" + ) + }); + + project_b + .update(cx_b, |project, cx| { + project.format( + HashSet::from_iter([buffer_b.clone()]), + true, + FormatTrigger::Save, + cx, + ) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.text()), + buffer_text.to_string() + "\n" + prettier_format_suffix, + "Prettier formatting was not applied to client buffer after client's request" + ); + + project_a + .update(cx_a, |project, cx| { + project.format( + HashSet::from_iter([buffer_a.clone()]), + true, + FormatTrigger::Manual, + cx, + ) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.text()), + buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix, + "Prettier formatting was not applied to client buffer after host's request" + ); +} + #[gpui::test(iterations = 10)] async fn test_definition( deterministic: Arc, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 2e13874125..7397489b34 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -15,6 +15,7 @@ use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; use language::LanguageRegistry; +use node_runtime::FakeNodeRuntime; use parking_lot::Mutex; use project::{Project, WorktreeId}; use rpc::RECEIVE_TIMEOUT; @@ -218,6 +219,7 @@ impl TestServer { build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], + node_runtime: FakeNodeRuntime::new(), }); cx.update(|cx| { @@ -567,6 +569,7 @@ impl TestClient { cx.update(|cx| { Project::local( self.client().clone(), + self.app_state.node_runtime.clone(), self.app_state.user_store.clone(), self.app_state.languages.clone(), self.app_state.fs.clone(), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dc723c7012..c68f72d16f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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}; @@ -5076,7 +5076,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) + }); let mut language = Language::new( LanguageConfig { @@ -5092,6 +5094,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { document_formatting_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, + // Enable Prettier formatting for the same buffer, and ensure + // LSP is called instead of Prettier. + enabled_formatters: vec![BundledFormatter::Prettier { + parser_name: Some("test_parser"), + plugin_names: Vec::new(), + }], ..Default::default() })) .await; @@ -5100,7 +5108,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + project.update(cx, |project, _| { + project.enable_test_prettier(&[]); + project.languages().add(Arc::new(language)); + }); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await @@ -5218,7 +5229,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { @@ -7815,6 +7828,75 @@ 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 test_plugin = "test_plugin"; + 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; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let prettier_format_suffix = project.update(cx, |project, _| { + let suffix = project.enable_test_prettier(&[test_plugin]); + project.languages().add(Arc::new(language)); + suffix + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + let buffer_text = "one\ntwo\nthree\n"; + 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(buffer_text, 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)), + buffer_text.to_string() + prettier_format_suffix, + "Test prettier formatting was not applied to the original buffer text", + ); + + update_test_language_settings(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + 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)), + buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, + "Autoformatting (via test prettier) was not applied to the original buffer text", + ); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 1d95db9b6c..1bc8fa9a24 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, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7d113a88af..bd389652a0 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -227,6 +227,10 @@ impl CachedLspAdapter { ) -> Option { self.adapter.label_for_symbol(name, kind, language).await } + + pub fn enabled_formatters(&self) -> Vec { + self.adapter.enabled_formatters() + } } pub trait LspAdapterDelegate: Send + Sync { @@ -333,6 +337,33 @@ pub trait LspAdapter: 'static + Send + Sync { async fn language_ids(&self) -> HashMap { Default::default() } + + fn enabled_formatters(&self) -> Vec { + Vec::new() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BundledFormatter { + Prettier { + // See https://prettier.io/docs/en/options.html#parser for a list of valid values. + // Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used. + // There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins. + // + // But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed. + // For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict. + parser_name: Option<&'static str>, + plugin_names: Vec<&'static str>, + }, +} + +impl BundledFormatter { + pub fn prettier(parser_name: &'static str) -> Self { + Self::Prettier { + parser_name: Some(parser_name), + plugin_names: Vec::new(), + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -467,6 +498,7 @@ pub struct FakeLspAdapter { pub initializer: Option>, pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, + pub enabled_formatters: Vec, } #[derive(Clone, Debug, Default)] @@ -1729,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(), } } } @@ -1785,6 +1818,10 @@ impl LspAdapter for Arc { async fn initialization_options(&self) -> Option { self.initialization_options.clone() } + + fn enabled_formatters(&self) -> Vec { + self.enabled_formatters.clone() + } } fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option)]) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c3f706802a..9cac5c523e 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -50,6 +50,7 @@ pub struct LanguageSettings { pub remove_trailing_whitespace_on_save: bool, pub ensure_final_newline_on_save: bool, pub formatter: Formatter, + pub prettier: HashMap, pub enable_language_server: bool, pub show_copilot_suggestions: bool, pub show_whitespaces: ShowWhitespaceSetting, @@ -98,6 +99,8 @@ pub struct LanguageSettingsContent { #[serde(default)] pub formatter: Option, #[serde(default)] + pub prettier: Option>, + #[serde(default)] pub enable_language_server: Option, #[serde(default)] pub show_copilot_suggestions: Option, @@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting { All, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Formatter { + #[default] + Auto, LanguageServer, + Prettier, External { command: Arc, arguments: Arc<[String]>, @@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent src.preferred_line_length, ); merge(&mut settings.formatter, src.formatter.clone()); + merge(&mut settings.prettier, src.prettier.clone()); merge(&mut settings.format_on_save, src.format_on_save.clone()); merge( &mut settings.remove_trailing_whitespace_on_save, diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 820a8b6f81..dcb8833f8c 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime { } } -pub struct FakeNodeRuntime; +pub struct FakeNodeRuntime(Option); + +struct PrettierSupport { + plugins: Vec<&'static str>, +} impl FakeNodeRuntime { pub fn new() -> Arc { - Arc::new(FakeNodeRuntime) + Arc::new(FakeNodeRuntime(None)) + } + + pub fn with_prettier_support(plugins: &[&'static str]) -> Arc { + Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins)))) } } #[async_trait::async_trait] impl NodeRuntime for FakeNodeRuntime { - async fn binary_path(&self) -> Result { - unreachable!() + async fn binary_path(&self) -> anyhow::Result { + if let Some(prettier_support) = &self.0 { + prettier_support.binary_path().await + } else { + unreachable!() + } + } + + async fn run_npm_subcommand( + &self, + directory: Option<&Path>, + subcommand: &str, + args: &[&str], + ) -> anyhow::Result { + if let Some(prettier_support) = &self.0 { + prettier_support + .run_npm_subcommand(directory, subcommand, args) + .await + } else { + unreachable!() + } + } + + async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { + if let Some(prettier_support) = &self.0 { + prettier_support.npm_package_latest_version(name).await + } else { + unreachable!() + } + } + + async fn npm_install_packages( + &self, + directory: &Path, + packages: &[(&str, &str)], + ) -> anyhow::Result<()> { + if let Some(prettier_support) = &self.0 { + prettier_support + .npm_install_packages(directory, packages) + .await + } else { + unreachable!() + } + } +} + +impl PrettierSupport { + const PACKAGE_VERSION: &str = "0.0.1"; + + fn new(plugins: &[&'static str]) -> Self { + Self { + plugins: plugins.to_vec(), + } + } +} + +#[async_trait::async_trait] +impl NodeRuntime for PrettierSupport { + async fn binary_path(&self) -> anyhow::Result { + Ok(PathBuf::from("prettier_fake_node")) } async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result { unreachable!() } - async fn npm_package_latest_version(&self, _: &str) -> Result { - unreachable!() + async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { + if name == "prettier" || self.plugins.contains(&name) { + Ok(Self::PACKAGE_VERSION.to_string()) + } else { + panic!("Unexpected package name: {name}") + } } - async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> { - unreachable!() + async fn npm_install_packages( + &self, + _: &Path, + packages: &[(&str, &str)], + ) -> anyhow::Result<()> { + assert_eq!( + packages.len(), + self.plugins.len() + 1, + "Unexpected packages length to install: {:?}, expected `prettier` + {:?}", + packages, + self.plugins + ); + for (name, version) in packages { + assert!( + name == &"prettier" || self.plugins.contains(name), + "Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", + name, + packages, + Self::PACKAGE_VERSION, + self.plugins + ); + assert_eq!( + version, + &Self::PACKAGE_VERSION, + "Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", + version, + packages, + Self::PACKAGE_VERSION, + self.plugins + ); + } + Ok(()) } } diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml new file mode 100644 index 0000000000..997fa87126 --- /dev/null +++ b/crates/prettier/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "prettier" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/prettier.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +client = { path = "../client" } +collections = { path = "../collections"} +language = { path = "../language" } +gpui = { path = "../gpui" } +fs = { path = "../fs" } +lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime"} +util = { path = "../util" } + +log.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +anyhow.workspace = true +futures.workspace = true + +[dev-dependencies] +language = { path = "../language", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs new file mode 100644 index 0000000000..c3811b567b --- /dev/null +++ b/crates/prettier/src/prettier.rs @@ -0,0 +1,513 @@ +use std::collections::VecDeque; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::Context; +use collections::{HashMap, HashSet}; +use fs::Fs; +use gpui::{AsyncAppContext, ModelHandle}; +use language::language_settings::language_settings; +use language::{Buffer, BundledFormatter, Diff}; +use lsp::{LanguageServer, LanguageServerId}; +use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; +use util::paths::DEFAULT_PRETTIER_DIR; + +pub enum Prettier { + Real(RealPrettier), + #[cfg(any(test, feature = "test-support"))] + Test(TestPrettier), +} + +pub struct RealPrettier { + worktree_id: Option, + default: bool, + prettier_dir: PathBuf, + server: Arc, +} + +#[cfg(any(test, feature = "test-support"))] +pub struct TestPrettier { + worktree_id: Option, + prettier_dir: PathBuf, + default: bool, +} + +#[derive(Debug)] +pub struct LocateStart { + pub worktree_root_path: Arc, + pub starting_path: Arc, +} + +pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; +pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); +const PRETTIER_PACKAGE_NAME: &str = "prettier"; +const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; + +impl Prettier { + pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ + ".prettierrc", + ".prettierrc.json", + ".prettierrc.json5", + ".prettierrc.yaml", + ".prettierrc.yml", + ".prettierrc.toml", + ".prettierrc.js", + ".prettierrc.cjs", + "package.json", + "prettier.config.js", + "prettier.config.cjs", + ".editorconfig", + ]; + + #[cfg(any(test, feature = "test-support"))] + pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; + + pub async fn locate( + starting_path: Option, + fs: Arc, + ) -> anyhow::Result { + let paths_to_check = match starting_path.as_ref() { + Some(starting_path) => { + let worktree_root = starting_path + .worktree_root_path + .components() + .into_iter() + .take_while(|path_component| { + path_component.as_os_str().to_string_lossy() != "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_string_lossy() == "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(), + }; + + match find_closest_prettier_dir(paths_to_check, fs.as_ref()) + .await + .with_context(|| format!("finding prettier starting with {starting_path:?}"))? + { + Some(prettier_dir) => Ok(prettier_dir), + None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()), + } + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn start( + worktree_id: Option, + _: LanguageServerId, + prettier_dir: PathBuf, + _: Arc, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok( + #[cfg(any(test, feature = "test-support"))] + 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, + server_id: LanguageServerId, + prettier_dir: PathBuf, + node: Arc, + cx: AsyncAppContext, + ) -> anyhow::Result { + use lsp::LanguageServerBinary; + + let backgroud = cx.background(); + anyhow::ensure!( + prettier_dir.is_dir(), + "Prettier dir {prettier_dir:?} is not a directory" + ); + let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE); + anyhow::ensure!( + prettier_server.is_file(), + "no prettier server package found at {prettier_server:?}" + ); + + let node_path = backgroud + .spawn(async move { node.binary_path().await }) + .await?; + let server = LanguageServer::new( + server_id, + LanguageServerBinary { + path: node_path, + arguments: vec![prettier_server.into(), prettier_dir.as_path().into()], + }, + Path::new("/"), + None, + cx, + ) + .context("prettier server creation")?; + let server = backgroud + .spawn(server.initialize(None)) + .await + .context("prettier server initialization")?; + Ok(Self::Real(RealPrettier { + worktree_id, + server, + default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), + prettier_dir, + })) + } + + pub async fn format( + &self, + buffer: &ModelHandle, + buffer_path: Option, + cx: &AsyncAppContext, + ) -> anyhow::Result { + match self { + Self::Real(local) => { + let params = buffer.read_with(cx, |buffer, cx| { + let buffer_language = buffer.language(); + let parsers_with_plugins = buffer_language + .into_iter() + .flat_map(|language| { + language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.enabled_formatters()) + .filter_map(|formatter| match formatter { + BundledFormatter::Prettier { + parser_name, + plugin_names, + } => Some((parser_name, plugin_names)), + }) + }) + .fold( + HashMap::default(), + |mut parsers_with_plugins, (parser_name, plugins)| { + match parser_name { + Some(parser_name) => parsers_with_plugins + .entry(parser_name) + .or_insert_with(HashSet::default) + .extend(plugins), + None => parsers_with_plugins.values_mut().for_each(|existing_plugins| { + existing_plugins.extend(plugins.iter()); + }), + } + parsers_with_plugins + }, + ); + + let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len()); + if parsers_with_plugins.len() > 1 { + log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}"); + } + + let prettier_node_modules = self.prettier_dir().join("node_modules"); + anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}"); + let plugin_name_into_path = |plugin_name: &str| { + let prettier_plugin_dir = prettier_node_modules.join(plugin_name); + for possible_plugin_path in [ + prettier_plugin_dir.join("dist").join("index.mjs"), + prettier_plugin_dir.join("dist").join("index.js"), + prettier_plugin_dir.join("dist").join("plugin.js"), + prettier_plugin_dir.join("index.mjs"), + prettier_plugin_dir.join("index.js"), + prettier_plugin_dir.join("plugin.js"), + prettier_plugin_dir, + ] { + if possible_plugin_path.is_file() { + return Some(possible_plugin_path); + } + } + None + }; + let (parser, located_plugins) = match selected_parser_with_plugins { + Some((parser, plugins)) => { + // Tailwind plugin requires being added last + // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins + let mut add_tailwind_back = false; + + let mut plugins = plugins.into_iter().filter(|&&plugin_name| { + if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { + add_tailwind_back = true; + false + } else { + true + } + }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::>(); + if add_tailwind_back { + plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME))); + } + (Some(parser.to_string()), plugins) + }, + None => (None, Vec::new()), + }; + + let prettier_options = if self.is_default() { + let language_settings = language_settings(buffer_language, buffer.file(), cx); + let mut options = language_settings.prettier.clone(); + if !options.contains_key("tabWidth") { + options.insert( + "tabWidth".to_string(), + serde_json::Value::Number(serde_json::Number::from( + language_settings.tab_size.get(), + )), + ); + } + if !options.contains_key("printWidth") { + options.insert( + "printWidth".to_string(), + serde_json::Value::Number(serde_json::Number::from( + language_settings.preferred_line_length, + )), + ); + } + Some(options) + } else { + None + }; + + let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| { + match located_plugin_path { + Some(path) => Some(path), + None => { + log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}"); + None}, + } + }).collect(); + log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx))); + + anyhow::Ok(FormatParams { + text: buffer.text(), + options: FormatOptions { + parser, + plugins, + path: buffer_path, + prettier_options, + }, + }) + }).context("prettier params calculation")?; + let response = local + .server + .request::(params) + .await + .context("prettier format request")?; + let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx)); + Ok(diff_task.await) + } + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => Ok(buffer + .read_with(cx, |buffer, cx| { + let formatted_text = buffer.text() + Self::FORMAT_SUFFIX; + buffer.diff(formatted_text, cx) + }) + .await), + } + } + + pub async fn clear_cache(&self) -> anyhow::Result<()> { + match self { + Self::Real(local) => local + .server + .request::(()) + .await + .context("prettier clear cache"), + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => Ok(()), + } + } + + pub fn server(&self) -> Option<&Arc> { + match self { + Self::Real(local) => Some(&local.server), + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => None, + } + } + + pub fn is_default(&self) -> bool { + match self { + Self::Real(local) => local.default, + #[cfg(any(test, feature = "test-support"))] + Self::Test(test_prettier) => test_prettier.default, + } + } + + pub fn prettier_dir(&self) -> &Path { + match self { + Self::Real(local) => &local.prettier_dir, + #[cfg(any(test, feature = "test-support"))] + Self::Test(test_prettier) => &test_prettier.prettier_dir, + } + } + + pub fn worktree_id(&self) -> Option { + match self { + Self::Real(local) => local.worktree_id, + #[cfg(any(test, feature = "test-support"))] + Self::Test(test_prettier) => test_prettier.worktree_id, + } + } +} + +async fn find_closest_prettier_dir( + paths_to_check: Vec, + fs: &dyn Fs, +) -> anyhow::Result> { + for path in paths_to_check { + let possible_package_json = path.join("package.json"); + if let Some(package_json_metadata) = fs + .metadata(&possible_package_json) + .await + .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))? + { + if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + if let Ok(json_contents) = serde_json::from_str::>( + &package_json_contents, + ) { + if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return Ok(Some(path)); + } + } + if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies") + { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return Ok(Some(path)); + } + } + } + } + } + + let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); + if let Some(node_modules_location_metadata) = fs + .metadata(&possible_node_modules_location) + .await + .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? + { + if node_modules_location_metadata.is_dir { + return Ok(Some(path)); + } + } + } + Ok(None) +} + +enum Format {} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatParams { + text: String, + options: FormatOptions, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatOptions { + plugins: Vec, + parser: Option, + #[serde(rename = "filepath")] + path: Option, + prettier_options: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatResult { + text: String, +} + +impl lsp::request::Request for Format { + type Params = FormatParams; + type Result = FormatResult; + const METHOD: &'static str = "prettier/format"; +} + +enum ClearCache {} + +impl lsp::request::Request for ClearCache { + type Params = (); + type Result = (); + const METHOD: &'static str = "prettier/clear_cache"; +} diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js new file mode 100644 index 0000000000..a56c220f20 --- /dev/null +++ b/crates/prettier/src/prettier_server.js @@ -0,0 +1,217 @@ +const { Buffer } = require('buffer'); +const fs = require("fs"); +const path = require("path"); +const { once } = require('events'); + +const prettierContainerPath = process.argv[2]; +if (prettierContainerPath == null || prettierContainerPath.length == 0) { + process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`); + process.exit(1); +} +fs.stat(prettierContainerPath, (err, stats) => { + if (err) { + process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`); + process.exit(1); + } + + if (!stats.isDirectory()) { + process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`); + process.exit(1); + } +}); +const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier'); + +class Prettier { + constructor(path, prettier, config) { + this.path = path; + this.prettier = prettier; + this.config = config; + } +} + +(async () => { + let prettier; + let config; + try { + prettier = await loadPrettier(prettierPath); + config = await prettier.resolveConfig(prettierPath) || {}; + } catch (e) { + process.stderr.write(`Failed to load prettier: ${e}\n`); + process.exit(1); + } + process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`); + process.stdin.resume(); + handleBuffer(new Prettier(prettierPath, prettier, config)); +})() + +async function handleBuffer(prettier) { + for await (const messageText of readStdin()) { + let message; + try { + message = JSON.parse(messageText); + } catch (e) { + sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`)); + continue; + } + // allow concurrent request handling by not `await`ing the message handling promise (async function) + handleMessage(message, prettier).catch(e => { + sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) }); + }); + } +} + +const headerSeparator = "\r\n"; +const contentLengthHeaderName = 'Content-Length'; + +async function* readStdin() { + let buffer = Buffer.alloc(0); + let streamEnded = false; + process.stdin.on('end', () => { + streamEnded = true; + }); + process.stdin.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + }); + + async function handleStreamEnded(errorMessage) { + sendResponse(makeError(errorMessage)); + buffer = Buffer.alloc(0); + messageLength = null; + await once(process.stdin, 'readable'); + streamEnded = false; + } + + try { + let headersLength = null; + let messageLength = null; + main_loop: while (true) { + if (messageLength === null) { + while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) { + if (streamEnded) { + await handleStreamEnded('Unexpected end of stream: headers not found'); + continue main_loop; + } else if (buffer.length > contentLengthHeaderName.length * 10) { + await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`); + continue main_loop; + } + await once(process.stdin, 'readable'); + } + const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii'); + const contentLengthHeader = headers.split(headerSeparator) + .map(header => header.split(':')) + .filter(header => header[2] === undefined) + .filter(header => (header[1] || '').length > 0) + .find(header => (header[0] || '').trim() === contentLengthHeaderName); + const contentLength = (contentLengthHeader || [])[1]; + if (contentLength === undefined) { + await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`); + continue main_loop; + } + headersLength = headers.length + headerSeparator.length * 2; + messageLength = parseInt(contentLength, 10); + } + + while (buffer.length < (headersLength + messageLength)) { + if (streamEnded) { + await handleStreamEnded( + `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`); + continue main_loop; + } + await once(process.stdin, 'readable'); + } + + const messageEnd = headersLength + messageLength; + const message = buffer.subarray(headersLength, messageEnd); + buffer = buffer.subarray(messageEnd); + headersLength = null; + messageLength = null; + yield message.toString('utf8'); + } + } catch (e) { + sendResponse(makeError(`Error reading stdin: ${e}`)); + } finally { + process.stdin.off('data', () => { }); + } +} + +async function handleMessage(message, prettier) { + const { method, id, params } = message; + if (method === undefined) { + throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); + } + if (id === undefined) { + throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); + } + + if (method === 'prettier/format') { + if (params === undefined || params.text === undefined) { + throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`); + } + if (params.options === undefined) { + throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`); + } + + let resolvedConfig = {}; + if (params.options.filepath !== undefined) { + resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {}; + } + + const options = { + ...(params.options.prettierOptions || prettier.config), + ...resolvedConfig, + parser: params.options.parser, + plugins: params.options.plugins, + path: params.options.filepath + }; + process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`); + const formattedText = await prettier.prettier.format(params.text, options); + sendResponse({ id, result: { text: formattedText } }); + } else if (method === 'prettier/clear_cache') { + prettier.prettier.clearConfigCache(); + prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {}; + sendResponse({ id, result: null }); + } else if (method === 'initialize') { + sendResponse({ + id, + result: { + "capabilities": {} + } + }); + } else { + throw new Error(`Unknown method: ${method}`); + } +} + +function makeError(message) { + return { + error: { + "code": -32600, // invalid request code + message, + } + }; +} + +function sendResponse(response) { + const responsePayloadString = JSON.stringify({ + jsonrpc: "2.0", + ...response + }); + const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`; + process.stdout.write(headers + responsePayloadString); +} + +function loadPrettier(prettierPath) { + return new Promise((resolve, reject) => { + fs.access(prettierPath, fs.constants.F_OK, (err) => { + if (err) { + reject(`Path '${prettierPath}' does not exist.Error: ${err}`); + } else { + try { + resolve(require(prettierPath)); + } catch (err) { + reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`); + } + } + }); + }); +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ffea6646e9..cfa623d534 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -15,6 +15,7 @@ test-support = [ "language/test-support", "settings/test-support", "text/test-support", + "prettier/test-support", ] [dependencies] @@ -31,6 +32,8 @@ git = { path = "../git" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime" } +prettier = { path = "../prettier" } rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } @@ -73,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 diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a50e02a631..f9e1b1ce96 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -20,7 +20,7 @@ use futures::{ mpsc::{self, UnboundedReceiver}, oneshot, }, - future::{try_join_all, Shared}, + future::{self, try_join_all, Shared}, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; @@ -31,17 +31,19 @@ use gpui::{ }; use itertools::Itertools; use language::{ - language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, + language_settings::{ + language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, + }, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, - CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, - File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, - OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, - ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter, + CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, + Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, + LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, + TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -49,7 +51,9 @@ use lsp::{ DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf, }; use lsp_command::*; +use node_runtime::NodeRuntime; use postage::watch; +use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -75,10 +79,13 @@ use std::{ time::{Duration, Instant}, }; use terminals::Terminals; -use text::Anchor; +use text::{Anchor, LineEnding, Rope}; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -152,6 +159,11 @@ pub struct Project { copilot_lsp_subscription: Option, copilot_log_subscription: Option, current_lsp_settings: HashMap, LspSettings>, + node: Option>, + prettier_instances: HashMap< + (Option, PathBuf), + Shared, Arc>>>, + >, } struct DelayedDebounced { @@ -605,6 +617,7 @@ impl Project { pub fn local( client: Arc, + node: Arc, user_store: ModelHandle, languages: Arc, fs: Arc, @@ -660,6 +673,8 @@ impl Project { copilot_lsp_subscription, copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), + node: Some(node), + prettier_instances: HashMap::default(), } }) } @@ -757,6 +772,8 @@ impl Project { copilot_lsp_subscription, copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), + node: None, + prettier_instances: HashMap::default(), }; for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); @@ -795,8 +812,16 @@ impl Project { let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = - cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx)); + let project = cx.update(|cx| { + Project::local( + client, + node_runtime::FakeNodeRuntime::new(), + user_store, + Arc::new(languages), + fs, + cx, + ) + }); for path in root_paths { let (tree, _) = project .update(cx, |project, cx| { @@ -810,19 +835,37 @@ impl Project { project } + /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes. + /// Instead, if appends the suffix to every input, this suffix is returned by this method. + #[cfg(any(test, feature = "test-support"))] + pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str { + self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support( + plugins, + )); + Prettier::FORMAT_SUFFIX + } + fn on_settings_changed(&mut self, cx: &mut ModelContext) { let mut language_servers_to_start = Vec::new(); + let mut language_formatters_to_check = Vec::new(); for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); - if let Some((file, language)) = buffer.file().zip(buffer.language()) { - let settings = language_settings(Some(language), Some(file), cx); + let buffer_file = File::from_dyn(buffer.file()); + let buffer_language = buffer.language(); + let settings = language_settings(buffer_language, buffer.file(), cx); + if let Some(language) = buffer_language { if settings.enable_language_server { - if let Some(file) = File::from_dyn(Some(file)) { + if let Some(file) = buffer_file { language_servers_to_start - .push((file.worktree.clone(), language.clone())); + .push((file.worktree.clone(), Arc::clone(language))); } } + language_formatters_to_check.push(( + buffer_file.map(|f| f.worktree_id(cx)), + Arc::clone(language), + settings.clone(), + )); } } } @@ -875,6 +918,11 @@ impl Project { .detach(); } + for (worktree, language, settings) in language_formatters_to_check { + self.install_default_formatters(worktree, &language, &settings, cx) + .detach_and_log_err(cx); + } + // Start all the newly-enabled language servers. for (worktree, language) in language_servers_to_start { let worktree_path = worktree.read(cx).abs_path(); @@ -2623,7 +2671,26 @@ impl Project { } }); - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + let buffer_file = buffer.read(cx).file().cloned(); + let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); + let buffer_file = File::from_dyn(buffer_file.as_ref()); + let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); + + let task_buffer = buffer.clone(); + let prettier_installation_task = + self.install_default_formatters(worktree, &new_language, &settings, cx); + cx.spawn(|project, mut cx| async move { + prettier_installation_task.await?; + let _ = project + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(&task_buffer, cx) + }) + .await; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); @@ -3949,7 +4016,7 @@ impl Project { push_to_history: bool, trigger: FormatTrigger, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { if self.is_local() { let mut buffers_with_paths_and_servers = buffers .into_iter() @@ -4027,6 +4094,7 @@ impl Project { enum FormatOperation { Lsp(Vec<(Range, String)>), External(Diff), + Prettier(Diff), } // Apply language-specific formatting using either a language server @@ -4062,8 +4130,8 @@ impl Project { | (_, FormatOnSave::External { command, arguments }) => { if let Some(buffer_abs_path) = buffer_abs_path { format_operation = Self::format_via_external_command( - &buffer, - &buffer_abs_path, + buffer, + buffer_abs_path, &command, &arguments, &mut cx, @@ -4076,6 +4144,69 @@ impl Project { .map(FormatOperation::External); } } + (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(prettier_task) = this + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }).await { + match prettier_task.await + { + Ok(prettier) => { + let buffer_path = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + format_operation = Some(FormatOperation::Prettier( + prettier + .format(buffer, buffer_path, &cx) + .await + .context("formatting via prettier")?, + )); + } + Err(e) => anyhow::bail!( + "Failed to create prettier instance for buffer during autoformatting: {e:#}" + ), + } + } else if let Some((language_server, buffer_abs_path)) = + language_server.as_ref().zip(buffer_abs_path.as_ref()) + { + format_operation = Some(FormatOperation::Lsp( + Self::format_via_lsp( + &this, + &buffer, + buffer_abs_path, + &language_server, + tab_size, + &mut cx, + ) + .await + .context("failed to format via language server")?, + )); + } + } + (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(prettier_task) = this + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }).await { + match prettier_task.await + { + Ok(prettier) => { + let buffer_path = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + format_operation = Some(FormatOperation::Prettier( + prettier + .format(buffer, buffer_path, &cx) + .await + .context("formatting via prettier")?, + )); + } + Err(e) => anyhow::bail!( + "Failed to create prettier instance for buffer during formatting: {e:#}" + ), + } + } + } }; buffer.update(&mut cx, |b, cx| { @@ -4100,6 +4231,9 @@ impl Project { FormatOperation::External(diff) => { b.apply_diff(diff, cx); } + FormatOperation::Prettier(diff) => { + b.apply_diff(diff, cx); + } } if let Some(transaction_id) = whitespace_transaction_id { @@ -5873,6 +6007,7 @@ impl Project { this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); this.update_local_worktree_settings(&worktree, changes, cx); + this.update_prettier_settings(&worktree, changes, cx); cx.emit(Event::WorktreeUpdatedEntries( worktree.read(cx).id(), changes.clone(), @@ -6252,6 +6387,69 @@ impl Project { .detach(); } + fn update_prettier_settings( + &self, + worktree: &ModelHandle, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext<'_, Project>, + ) { + let prettier_config_files = Prettier::CONFIG_FILE_NAMES + .iter() + .map(Path::new) + .collect::>(); + + let prettier_config_file_changed = changes + .iter() + .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) + .filter(|(path, _, _)| { + !path + .components() + .any(|component| component.as_os_str().to_string_lossy() == "node_modules") + }) + .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); + let current_worktree_id = worktree.read(cx).id(); + if let Some((config_path, _, _)) = prettier_config_file_changed { + log::info!( + "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" + ); + let prettiers_to_reload = self + .prettier_instances + .iter() + .filter_map(|((worktree_id, prettier_path), prettier_task)| { + if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) { + Some((*worktree_id, prettier_path.clone(), prettier_task.clone())) + } else { + None + } + }) + .collect::>(); + + cx.background() + .spawn(async move { + for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { + async move { + prettier_task.await? + .clear_cache() + .await + .with_context(|| { + format!( + "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" + ) + }) + .map_err(Arc::new) + } + })) + .await + { + if let Err(e) = task_result { + log::error!("Failed to clear cache for prettier: {e:#}"); + } + } + }) + .detach(); + } + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -8109,6 +8307,236 @@ impl Project { Vec::new() } } + + fn prettier_instance_for_buffer( + &mut self, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Task, Arc>>>>> { + let buffer = buffer.read(cx); + let buffer_file = buffer.file(); + let Some(buffer_language) = buffer.language() else { + return Task::ready(None); + }; + if !buffer_language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.enabled_formatters()) + .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. })) + { + return Task::ready(None); + } + + let buffer_file = File::from_dyn(buffer_file); + let buffer_path = buffer_file.map(|file| Arc::clone(file.path())); + let worktree_path = buffer_file + .as_ref() + .and_then(|file| Some(file.worktree.read(cx).abs_path())); + let worktree_id = buffer_file.map(|file| file.worktree_id(cx)); + if self.is_local() || worktree_id.is_none() || worktree_path.is_none() { + let Some(node) = self.node.as_ref().map(Arc::clone) else { + return Task::ready(None); + }; + cx.spawn(|this, mut cx| async move { + let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs)); + let prettier_dir = 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 Some( + Task::ready(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 + .get(&(worktree_id, prettier_dir.clone())) + .cloned() + }) { + return Some(existing_prettier); + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let task_prettier_dir = prettier_dir.clone(); + let weak_project = this.downgrade(); + let new_server_id = + this.update(&mut cx, |this, _| this.languages.next_language_server_id()); + let new_prettier_task = cx + .spawn(|mut cx| async move { + let prettier = Prettier::start( + worktree_id.map(|id| id.to_usize()), + new_server_id, + task_prettier_dir, + node, + cx.clone(), + ) + .await + .context("prettier start") + .map_err(Arc::new)?; + log::info!("Started prettier in {:?}", prettier.prettier_dir()); + + if let Some((project, prettier_server)) = + weak_project.upgrade(&mut cx).zip(prettier.server()) + { + project.update(&mut cx, |project, cx| { + let name = if prettier.is_default() { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let prettier_dir = prettier.prettier_dir(); + let worktree_path = prettier + .worktree_id() + .map(WorktreeId::from_usize) + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.read(cx).abs_path()); + match worktree_path { + Some(worktree_path) => { + if worktree_path.as_ref() == prettier_dir { + LanguageServerName(Arc::from(format!( + "prettier ({})", + prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default() + ))) + } else { + let dir_to_display = match prettier_dir + .strip_prefix(&worktree_path) + .ok() + { + Some(relative_path) => relative_path, + None => prettier_dir, + }; + LanguageServerName(Arc::from(format!( + "prettier ({})", + dir_to_display.display(), + ))) + } + } + None => LanguageServerName(Arc::from(format!( + "prettier ({})", + prettier_dir.display(), + ))), + } + }; + + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }); + } + Ok(Arc::new(prettier)).map_err(Arc::new) + }) + .shared(); + this.update(&mut cx, |project, _| { + project + .prettier_instances + .insert((worktree_id, prettier_dir), new_prettier_task.clone()); + }); + Some(new_prettier_task) + }) + } else if self.remote_id().is_some() { + return Task::ready(None); + } else { + Task::ready(Some( + Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), + )) + } + } + + fn install_default_formatters( + &self, + worktree: Option, + new_language: &Language, + language_settings: &LanguageSettings, + cx: &mut ModelContext, + ) -> Task> { + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())), + }; + let Some(node) = self.node.as_ref().cloned() else { + return Task::ready(Ok(())); + }; + + let mut prettier_plugins = None; + for formatter in new_language + .lsp_adapters() + .into_iter() + .flat_map(|adapter| adapter.enabled_formatters()) + { + match formatter { + BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins + .get_or_insert_with(|| HashSet::default()) + .extend(plugin_names), + } + } + let Some(prettier_plugins) = prettier_plugins else { + return Task::ready(Ok(())); + }; + + let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path(); + let already_running_prettier = self + .prettier_instances + .get(&(worktree, default_prettier_dir.to_path_buf())) + .cloned(); + + let fs = Arc::clone(&self.fs); + cx.background() + .spawn(async move { + let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE); + // method creates parent directory if it doesn't exist + fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await + .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; + + let packages_to_versions = future::try_join_all( + prettier_plugins + .iter() + .chain(Some(&"prettier")) + .map(|package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node.npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }), + ) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions.iter().map(|(package, version)| { + (package.as_str(), version.as_str()) + }).collect::>(); + node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?; + + if !prettier_plugins.is_empty() { + if let Some(prettier) = already_running_prettier { + prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?; + } + } + + anyhow::Ok(()) + }) + } } fn subscribe_for_copilot_events( diff --git a/crates/semantic_index/examples/eval.rs b/crates/semantic_index/examples/eval.rs index 573cf73d78..33d6b3689c 100644 --- a/crates/semantic_index/examples/eval.rs +++ b/crates/semantic_index/examples/eval.rs @@ -494,6 +494,7 @@ fn main() { let project = cx.update(|cx| { Project::local( client.clone(), + node_runtime::FakeNodeRuntime::new(), user_store.clone(), languages.clone(), fs.clone(), diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 4578ce0bc9..96d77236a9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -11,6 +11,7 @@ lazy_static::lazy_static! { pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); + pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier"); pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d1240a45ce..99f19ed9d0 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -30,6 +30,7 @@ gpui = { path = "../gpui" } install_cli = { path = "../install_cli" } language = { path = "../language" } menu = { path = "../menu" } +node_runtime = { path = "../node_runtime" } project = { path = "../project" } settings = { path = "../settings" } terminal = { path = "../terminal" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8b068fa10c..454b0138e6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -42,6 +42,7 @@ use gpui::{ use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use itertools::Itertools; use language::{LanguageRegistry, Rope}; +use node_runtime::NodeRuntime; use std::{ any::TypeId, borrow::Cow, @@ -456,6 +457,7 @@ pub struct AppState { pub initialize_workspace: fn(WeakViewHandle, bool, Arc, AsyncAppContext) -> Task>, pub background_actions: BackgroundActions, + pub node_runtime: Arc, } pub struct WorkspaceStore { @@ -474,6 +476,7 @@ struct Follower { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { + use node_runtime::FakeNodeRuntime; use settings::SettingsStore; if !cx.has_global::() { @@ -498,6 +501,7 @@ impl AppState { user_store, // channel_store, workspace_store, + node_runtime: FakeNodeRuntime::new(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), background_actions: || &[], @@ -816,6 +820,7 @@ impl Workspace { )> { let project_handle = Project::local( app_state.client.clone(), + app_state.node_runtime.clone(), app_state.user_store.clone(), app_state.languages.clone(), app_state.fs.clone(), @@ -3517,6 +3522,8 @@ impl Workspace { #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { + use node_runtime::FakeNodeRuntime; + let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); @@ -3530,6 +3537,7 @@ impl Workspace { build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], + node_runtime: FakeNodeRuntime::new(), }); Self::new(0, project, app_state, cx) } diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs index fdbc179209..f046437d75 100644 --- a/crates/zed/src/languages/css.rs +++ b/crates/zed/src/languages/css.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -96,6 +96,10 @@ impl LspAdapter for CssLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("css")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index b8f1c70cce..6f27b7ca8f 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -96,6 +96,10 @@ impl LspAdapter for HtmlLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("html")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 63f909ae2a..f017af0a22 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -4,7 +4,9 @@ use collections::HashMap; use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; -use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{ + BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate, +}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -144,6 +146,10 @@ impl LspAdapter for JsonLspAdapter { async fn language_ids(&self) -> HashMap { [("JSON".into(), "jsonc".into())].into_iter().collect() } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("json")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/svelte.rs b/crates/zed/src/languages/svelte.rs index 5e42d80e77..2089fe88b1 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/zed/src/languages/svelte.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -95,6 +95,13 @@ impl LspAdapter for SvelteLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::Prettier { + parser_name: Some("svelte"), + plugin_names: vec!["prettier-plugin-svelte"], + }] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index cf07fa71c9..8e81f728dc 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -6,7 +6,7 @@ use futures::{ FutureExt, StreamExt, }; use gpui::AppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::{json, Value}; @@ -127,6 +127,13 @@ impl LspAdapter for TailwindLspAdapter { .into_iter(), ) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::Prettier { + parser_name: None, + plugin_names: vec!["prettier-plugin-tailwindcss"], + }] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 676d0fd4c0..f09c964588 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -4,7 +4,7 @@ use async_tar::Archive; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt}; use gpui::AppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; @@ -161,6 +161,10 @@ impl LspAdapter for TypeScriptLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("typescript")] + } } async fn get_cached_ts_server_binary( @@ -309,6 +313,10 @@ impl LspAdapter for EsLintLspAdapter { async fn initialization_options(&self) -> Option { None } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("babel")] + } } async fn get_cached_eslint_server_binary( diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 8b438d0949..1c1ce18668 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{ - language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate, + language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter, + LspAdapterDelegate, }; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; @@ -108,6 +109,10 @@ impl LspAdapter for YamlLspAdapter { })) .boxed() } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("yaml")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f89a880c71..16189f6c4e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -154,7 +154,12 @@ fn main() { semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); vim::init(cx); terminal_view::init(cx); - copilot::init(copilot_language_server_id, http.clone(), node_runtime, cx); + copilot::init( + copilot_language_server_id, + http.clone(), + node_runtime.clone(), + cx, + ); assistant::init(cx); component_test::init(cx); @@ -181,6 +186,7 @@ fn main() { initialize_workspace, background_actions, workspace_store, + node_runtime, }); cx.set_global(Arc::downgrade(&app_state));