diff --git a/Cargo.lock b/Cargo.lock index 39e6222c6c..3554c220c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5815,6 +5815,26 @@ dependencies = [ "util", ] +[[package]] +name = "prettier2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "collections", + "fs", + "futures 0.3.28", + "gpui2", + "language2", + "log", + "lsp2", + "node_runtime", + "serde", + "serde_derive", + "serde_json", + "util", +] + [[package]] name = "pretty_assertions" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index 400d471f54..c42feac8fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "crates/plugin_macros", "crates/plugin_runtime", "crates/prettier", + "crates/prettier2", "crates/project", "crates/project2", "crates/project_panel", diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 9649f4745f..8c69330074 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -22,6 +22,7 @@ use parking_lot::{Mutex, RwLock}; use slotmap::SlotMap; use std::{ any::{type_name, Any, TypeId}, + borrow::Borrow, mem, sync::{atomic::Ordering::SeqCst, Arc, Weak}, time::Duration, @@ -670,29 +671,12 @@ impl Context for AppContext { } } -impl MainThread { - fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { - self.0.update(|cx| { - update(unsafe { - std::mem::transmute::<&mut AppContext, &mut MainThread>(cx) - }) - }) - } - - pub(crate) fn update_window( - &mut self, - id: WindowId, - update: impl FnOnce(&mut MainThread) -> R, - ) -> Result { - self.0.update_window(id, |cx| { - update(unsafe { - std::mem::transmute::<&mut WindowContext, &mut MainThread>(cx) - }) - }) - } - +impl MainThread +where + C: Borrow, +{ pub(crate) fn platform(&self) -> &dyn Platform { - self.platform.borrow_on_main_thread() + self.0.borrow().platform.borrow_on_main_thread() } pub fn activate(&self, ignoring_other_apps: bool) { @@ -722,6 +706,28 @@ impl MainThread { pub fn open_url(&self, url: &str) { self.platform().open_url(url); } +} + +impl MainThread { + fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { + self.0.update(|cx| { + update(unsafe { + std::mem::transmute::<&mut AppContext, &mut MainThread>(cx) + }) + }) + } + + pub(crate) fn update_window( + &mut self, + id: WindowId, + update: impl FnOnce(&mut MainThread) -> R, + ) -> Result { + self.0.update_window(id, |cx| { + update(unsafe { + std::mem::transmute::<&mut WindowContext, &mut MainThread>(cx) + }) + }) + } pub fn open_window( &mut self, diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index c3bbd1b2e9..95b96c1868 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -56,7 +56,7 @@ pub use window::*; use derive_more::{Deref, DerefMut}; use std::{ any::{Any, TypeId}, - borrow::Borrow, + borrow::{Borrow, BorrowMut}, mem, ops::{Deref, DerefMut}, sync::Arc, @@ -141,20 +141,29 @@ impl Context for MainThread { } pub trait BorrowAppContext { - fn app_mut(&mut self) -> &mut AppContext; + fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R + where + F: FnOnce(&mut Self) -> R; + fn set_global(&mut self, global: T); +} + +impl BorrowAppContext for C +where + C: BorrowMut, +{ fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R where F: FnOnce(&mut Self) -> R, { - self.app_mut().push_text_style(style); + self.borrow_mut().push_text_style(style); let result = f(self); - self.app_mut().pop_text_style(); + self.borrow_mut().pop_text_style(); result } fn set_global(&mut self, global: T) { - self.app_mut().set_global(global) + self.borrow_mut().set_global(global) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index f803bb7bb6..66106a0057 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,13 +1,13 @@ use crate::{ - px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, - DisplayId, Edges, Effect, Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, - GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, - Keystroke, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, - MouseUpEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, - Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task, - Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS, + px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, + BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, Element, + EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData, + InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, MainThread, + MainThreadOnly, MonochromeSprite, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, + PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, Subscription, + TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle, WindowOptions, + SUBPIXEL_VARIANTS, }; use anyhow::Result; use collections::HashMap; @@ -17,7 +17,7 @@ use slotmap::SlotMap; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, - borrow::Cow, + borrow::{Borrow, BorrowMut, Cow}, fmt::Debug, future::Future, marker::PhantomData, @@ -1122,12 +1122,6 @@ impl<'a, 'w> WindowContext<'a, 'w> { } } -impl<'a, 'w> MainThread> { - fn platform(&self) -> &dyn Platform { - self.platform.borrow_on_main_thread() - } -} - impl Context for WindowContext<'_, '_> { type EntityContext<'a, 'w, T: 'static + Send + Sync> = ViewContext<'a, 'w, T>; type Result = T; @@ -1174,15 +1168,30 @@ impl<'a, 'w> std::ops::DerefMut for WindowContext<'a, 'w> { } } -impl BorrowAppContext for WindowContext<'_, '_> { - fn app_mut(&mut self) -> &mut AppContext { - &mut *self.app +impl<'a, 'w> Borrow for WindowContext<'a, 'w> { + fn borrow(&self) -> &AppContext { + &self.app } } -pub trait BorrowWindow: BorrowAppContext { - fn window(&self) -> &Window; - fn window_mut(&mut self) -> &mut Window; +impl<'a, 'w> BorrowMut for WindowContext<'a, 'w> { + fn borrow_mut(&mut self) -> &mut AppContext { + &mut self.app + } +} + +pub trait BorrowWindow: BorrowMut + BorrowMut { + fn app_mut(&mut self) -> &mut AppContext { + self.borrow_mut() + } + + fn window(&self) -> &Window { + self.borrow() + } + + fn window_mut(&mut self) -> &mut Window { + self.borrow_mut() + } fn with_element_id( &mut self, @@ -1205,7 +1214,8 @@ pub trait BorrowWindow: BorrowAppContext { } let result = f(global_id, self); - self.window_mut().element_id_stack.pop(); + let window: &mut Window = self.borrow_mut(); + window.element_id_stack.pop(); result } @@ -1308,34 +1318,46 @@ pub trait BorrowWindow: BorrowAppContext { } } -impl BorrowWindow for WindowContext<'_, '_> { - fn window(&self) -> &Window { - &*self.window - } - - fn window_mut(&mut self) -> &mut Window { - &mut *self.window +impl Borrow for WindowContext<'_, '_> { + fn borrow(&self) -> &Window { + &self.window } } -pub struct ViewContext<'a, 'w, S> { +impl BorrowMut for WindowContext<'_, '_> { + fn borrow_mut(&mut self) -> &mut Window { + &mut self.window + } +} + +impl BorrowWindow for T where T: BorrowMut + BorrowMut {} + +pub struct ViewContext<'a, 'w, V> { window_cx: WindowContext<'a, 'w>, - entity_type: PhantomData, + entity_type: PhantomData, entity_id: EntityId, } -impl BorrowAppContext for ViewContext<'_, '_, S> { - fn app_mut(&mut self) -> &mut AppContext { +impl Borrow for ViewContext<'_, '_, V> { + fn borrow(&self) -> &AppContext { + &*self.window_cx.app + } +} + +impl BorrowMut for ViewContext<'_, '_, V> { + fn borrow_mut(&mut self) -> &mut AppContext { &mut *self.window_cx.app } } -impl BorrowWindow for ViewContext<'_, '_, S> { - fn window(&self) -> &Window { - &self.window_cx.window +impl Borrow for ViewContext<'_, '_, V> { + fn borrow(&self) -> &Window { + &*self.window_cx.window } +} - fn window_mut(&mut self) -> &mut Window { +impl BorrowMut for ViewContext<'_, '_, V> { + fn borrow_mut(&mut self) -> &mut Window { &mut *self.window_cx.window } } diff --git a/crates/prettier2/Cargo.toml b/crates/prettier2/Cargo.toml new file mode 100644 index 0000000000..0fca004344 --- /dev/null +++ b/crates/prettier2/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "prettier2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/prettier2.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +client2 = { path = "../client2" } +collections = { path = "../collections"} +language2 = { path = "../language2" } +gpui2 = { path = "../gpui2" } +fs = { path = "../fs" } +lsp2 = { path = "../lsp2" } +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] +language2 = { path = "../language2", features = ["test-support"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs new file mode 100644 index 0000000000..c03e98c094 --- /dev/null +++ b/crates/prettier2/src/prettier2.rs @@ -0,0 +1,513 @@ +use anyhow::Context; +use collections::{HashMap, HashSet}; +use fs::Fs; +use gpui2::{AsyncAppContext, Handle}; +use language2::{language_settings::language_settings, Buffer, BundledFormatter, Diff}; +use lsp2::{LanguageServer, LanguageServerId}; +use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; +use std::{ + collections::VecDeque, + path::{Path, PathBuf}, + sync::Arc, +}; +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 lsp2::LanguageServerBinary; + + let executor = cx.executor(); + 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 = executor + .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 = executor + .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: &Handle, + buffer_path: Option, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + match self { + Self::Real(local) => { + let params = buffer.update(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.update(cx, |buffer, cx| buffer.diff(response.text, cx))?; + Ok(diff_task.await) + } + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => Ok(buffer + .update(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 lsp2::request::Request for Format { + type Params = FormatParams; + type Result = FormatResult; + const METHOD: &'static str = "prettier/format"; +} + +enum ClearCache {} + +impl lsp2::request::Request for ClearCache { + type Params = (); + type Result = (); + const METHOD: &'static str = "prettier/clear_cache"; +} diff --git a/crates/prettier2/src/prettier_server.js b/crates/prettier2/src/prettier_server.js new file mode 100644 index 0000000000..a56c220f20 --- /dev/null +++ b/crates/prettier2/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/project2/src/project2.rs b/crates/project2/src/project2.rs index 1efefc9cc7..066cf80e04 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -196,7 +196,7 @@ impl DelayedDebounced { self.cancel_channel = Some(sender); let previous_task = self.task.take(); - self.task = Some(cx.executor().spawn(|workspace, mut cx| async move { + self.task = Some(cx.spawn(|project, mut cx| async move { let mut timer = cx.executor().timer(delay).fuse(); if let Some(previous_task) = previous_task { previous_task.await; @@ -207,7 +207,7 @@ impl DelayedDebounced { _ = timer => {} } - if let Ok(task) = workspace.update(&mut cx, |workspace, cx| (func)(workspace, cx)) { + if let Ok(task) = project.update(&mut cx, |project, cx| (func)(project, cx)) { task.await; } })); @@ -8113,7 +8113,7 @@ impl Project { if let Some(buffer) = buffer { buffer.update(cx, |buffer, cx| { buffer.did_reload(version, fingerprint, line_ending, mtime, cx); - })?; + }); } Ok(()) })? diff --git a/crates/terminal2/src/mappings/colors.rs b/crates/terminal2/src/mappings/colors.rs index 5f80698e11..f89413c49c 100644 --- a/crates/terminal2/src/mappings/colors.rs +++ b/crates/terminal2/src/mappings/colors.rs @@ -1,130 +1,131 @@ -use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb}; -use gpui2::color::Color; -use theme2::TerminalStyle; +// todo!() +// use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb}; +// use gpui2::color::Color; +// use theme2::TerminalStyle; -///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent -pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { - match alac_color { - //Named and theme defined colors - alacritty_terminal::ansi::Color::Named(n) => match n { - alacritty_terminal::ansi::NamedColor::Black => style.black, - alacritty_terminal::ansi::NamedColor::Red => style.red, - alacritty_terminal::ansi::NamedColor::Green => style.green, - alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, - alacritty_terminal::ansi::NamedColor::Blue => style.blue, - alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, - alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, - alacritty_terminal::ansi::NamedColor::White => style.white, - alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, - alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, - alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, - alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, - alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, - alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, - alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, - alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, - alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, - alacritty_terminal::ansi::NamedColor::Background => style.background, - alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, - alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, - alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, - alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, - alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, - alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, - alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, - alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, - alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, - alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, - alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, - }, - //'True' colors - alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), - //8 bit, indexed colors - alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style), - } -} +// ///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +// pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { +// match alac_color { +// //Named and theme defined colors +// alacritty_terminal::ansi::Color::Named(n) => match n { +// alacritty_terminal::ansi::NamedColor::Black => style.black, +// alacritty_terminal::ansi::NamedColor::Red => style.red, +// alacritty_terminal::ansi::NamedColor::Green => style.green, +// alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, +// alacritty_terminal::ansi::NamedColor::Blue => style.blue, +// alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, +// alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, +// alacritty_terminal::ansi::NamedColor::White => style.white, +// alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, +// alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, +// alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, +// alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, +// alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, +// alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, +// alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, +// alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, +// alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, +// alacritty_terminal::ansi::NamedColor::Background => style.background, +// alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, +// alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, +// alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, +// alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, +// alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, +// alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, +// alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, +// alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, +// alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, +// alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, +// alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, +// }, +// //'True' colors +// alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), +// //8 bit, indexed colors +// alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style), +// } +// } -///Converts an 8 bit ANSI color to it's GPUI equivalent. -///Accepts usize for compatibility with the alacritty::Colors interface, -///Other than that use case, should only be called with values in the [0,255] range -pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { - match index { - //0-15 are the same as the named colors above - 0 => style.black, - 1 => style.red, - 2 => style.green, - 3 => style.yellow, - 4 => style.blue, - 5 => style.magenta, - 6 => style.cyan, - 7 => style.white, - 8 => style.bright_black, - 9 => style.bright_red, - 10 => style.bright_green, - 11 => style.bright_yellow, - 12 => style.bright_blue, - 13 => style.bright_magenta, - 14 => style.bright_cyan, - 15 => style.bright_white, - //16-231 are mapped to their RGB colors on a 0-5 range per channel - 16..=231 => { - let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components - let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow - Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color - } - //232-255 are a 24 step grayscale from black to white - 232..=255 => { - let i = *index as u8 - 232; //Align index to 0..24 - let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks - Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale - } - //For compatibility with the alacritty::Colors interface - 256 => style.foreground, - 257 => style.background, - 258 => style.cursor, - 259 => style.dim_black, - 260 => style.dim_red, - 261 => style.dim_green, - 262 => style.dim_yellow, - 263 => style.dim_blue, - 264 => style.dim_magenta, - 265 => style.dim_cyan, - 266 => style.dim_white, - 267 => style.bright_foreground, - 268 => style.black, //'Dim Background', non-standard color - _ => Color::new(0, 0, 0, 255), - } -} -///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube -///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). -/// -///Wikipedia gives a formula for calculating the index for a given color: -/// -///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) -/// -///This function does the reverse, calculating the r, g, and b components from a given index. -fn rgb_for_index(i: &u8) -> (u8, u8, u8) { - debug_assert!((&16..=&231).contains(&i)); - let i = i - 16; - let r = (i - (i % 36)) / 36; - let g = ((i % 36) - (i % 6)) / 6; - let b = (i % 36) % 6; - (r, g, b) -} +// ///Converts an 8 bit ANSI color to it's GPUI equivalent. +// ///Accepts usize for compatibility with the alacritty::Colors interface, +// ///Other than that use case, should only be called with values in the [0,255] range +// pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { +// match index { +// //0-15 are the same as the named colors above +// 0 => style.black, +// 1 => style.red, +// 2 => style.green, +// 3 => style.yellow, +// 4 => style.blue, +// 5 => style.magenta, +// 6 => style.cyan, +// 7 => style.white, +// 8 => style.bright_black, +// 9 => style.bright_red, +// 10 => style.bright_green, +// 11 => style.bright_yellow, +// 12 => style.bright_blue, +// 13 => style.bright_magenta, +// 14 => style.bright_cyan, +// 15 => style.bright_white, +// //16-231 are mapped to their RGB colors on a 0-5 range per channel +// 16..=231 => { +// let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components +// let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow +// Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color +// } +// //232-255 are a 24 step grayscale from black to white +// 232..=255 => { +// let i = *index as u8 - 232; //Align index to 0..24 +// let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks +// Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale +// } +// //For compatibility with the alacritty::Colors interface +// 256 => style.foreground, +// 257 => style.background, +// 258 => style.cursor, +// 259 => style.dim_black, +// 260 => style.dim_red, +// 261 => style.dim_green, +// 262 => style.dim_yellow, +// 263 => style.dim_blue, +// 264 => style.dim_magenta, +// 265 => style.dim_cyan, +// 266 => style.dim_white, +// 267 => style.bright_foreground, +// 268 => style.black, //'Dim Background', non-standard color +// _ => Color::new(0, 0, 0, 255), +// } +// } +// ///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube +// ///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). +// /// +// ///Wikipedia gives a formula for calculating the index for a given color: +// /// +// ///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +// /// +// ///This function does the reverse, calculating the r, g, and b components from a given index. +// fn rgb_for_index(i: &u8) -> (u8, u8, u8) { +// debug_assert!((&16..=&231).contains(&i)); +// let i = i - 16; +// let r = (i - (i % 36)) / 36; +// let g = ((i % 36) - (i % 6)) / 6; +// let b = (i % 36) % 6; +// (r, g, b) +// } -//Convenience method to convert from a GPUI color to an alacritty Rgb -pub fn to_alac_rgb(color: Color) -> AlacRgb { - AlacRgb::new(color.r, color.g, color.g) -} +// //Convenience method to convert from a GPUI color to an alacritty Rgb +// pub fn to_alac_rgb(color: Color) -> AlacRgb { +// AlacRgb::new(color.r, color.g, color.g) +// } -#[cfg(test)] -mod tests { - #[test] - fn test_rgb_for_index() { - //Test every possible value in the color cube - for i in 16..=231 { - let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8)); - assert_eq!(i, 16 + 36 * r + 6 * g + b); - } - } -} +// #[cfg(test)] +// mod tests { +// #[test] +// fn test_rgb_for_index() { +// //Test every possible value in the color cube +// for i in 16..=231 { +// let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8)); +// assert_eq!(i, 16 + 36 * r + 6 * g + b); +// } +// } +// }