Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:55:38.718Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

prettier.rs

1220 lines · 49.7 KB · rust
1use anyhow::Context as _;
2use collections::{HashMap, HashSet};
3use fs::Fs;
4use gpui::{AsyncApp, Entity};
5use language::language_settings::{LanguageSettings, PrettierSettings};
6use language::{Buffer, Diff, Language, OffsetUtf16};
7use lsp::{LanguageServer, LanguageServerId};
8use node_runtime::NodeRuntime;
9use paths::default_prettier_dir;
10use serde::{Deserialize, Serialize};
11use std::{
12    ops::{ControlFlow, Range},
13    path::{Path, PathBuf},
14    sync::Arc,
15    time::Duration,
16};
17use util::{
18    paths::{PathMatcher, PathStyle},
19    rel_path::RelPath,
20};
21
22#[derive(Debug, Clone)]
23pub enum Prettier {
24    Real(RealPrettier),
25    #[cfg(any(test, feature = "test-support"))]
26    Test(TestPrettier),
27}
28
29#[derive(Debug, Clone)]
30pub struct RealPrettier {
31    default: bool,
32    prettier_dir: PathBuf,
33    server: Arc<LanguageServer>,
34}
35
36#[cfg(any(test, feature = "test-support"))]
37#[derive(Debug, Clone)]
38pub struct TestPrettier {
39    prettier_dir: PathBuf,
40    default: bool,
41}
42
43pub const FAIL_THRESHOLD: usize = 4;
44pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
45pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
46const PRETTIER_PACKAGE_NAME: &str = "prettier";
47const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
48
49#[cfg(any(test, feature = "test-support"))]
50pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
51#[cfg(any(test, feature = "test-support"))]
52pub const RANGE_FORMAT_SUFFIX: &str = "\nrange formatted by test prettier";
53
54impl Prettier {
55    pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
56        ".prettierrc",
57        ".prettierrc.json",
58        ".prettierrc.json5",
59        ".prettierrc.yaml",
60        ".prettierrc.yml",
61        ".prettierrc.toml",
62        ".prettierrc.js",
63        ".prettierrc.cjs",
64        ".prettierrc.mjs",
65        ".prettierrc.ts",
66        ".prettierrc.cts",
67        ".prettierrc.mts",
68        "package.json",
69        "prettier.config.js",
70        "prettier.config.cjs",
71        "prettier.config.mjs",
72        "prettier.config.ts",
73        "prettier.config.cts",
74        "prettier.config.mts",
75        ".editorconfig",
76        ".prettierignore",
77    ];
78
79    pub async fn locate_prettier_installation(
80        fs: &dyn Fs,
81        installed_prettiers: &HashSet<PathBuf>,
82        locate_from: &Path,
83    ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
84        let mut path_to_check = locate_from
85            .components()
86            .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
87            .collect::<PathBuf>();
88        if path_to_check != locate_from {
89            log::debug!(
90                "Skipping prettier location for path {path_to_check:?} that is inside node_modules"
91            );
92            return Ok(ControlFlow::Break(()));
93        }
94        let path_to_check_metadata = fs
95            .metadata(&path_to_check)
96            .await
97            .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
98            .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
99        if !path_to_check_metadata.is_dir {
100            path_to_check.pop();
101        }
102
103        let mut closest_package_json_path = None;
104        loop {
105            if installed_prettiers.contains(&path_to_check) {
106                log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
107                return Ok(ControlFlow::Continue(Some(path_to_check)));
108            } else if let Some(package_json_contents) =
109                read_package_json(fs, &path_to_check).await?
110            {
111                if has_prettier_in_node_modules(fs, &path_to_check).await? {
112                    log::debug!("Found prettier path {path_to_check:?} in the node_modules");
113                    return Ok(ControlFlow::Continue(Some(path_to_check)));
114                } else {
115                    match &closest_package_json_path {
116                        None => closest_package_json_path = Some(path_to_check.clone()),
117                        Some(closest_package_json_path) => {
118                            match package_json_contents.get("workspaces") {
119                                Some(serde_json::Value::Array(workspaces)) => {
120                                    let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
121                                    if workspaces.iter().filter_map(|value| {
122                                        if let serde_json::Value::String(s) = value {
123                                            Some(s.clone())
124                                        } else {
125                                            log::warn!("Skipping non-string 'workspaces' value: {value:?}");
126                                            None
127                                        }
128                                    }).any(|workspace_definition| {
129                                        workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and(
130                                            |path_matcher| RelPath::new(subproject_path, PathStyle::local()).is_ok_and(|path|  path_matcher.is_match(path)))
131                                    }) {
132                                        anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?,
133                                            "Path {path_to_check:?} is the workspace root for project in \
134                                            {closest_package_json_path:?}, but it has no prettier installed"
135                                        );
136                                        log::info!(
137                                            "Found prettier path {path_to_check:?} in the workspace \
138                                            root for project in {closest_package_json_path:?}"
139                                        );
140                                        return Ok(ControlFlow::Continue(Some(path_to_check)));
141                                    } else {
142                                        log::warn!(
143                                            "Skipping path {path_to_check:?} workspace root with \
144                                            workspaces {workspaces:?} that have no prettier installed"
145                                        );
146                                    }
147                                }
148                                Some(unknown) => log::error!(
149                                    "Failed to parse workspaces for {path_to_check:?} from package.json, \
150                                    got {unknown:?}. Skipping."
151                                ),
152                                None => log::warn!(
153                                    "Skipping path {path_to_check:?} that has no prettier \
154                                    dependency and no workspaces section in its package.json"
155                                ),
156                            }
157                        }
158                    }
159                }
160            }
161
162            if !path_to_check.pop() {
163                log::debug!("Found no prettier in ancestors of {locate_from:?}");
164                return Ok(ControlFlow::Continue(None));
165            }
166        }
167    }
168
169    pub async fn locate_prettier_ignore(
170        fs: &dyn Fs,
171        prettier_ignores: &HashSet<PathBuf>,
172        locate_from: &Path,
173    ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
174        let mut path_to_check = locate_from
175            .components()
176            .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
177            .collect::<PathBuf>();
178        if path_to_check != locate_from {
179            log::debug!(
180                "Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules"
181            );
182            return Ok(ControlFlow::Break(()));
183        }
184
185        let path_to_check_metadata = fs
186            .metadata(&path_to_check)
187            .await
188            .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
189            .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
190        if !path_to_check_metadata.is_dir {
191            path_to_check.pop();
192        }
193
194        let mut closest_package_json_path = None;
195        loop {
196            if prettier_ignores.contains(&path_to_check) {
197                log::debug!("Found prettier ignore at {path_to_check:?}");
198                return Ok(ControlFlow::Continue(Some(path_to_check)));
199            } else if let Some(package_json_contents) =
200                read_package_json(fs, &path_to_check).await?
201            {
202                let ignore_path = path_to_check.join(".prettierignore");
203                if let Some(metadata) = fs
204                    .metadata(&ignore_path)
205                    .await
206                    .with_context(|| format!("fetching metadata for {ignore_path:?}"))?
207                    && !metadata.is_dir
208                    && !metadata.is_symlink
209                {
210                    log::info!("Found prettier ignore at {ignore_path:?}");
211                    return Ok(ControlFlow::Continue(Some(path_to_check)));
212                }
213                match &closest_package_json_path {
214                    None => closest_package_json_path = Some(path_to_check.clone()),
215                    Some(closest_package_json_path) => {
216                        if let Some(serde_json::Value::Array(workspaces)) =
217                            package_json_contents.get("workspaces")
218                        {
219                            let subproject_path = closest_package_json_path
220                                .strip_prefix(&path_to_check)
221                                .expect("traversing path parents, should be able to strip prefix");
222
223                            if workspaces
224                                .iter()
225                                .filter_map(|value| {
226                                    if let serde_json::Value::String(s) = value {
227                                        Some(s.clone())
228                                    } else {
229                                        log::warn!(
230                                            "Skipping non-string 'workspaces' value: {value:?}"
231                                        );
232                                        None
233                                    }
234                                })
235                                .any(|workspace_definition| {
236                                    workspace_definition == subproject_path.to_string_lossy()
237                                        || PathMatcher::new(
238                                            &[workspace_definition],
239                                            PathStyle::local(),
240                                        )
241                                        .ok()
242                                        .is_some_and(
243                                            |path_matcher| {
244                                                RelPath::new(subproject_path, PathStyle::local())
245                                                    .is_ok_and(|rel_path| {
246                                                        path_matcher.is_match(rel_path)
247                                                    })
248                                            },
249                                        )
250                                })
251                            {
252                                let workspace_ignore = path_to_check.join(".prettierignore");
253                                if let Some(metadata) = fs.metadata(&workspace_ignore).await?
254                                    && !metadata.is_dir
255                                {
256                                    log::info!(
257                                        "Found prettier ignore at workspace root {workspace_ignore:?}"
258                                    );
259                                    return Ok(ControlFlow::Continue(Some(path_to_check)));
260                                }
261                            }
262                        }
263                    }
264                }
265            }
266
267            if !path_to_check.pop() {
268                log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
269                return Ok(ControlFlow::Continue(None));
270            }
271        }
272    }
273
274    #[cfg(any(test, feature = "test-support"))]
275    pub async fn start(
276        _: LanguageServerId,
277        prettier_dir: PathBuf,
278        _: NodeRuntime,
279        _: Duration,
280        _: AsyncApp,
281    ) -> anyhow::Result<Self> {
282        Ok(Self::Test(TestPrettier {
283            default: prettier_dir == default_prettier_dir().as_path(),
284            prettier_dir,
285        }))
286    }
287
288    #[cfg(not(any(test, feature = "test-support")))]
289    pub async fn start(
290        server_id: LanguageServerId,
291        prettier_dir: PathBuf,
292        node: NodeRuntime,
293        request_timeout: Duration,
294        mut cx: AsyncApp,
295    ) -> anyhow::Result<Self> {
296        use lsp::{LanguageServerBinary, LanguageServerName};
297
298        let executor = cx.background_executor().clone();
299        anyhow::ensure!(
300            prettier_dir.is_dir(),
301            "Prettier dir {prettier_dir:?} is not a directory"
302        );
303        let prettier_server = default_prettier_dir().join(PRETTIER_SERVER_FILE);
304        anyhow::ensure!(
305            prettier_server.is_file(),
306            "no prettier server package found at {prettier_server:?}"
307        );
308
309        let node_path = executor
310            .spawn(async move { node.binary_path().await })
311            .await?;
312        let server_name = LanguageServerName("prettier".into());
313        let server_binary = LanguageServerBinary {
314            path: node_path,
315            arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
316            env: None,
317        };
318
319        let server = LanguageServer::new(
320            Arc::new(parking_lot::Mutex::new(None)),
321            server_id,
322            server_name,
323            server_binary,
324            &prettier_dir,
325            None,
326            Default::default(),
327            &mut cx,
328        )
329        .context("prettier server creation")?;
330
331        let server = cx
332            .update(|cx| {
333                let params = server.default_initialize_params(false, false, cx);
334                let configuration = lsp::DidChangeConfigurationParams {
335                    settings: Default::default(),
336                };
337                executor.spawn(server.initialize(params, configuration.into(), request_timeout, cx))
338            })
339            .await
340            .context("prettier server initialization")?;
341        Ok(Self::Real(RealPrettier {
342            server,
343            default: prettier_dir == default_prettier_dir().as_path(),
344            prettier_dir,
345        }))
346    }
347
348    pub async fn format(
349        &self,
350        buffer: &Entity<Buffer>,
351        buffer_path: Option<PathBuf>,
352        ignore_dir: Option<PathBuf>,
353        range_utf16: Option<Range<OffsetUtf16>>,
354        request_timeout: Duration,
355        cx: &mut AsyncApp,
356    ) -> anyhow::Result<Diff> {
357        match self {
358            Self::Real(local) => {
359                let params = buffer
360                    .update(cx, |buffer, cx| {
361                        let buffer_language = buffer.language().map(|language| language.as_ref());
362                        let language_settings = LanguageSettings::for_buffer(&buffer, cx);
363                        let prettier_settings = &language_settings.prettier;
364                        anyhow::ensure!(
365                            prettier_settings.allowed,
366                            "Cannot format: prettier is not allowed for language {buffer_language:?}"
367                        );
368                        let prettier_node_modules = self.prettier_dir().join("node_modules");
369                        anyhow::ensure!(
370                            prettier_node_modules.is_dir(),
371                            "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
372                        );
373                        let plugin_name_into_path = |plugin_name: &str| {
374                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
375                            [
376                                prettier_plugin_dir.join("dist").join("index.mjs"),
377                                prettier_plugin_dir.join("dist").join("index.js"),
378                                prettier_plugin_dir.join("dist").join("plugin.js"),
379                                prettier_plugin_dir.join("src").join("plugin.js"),
380                                prettier_plugin_dir.join("lib").join("index.js"),
381                                prettier_plugin_dir.join("index.mjs"),
382                                prettier_plugin_dir.join("index.js"),
383                                prettier_plugin_dir.join("plugin.js"),
384                                // this one is for @prettier/plugin-php
385                                prettier_plugin_dir.join("standalone.js"),
386                                // this one is for prettier-plugin-latex
387                                prettier_plugin_dir.join("dist").join("prettier-plugin-latex.js"),
388                                prettier_plugin_dir,
389                            ]
390                            .into_iter()
391                            .find(|possible_plugin_path| possible_plugin_path.is_file())
392                        };
393
394                        // Tailwind plugin requires being added last
395                        // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
396                        let mut add_tailwind_back = false;
397
398                        let mut located_plugins = prettier_settings.plugins.iter()
399                            .filter(|plugin_name| {
400                                if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
401                                    add_tailwind_back = true;
402                                    false
403                                } else {
404                                    true
405                                }
406                            })
407                            .map(|plugin_name| {
408                                let plugin_path = plugin_name_into_path(plugin_name);
409                                (plugin_name.clone(), plugin_path)
410                            })
411                            .collect::<Vec<_>>();
412                        if add_tailwind_back {
413                            located_plugins.push((
414                                TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(),
415                                plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME),
416                            ));
417                        }
418
419                        let prettier_options = if self.is_default() {
420                            let mut options = prettier_settings.options.clone();
421                            if !options.contains_key("tabWidth") {
422                                options.insert(
423                                    "tabWidth".to_string(),
424                                    serde_json::Value::Number(serde_json::Number::from(
425                                        language_settings.tab_size.get(),
426                                    )),
427                                );
428                            }
429                            if !options.contains_key("printWidth") {
430                                options.insert(
431                                    "printWidth".to_string(),
432                                    serde_json::Value::Number(serde_json::Number::from(
433                                        language_settings.preferred_line_length,
434                                    )),
435                                );
436                            }
437                            if !options.contains_key("useTabs") {
438                                options.insert(
439                                    "useTabs".to_string(),
440                                    serde_json::Value::Bool(language_settings.hard_tabs),
441                                );
442                            }
443                            Some(options)
444                        } else {
445                            None
446                        };
447
448                        let plugins = located_plugins
449                            .into_iter()
450                            .filter_map(|(plugin_name, located_plugin_path)| {
451                                match located_plugin_path {
452                                    Some(path) => Some(path),
453                                    None => {
454                                        log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
455                                        None
456                                    }
457                                }
458                            })
459                            .collect();
460
461                        let parser = prettier_parser_name(buffer_path.as_deref(), buffer_language, prettier_settings).context("getting prettier parser")?;
462
463                        let ignore_path = ignore_dir.and_then(|dir| {
464                            let ignore_file = dir.join(".prettierignore");
465                            ignore_file.is_file().then_some(ignore_file)
466                        });
467
468                        log::debug!(
469                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
470                            buffer.file().map(|f| f.full_path(cx)),
471                            plugins,
472                            prettier_options,
473                            ignore_path,
474                        );
475
476                        anyhow::Ok(FormatParams {
477                            text: buffer.text(),
478                            options: FormatOptions {
479                                path: buffer_path,
480                                parser,
481                                plugins,
482                                prettier_options,
483                                ignore_path,
484                                range_start: range_utf16.as_ref().map(|r| r.start.0),
485                                range_end: range_utf16.as_ref().map(|r| r.end.0),
486                            },
487                        })
488                })
489                .context("building prettier request")?;
490
491                let response = local
492                    .server
493                    .request::<Format>(params, request_timeout)
494                    .await
495                    .into_response()?;
496                let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx));
497                Ok(diff_task.await)
498            }
499            #[cfg(any(test, feature = "test-support"))]
500            Self::Test(_) => Ok(buffer
501                .update(cx, |buffer, cx| {
502                    match buffer
503                        .language()
504                        .map(|language| language.lsp_id())
505                        .as_deref()
506                    {
507                        Some("rust") => anyhow::bail!("prettier does not support Rust"),
508                        Some(_other) => {
509                            let buffer_language =
510                                buffer.language().map(|language| language.as_ref());
511                            let language_settings = LanguageSettings::for_buffer(buffer, cx);
512                            let prettier_settings = &language_settings.prettier;
513                            let parser = prettier_parser_name(
514                                buffer_path.as_deref(),
515                                buffer_language,
516                                prettier_settings,
517                            )?;
518
519                            let formatted_text = if let Some(range) = &range_utf16 {
520                                let text = buffer.text();
521                                let start_byte = buffer.offset_utf16_to_offset(range.start);
522                                let insert_at = text[start_byte..]
523                                    .find('\n')
524                                    .map(|pos| start_byte + pos)
525                                    .unwrap_or(text.len());
526                                let mut suffix = RANGE_FORMAT_SUFFIX.to_string();
527                                if let Some(parser) = &parser {
528                                    suffix = format!("{suffix}\n{parser}");
529                                }
530                                let mut result = String::new();
531                                result.push_str(&text[..insert_at]);
532                                result.push_str(&suffix);
533                                result.push_str(&text[insert_at..]);
534                                result
535                            } else {
536                                let mut text = buffer.text() + FORMAT_SUFFIX;
537                                if let Some(parser) = &parser {
538                                    text = format!("{text}\n{parser}");
539                                }
540                                text
541                            };
542
543                            Ok(buffer.diff(formatted_text, cx))
544                        }
545                        None => panic!("Should not format buffer without a language with prettier"),
546                    }
547                })?
548                .await),
549        }
550    }
551
552    pub async fn clear_cache(&self, request_timeout: Duration) -> anyhow::Result<()> {
553        match self {
554            Self::Real(local) => local
555                .server
556                .request::<ClearCache>((), request_timeout)
557                .await
558                .into_response()
559                .context("prettier clear cache"),
560            #[cfg(any(test, feature = "test-support"))]
561            Self::Test(_) => Ok(()),
562        }
563    }
564
565    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
566        match self {
567            Self::Real(local) => Some(&local.server),
568            #[cfg(any(test, feature = "test-support"))]
569            Self::Test(_) => None,
570        }
571    }
572
573    pub fn is_default(&self) -> bool {
574        match self {
575            Self::Real(local) => local.default,
576            #[cfg(any(test, feature = "test-support"))]
577            Self::Test(test_prettier) => test_prettier.default,
578        }
579    }
580
581    pub fn prettier_dir(&self) -> &Path {
582        match self {
583            Self::Real(local) => &local.prettier_dir,
584            #[cfg(any(test, feature = "test-support"))]
585            Self::Test(test_prettier) => &test_prettier.prettier_dir,
586        }
587    }
588}
589
590fn prettier_parser_name(
591    buffer_path: Option<&Path>,
592    buffer_language: Option<&Language>,
593    prettier_settings: &PrettierSettings,
594) -> anyhow::Result<Option<String>> {
595    let parser = if buffer_path.is_none() {
596        let parser = prettier_settings
597            .parser
598            .as_deref()
599            .or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));
600        if parser.is_none() {
601            log::error!(
602                "Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}"
603            );
604            anyhow::bail!("Cannot determine prettier parser for unsaved file");
605        }
606        parser
607    } else if let (Some(buffer_language), Some(buffer_path)) = (buffer_language, buffer_path)
608        && buffer_path.extension().is_some_and(|extension| {
609            !buffer_language
610                .config()
611                .matcher
612                .path_suffixes
613                .contains(&extension.to_string_lossy().into_owned())
614        })
615    {
616        buffer_language.prettier_parser_name()
617    } else {
618        prettier_settings.parser.as_deref()
619    };
620
621    Ok(parser.map(ToOwned::to_owned))
622}
623
624async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {
625    let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
626    if let Some(node_modules_location_metadata) = fs
627        .metadata(&possible_node_modules_location)
628        .await
629        .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
630    {
631        return Ok(node_modules_location_metadata.is_dir);
632    }
633    Ok(false)
634}
635
636async fn read_package_json(
637    fs: &dyn Fs,
638    path: &Path,
639) -> anyhow::Result<Option<HashMap<String, serde_json::Value>>> {
640    let possible_package_json = path.join("package.json");
641    if let Some(package_json_metadata) = fs
642        .metadata(&possible_package_json)
643        .await
644        .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?
645        && !package_json_metadata.is_dir
646        && !package_json_metadata.is_symlink
647    {
648        let package_json_contents = fs
649            .load(&possible_package_json)
650            .await
651            .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
652        return serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_json_contents)
653            .map(Some)
654            .with_context(|| format!("parsing {possible_package_json:?} file contents"));
655    }
656    Ok(None)
657}
658
659enum Format {}
660
661#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663struct FormatParams {
664    text: String,
665    options: FormatOptions,
666}
667
668#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
669#[serde(rename_all = "camelCase")]
670struct FormatOptions {
671    plugins: Vec<PathBuf>,
672    parser: Option<String>,
673    #[serde(rename = "filepath")]
674    path: Option<PathBuf>,
675    prettier_options: Option<HashMap<String, serde_json::Value>>,
676    ignore_path: Option<PathBuf>,
677    #[serde(skip_serializing_if = "Option::is_none")]
678    range_start: Option<usize>,
679    #[serde(skip_serializing_if = "Option::is_none")]
680    range_end: Option<usize>,
681}
682
683#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
684#[serde(rename_all = "camelCase")]
685struct FormatResult {
686    text: String,
687}
688
689impl lsp::request::Request for Format {
690    type Params = FormatParams;
691    type Result = FormatResult;
692    const METHOD: &'static str = "prettier/format";
693}
694
695enum ClearCache {}
696
697impl lsp::request::Request for ClearCache {
698    type Params = ();
699    type Result = ();
700    const METHOD: &'static str = "prettier/clear_cache";
701}
702
703#[cfg(test)]
704mod tests {
705    use fs::FakeFs;
706    use serde_json::json;
707
708    use super::*;
709
710    #[gpui::test]
711    async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
712        let fs = FakeFs::new(cx.executor());
713        fs.insert_tree(
714            "/root",
715            json!({
716                ".config": {
717                    "zed": {
718                        "settings.json": r#"{ "formatter": "auto" }"#,
719                    },
720                },
721                "work": {
722                    "project": {
723                        "src": {
724                            "index.js": "// index.js file contents",
725                        },
726                        "node_modules": {
727                            "expect": {
728                                "build": {
729                                    "print.js": "// print.js file contents",
730                                },
731                                "package.json": r#"{
732                                    "devDependencies": {
733                                        "prettier": "2.5.1"
734                                    }
735                                }"#,
736                            },
737                            "prettier": {
738                                "index.js": "// Dummy prettier package file",
739                            },
740                        },
741                        "package.json": r#"{}"#
742                    },
743                }
744            }),
745        )
746        .await;
747
748        assert_eq!(
749            Prettier::locate_prettier_installation(
750                fs.as_ref(),
751                &HashSet::default(),
752                Path::new("/root/.config/zed/settings.json"),
753            )
754            .await
755            .unwrap(),
756            ControlFlow::Continue(None),
757            "Should find no prettier for path hierarchy without it"
758        );
759        assert_eq!(
760            Prettier::locate_prettier_installation(
761                fs.as_ref(),
762                &HashSet::default(),
763                Path::new("/root/work/project/src/index.js")
764            )
765            .await
766            .unwrap(),
767            ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
768            "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
769        );
770        assert_eq!(
771            Prettier::locate_prettier_installation(
772                fs.as_ref(),
773                &HashSet::default(),
774                Path::new("/root/work/project/node_modules/expect/build/print.js")
775            )
776            .await
777            .unwrap(),
778            ControlFlow::Break(()),
779            "Should not format files inside node_modules/"
780        );
781    }
782
783    #[gpui::test]
784    async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
785        let fs = FakeFs::new(cx.executor());
786        fs.insert_tree(
787            "/root",
788            json!({
789                "web_blog": {
790                    "node_modules": {
791                        "prettier": {
792                            "index.js": "// Dummy prettier package file",
793                        },
794                        "expect": {
795                            "build": {
796                                "print.js": "// print.js file contents",
797                            },
798                            "package.json": r#"{
799                                "devDependencies": {
800                                    "prettier": "2.5.1"
801                                }
802                            }"#,
803                        },
804                    },
805                    "pages": {
806                        "[slug].tsx": "// [slug].tsx file contents",
807                    },
808                    "package.json": r#"{
809                        "devDependencies": {
810                            "prettier": "2.3.0"
811                        },
812                        "prettier": {
813                            "semi": false,
814                            "printWidth": 80,
815                            "htmlWhitespaceSensitivity": "strict",
816                            "tabWidth": 4
817                        }
818                    }"#
819                }
820            }),
821        )
822        .await;
823
824        assert_eq!(
825            Prettier::locate_prettier_installation(
826                fs.as_ref(),
827                &HashSet::default(),
828                Path::new("/root/web_blog/pages/[slug].tsx")
829            )
830            .await
831            .unwrap(),
832            ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))),
833            "Should find a preinstalled prettier in the project root"
834        );
835        assert_eq!(
836            Prettier::locate_prettier_installation(
837                fs.as_ref(),
838                &HashSet::default(),
839                Path::new("/root/web_blog/node_modules/expect/build/print.js")
840            )
841            .await
842            .unwrap(),
843            ControlFlow::Break(()),
844            "Should not allow formatting node_modules/ contents"
845        );
846    }
847
848    #[gpui::test]
849    async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
850        let fs = FakeFs::new(cx.executor());
851        fs.insert_tree(
852            "/root",
853            json!({
854                "work": {
855                    "web_blog": {
856                        "node_modules": {
857                            "expect": {
858                                "build": {
859                                    "print.js": "// print.js file contents",
860                                },
861                                "package.json": r#"{
862                                    "devDependencies": {
863                                        "prettier": "2.5.1"
864                                    }
865                                }"#,
866                            },
867                        },
868                        "pages": {
869                            "[slug].tsx": "// [slug].tsx file contents",
870                        },
871                        "package.json": r#"{
872                            "devDependencies": {
873                                "prettier": "2.3.0"
874                            },
875                            "prettier": {
876                                "semi": false,
877                                "printWidth": 80,
878                                "htmlWhitespaceSensitivity": "strict",
879                                "tabWidth": 4
880                            }
881                        }"#
882                    }
883                }
884            }),
885        )
886        .await;
887
888        assert_eq!(
889            Prettier::locate_prettier_installation(
890                fs.as_ref(),
891                &HashSet::default(),
892                Path::new("/root/work/web_blog/pages/[slug].tsx")
893            )
894            .await
895            .unwrap(),
896            ControlFlow::Continue(None),
897            "Should find no prettier when node_modules don't have it"
898        );
899
900        assert_eq!(
901            Prettier::locate_prettier_installation(
902                fs.as_ref(),
903                &HashSet::from_iter(
904                    [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
905                ),
906                Path::new("/root/work/web_blog/pages/[slug].tsx")
907            )
908            .await
909            .unwrap(),
910            ControlFlow::Continue(Some(PathBuf::from("/root/work"))),
911            "Should return closest cached value found without path checks"
912        );
913
914        assert_eq!(
915            Prettier::locate_prettier_installation(
916                fs.as_ref(),
917                &HashSet::default(),
918                Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
919            )
920            .await
921            .unwrap(),
922            ControlFlow::Break(()),
923            "Should not allow formatting files inside node_modules/"
924        );
925        assert_eq!(
926            Prettier::locate_prettier_installation(
927                fs.as_ref(),
928                &HashSet::from_iter(
929                    [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
930                ),
931                Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
932            )
933            .await
934            .unwrap(),
935            ControlFlow::Break(()),
936            "Should ignore cache lookup for files inside node_modules/"
937        );
938    }
939
940    #[gpui::test]
941    async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
942        let fs = FakeFs::new(cx.executor());
943        fs.insert_tree(
944            "/root",
945            json!({
946                "work": {
947                    "full-stack-foundations": {
948                        "exercises": {
949                            "03.loading": {
950                                "01.problem.loader": {
951                                    "app": {
952                                        "routes": {
953                                            "users+": {
954                                                "$username_+": {
955                                                    "notes.tsx": "// notes.tsx file contents",
956                                                },
957                                            },
958                                        },
959                                    },
960                                    "node_modules": {
961                                        "test.js": "// test.js contents",
962                                    },
963                                    "package.json": r#"{
964                                        "devDependencies": {
965                                            "prettier": "^3.0.3"
966                                        }
967                                    }"#
968                                },
969                            },
970                        },
971                        "package.json": r#"{
972                            "workspaces": ["exercises/*/*", "examples/*"]
973                        }"#,
974                        "node_modules": {
975                            "prettier": {
976                                "index.js": "// Dummy prettier package file",
977                            },
978                        },
979                    },
980                }
981            }),
982        )
983        .await;
984
985        assert_eq!(
986            Prettier::locate_prettier_installation(
987                fs.as_ref(),
988                &HashSet::default(),
989                Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"),
990            ).await.unwrap(),
991            ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))),
992            "Should ascend to the multi-workspace root and find the prettier there",
993        );
994
995        assert_eq!(
996            Prettier::locate_prettier_installation(
997                fs.as_ref(),
998                &HashSet::default(),
999                Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js")
1000            )
1001            .await
1002            .unwrap(),
1003            ControlFlow::Break(()),
1004            "Should not allow formatting files inside root node_modules/"
1005        );
1006        assert_eq!(
1007            Prettier::locate_prettier_installation(
1008                fs.as_ref(),
1009                &HashSet::default(),
1010                Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js")
1011            )
1012            .await
1013            .unwrap(),
1014            ControlFlow::Break(()),
1015            "Should not allow formatting files inside submodule's node_modules/"
1016        );
1017    }
1018
1019    #[gpui::test]
1020    async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
1021        cx: &mut gpui::TestAppContext,
1022    ) {
1023        let fs = FakeFs::new(cx.executor());
1024        fs.insert_tree(
1025            "/root",
1026            json!({
1027                "work": {
1028                    "full-stack-foundations": {
1029                        "exercises": {
1030                            "03.loading": {
1031                                "01.problem.loader": {
1032                                    "app": {
1033                                        "routes": {
1034                                            "users+": {
1035                                                "$username_+": {
1036                                                    "notes.tsx": "// notes.tsx file contents",
1037                                                },
1038                                            },
1039                                        },
1040                                    },
1041                                    "node_modules": {},
1042                                    "package.json": r#"{
1043                                        "devDependencies": {
1044                                            "prettier": "^3.0.3"
1045                                        }
1046                                    }"#
1047                                },
1048                            },
1049                        },
1050                        "package.json": r#"{
1051                            "workspaces": ["exercises/*/*", "examples/*"]
1052                        }"#,
1053                    },
1054                }
1055            }),
1056        )
1057        .await;
1058
1059        match Prettier::locate_prettier_installation(
1060            fs.as_ref(),
1061            &HashSet::default(),
1062            Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx")
1063        )
1064        .await {
1065            Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
1066            Err(e) => {
1067                let message = e.to_string().replace("\\\\", "/");
1068                assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined");
1069                assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents");
1070            },
1071        };
1072    }
1073
1074    #[gpui::test]
1075    async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
1076        let fs = FakeFs::new(cx.executor());
1077        fs.insert_tree(
1078            "/root",
1079            json!({
1080                "project": {
1081                    "src": {
1082                        "index.js": "// index.js file contents",
1083                        "ignored.js": "// this file should be ignored",
1084                    },
1085                    ".prettierignore": "ignored.js",
1086                    "package.json": r#"{
1087                        "name": "test-project"
1088                    }"#
1089                }
1090            }),
1091        )
1092        .await;
1093
1094        assert_eq!(
1095            Prettier::locate_prettier_ignore(
1096                fs.as_ref(),
1097                &HashSet::default(),
1098                Path::new("/root/project/src/index.js"),
1099            )
1100            .await
1101            .unwrap(),
1102            ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
1103            "Should find prettierignore in project root"
1104        );
1105    }
1106
1107    #[gpui::test]
1108    async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
1109        cx: &mut gpui::TestAppContext,
1110    ) {
1111        let fs = FakeFs::new(cx.executor());
1112        fs.insert_tree(
1113            "/root",
1114            json!({
1115                "monorepo": {
1116                    "node_modules": {
1117                        "prettier": {
1118                            "index.js": "// Dummy prettier package file",
1119                        }
1120                    },
1121                    "packages": {
1122                        "web": {
1123                            "src": {
1124                                "index.js": "// index.js contents",
1125                                "ignored.js": "// this should be ignored",
1126                            },
1127                            ".prettierignore": "ignored.js",
1128                            "package.json": r#"{
1129                                "name": "web-package"
1130                            }"#
1131                        }
1132                    },
1133                    "package.json": r#"{
1134                        "workspaces": ["packages/*"],
1135                        "devDependencies": {
1136                            "prettier": "^2.0.0"
1137                        }
1138                    }"#
1139                }
1140            }),
1141        )
1142        .await;
1143
1144        assert_eq!(
1145            Prettier::locate_prettier_ignore(
1146                fs.as_ref(),
1147                &HashSet::default(),
1148                Path::new("/root/monorepo/packages/web/src/index.js"),
1149            )
1150            .await
1151            .unwrap(),
1152            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1153            "Should find prettierignore in child package"
1154        );
1155    }
1156
1157    #[gpui::test]
1158    async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
1159        cx: &mut gpui::TestAppContext,
1160    ) {
1161        let fs = FakeFs::new(cx.executor());
1162        fs.insert_tree(
1163            "/root",
1164            json!({
1165                "monorepo": {
1166                    "node_modules": {
1167                        "prettier": {
1168                            "index.js": "// Dummy prettier package file",
1169                        }
1170                    },
1171                    ".prettierignore": "main.js",
1172                    "packages": {
1173                        "web": {
1174                            "src": {
1175                                "main.js": "// this should not be ignored",
1176                                "ignored.js": "// this should be ignored",
1177                            },
1178                            ".prettierignore": "ignored.js",
1179                            "package.json": r#"{
1180                                "name": "web-package"
1181                            }"#
1182                        }
1183                    },
1184                    "package.json": r#"{
1185                        "workspaces": ["packages/*"],
1186                        "devDependencies": {
1187                            "prettier": "^2.0.0"
1188                        }
1189                    }"#
1190                }
1191            }),
1192        )
1193        .await;
1194
1195        assert_eq!(
1196            Prettier::locate_prettier_ignore(
1197                fs.as_ref(),
1198                &HashSet::default(),
1199                Path::new("/root/monorepo/packages/web/src/main.js"),
1200            )
1201            .await
1202            .unwrap(),
1203            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1204            "Should find child package prettierignore first"
1205        );
1206
1207        assert_eq!(
1208            Prettier::locate_prettier_ignore(
1209                fs.as_ref(),
1210                &HashSet::default(),
1211                Path::new("/root/monorepo/packages/web/src/ignored.js"),
1212            )
1213            .await
1214            .unwrap(),
1215            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1216            "Should find child package prettierignore first"
1217        );
1218    }
1219}
1220
Served at tenant.openagents/omega Member data and write actions are omitted.