Detailed changes
@@ -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",
@@ -52,6 +52,7 @@ members = [
"crates/plugin",
"crates/plugin_macros",
"crates/plugin_runtime",
+ "crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
@@ -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.
@@ -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"] }
@@ -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<Deterministic>,
+ 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::<AllLanguageSettings>(cx, |file| {
+ file.defaults.formatter = Some(Formatter::Auto);
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |file| {
+ file.defaults.formatter = Some(Formatter::LanguageServer);
+ });
+ });
+ });
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| 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<Deterministic>,
@@ -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(),
@@ -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<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -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,
@@ -227,6 +227,10 @@ impl CachedLspAdapter {
) -> Option<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await
}
+
+ pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ 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<String, String> {
Default::default()
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ 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<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)]
@@ -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<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>)]) {
@@ -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<String, serde_json::Value>,
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<Formatter>,
#[serde(default)]
+ pub prettier: Option<HashMap<String, serde_json::Value>>,
+ #[serde(default)]
pub enable_language_server: Option<bool>,
#[serde(default)]
pub show_copilot_suggestions: Option<bool>,
@@ -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<str>,
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,
@@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime {
}
}
-pub struct FakeNodeRuntime;
+pub struct FakeNodeRuntime(Option<PrettierSupport>);
+
+struct PrettierSupport {
+ plugins: Vec<&'static str>,
+}
impl FakeNodeRuntime {
pub fn new() -> Arc<dyn NodeRuntime> {
- Arc::new(FakeNodeRuntime)
+ Arc::new(FakeNodeRuntime(None))
+ }
+
+ pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
+ Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
}
}
#[async_trait::async_trait]
impl NodeRuntime for FakeNodeRuntime {
- async fn binary_path(&self) -> Result<PathBuf> {
- unreachable!()
+ async fn binary_path(&self) -> anyhow::Result<PathBuf> {
+ 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<Output> {
+ 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<String> {
+ 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<PathBuf> {
+ 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" || 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(())
}
}
@@ -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"] }
@@ -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<usize>,
+ default: bool,
+ prettier_dir: PathBuf,
+ server: Arc<LanguageServer>,
+}
+
+#[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>,
+ pub starting_path: Arc<Path>,
+}
+
+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<LocateStart>,
+ fs: Arc<dyn Fs>,
+ ) -> anyhow::Result<PathBuf> {
+ 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::<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_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<usize>,
+ _: LanguageServerId,
+ prettier_dir: PathBuf,
+ _: Arc<dyn NodeRuntime>,
+ _: AsyncAppContext,
+ ) -> anyhow::Result<Self> {
+ 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<usize>,
+ server_id: LanguageServerId,
+ prettier_dir: PathBuf,
+ node: Arc<dyn NodeRuntime>,
+ cx: AsyncAppContext,
+ ) -> anyhow::Result<Self> {
+ 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>,
+ buffer_path: Option<PathBuf>,
+ cx: &AsyncAppContext,
+ ) -> anyhow::Result<Diff> {
+ 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::<Vec<_>>();
+ 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::<Format>(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::<ClearCache>(())
+ .await
+ .context("prettier clear cache"),
+ #[cfg(any(test, feature = "test-support"))]
+ Self::Test(_) => Ok(()),
+ }
+ }
+
+ pub fn server(&self) -> Option<&Arc<LanguageServer>> {
+ 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<usize> {
+ 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<PathBuf>,
+ fs: &dyn Fs,
+) -> anyhow::Result<Option<PathBuf>> {
+ 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::<HashMap<String, serde_json::Value>>(
+ &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<PathBuf>,
+ parser: Option<String>,
+ #[serde(rename = "filepath")]
+ path: Option<PathBuf>,
+ prettier_options: Option<HashMap<String, serde_json::Value>>,
+}
+
+#[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";
+}
@@ -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}`);
+ }
+ }
+ });
+ });
+}
@@ -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
@@ -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<gpui::Subscription>,
copilot_log_subscription: Option<lsp::Subscription>,
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
+ node: Option<Arc<dyn NodeRuntime>>,
+ prettier_instances: HashMap<
+ (Option<WorktreeId>, PathBuf),
+ Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
+ >,
}
struct DelayedDebounced {
@@ -605,6 +617,7 @@ impl Project {
pub fn local(
client: Arc<Client>,
+ node: Arc<dyn NodeRuntime>,
user_store: ModelHandle<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
@@ -660,6 +673,8 @@ impl Project {
copilot_lsp_subscription,
copilot_log_subscription: None,
current_lsp_settings: settings::get::<ProjectSettings>(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::<ProjectSettings>(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<Self>) {
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<Project>,
- ) -> Task<Result<ProjectTransaction>> {
+ ) -> Task<anyhow::Result<ProjectTransaction>> {
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<Anchor>, 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<Worktree>,
+ changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+ cx: &mut ModelContext<'_, Project>,
+ ) {
+ let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+ .iter()
+ .map(Path::new)
+ .collect::<HashSet<_>>();
+
+ 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::<Vec<_>>();
+
+ 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<ProjectPath>, cx: &mut ModelContext<Self>) {
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<Buffer>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
+ 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<WorktreeId>,
+ new_language: &Language,
+ language_settings: &LanguageSettings,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ 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::<Vec<_>>();
+ 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(
@@ -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(),
@@ -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");
@@ -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" }
@@ -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<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
pub background_actions: BackgroundActions,
+ pub node_runtime: Arc<dyn NodeRuntime>,
}
pub struct WorkspaceStore {
@@ -474,6 +476,7 @@ struct Follower {
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
+ use node_runtime::FakeNodeRuntime;
use settings::SettingsStore;
if !cx.has_global::<SettingsStore>() {
@@ -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<Project>, cx: &mut ViewContext<Self>) -> 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)
}
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::prettier("css")]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::prettier("html")]
+ }
}
async fn get_cached_server_binary(
@@ -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<String, String> {
[("JSON".into(), "jsonc".into())].into_iter().collect()
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ vec![BundledFormatter::prettier("json")]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::Prettier {
+ parser_name: Some("svelte"),
+ plugin_names: vec!["prettier-plugin-svelte"],
+ }]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::Prettier {
+ parser_name: None,
+ plugin_names: vec!["prettier-plugin-tailwindcss"],
+ }]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ 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<serde_json::Value> {
None
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ vec![BundledFormatter::prettier("babel")]
+ }
}
async fn get_cached_eslint_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::prettier("yaml")]
+ }
}
async fn get_cached_server_binary(
@@ -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));