Use buffer language when formatting with Prettier (#43368)

John Gibb and Kirill Bulatov created

Set `prettier_parser` explicitly if the file extension for the buffer
does not match a known one for the current language

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/editor/src/editor_tests.rs | 103 +++++++++++++++++++++++++++++++++
crates/prettier/src/prettier.rs   |  78 +++++++++++++++++++-----
2 files changed, 165 insertions(+), 16 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -19095,6 +19095,109 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
+    });
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file(path!("/file.settings"), Default::default())
+        .await;
+
+    let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+
+    let ts_lang = Arc::new(Language::new(
+        LanguageConfig {
+            name: "TypeScript".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["ts".to_string()],
+                ..LanguageMatcher::default()
+            },
+            prettier_parser_name: Some("typescript".to_string()),
+            ..LanguageConfig::default()
+        },
+        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+    ));
+
+    language_registry.add(ts_lang.clone());
+
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
+    });
+
+    let test_plugin = "test_plugin";
+    let _ = language_registry.register_fake_lsp(
+        "TypeScript",
+        FakeLspAdapter {
+            prettier_plugins: vec![test_plugin],
+            ..Default::default()
+        },
+    );
+
+    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer(path!("/file.settings"), cx)
+        })
+        .await
+        .unwrap();
+
+    project.update(cx, |project, cx| {
+        project.set_language_for_buffer(&buffer, ts_lang, cx)
+    });
+
+    let buffer_text = "one\ntwo\nthree\n";
+    let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
+    editor.update_in(cx, |editor, window, cx| {
+        editor.set_text(buffer_text, window, cx)
+    });
+
+    editor
+        .update_in(cx, |editor, window, cx| {
+            editor.perform_format(
+                project.clone(),
+                FormatTrigger::Manual,
+                FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
+                window,
+                cx,
+            )
+        })
+        .unwrap()
+        .await;
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
+        "Test prettier formatting was not applied to the original buffer text",
+    );
+
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.formatter = Some(FormatterList::default())
+    });
+    let format = editor.update_in(cx, |editor, window, cx| {
+        editor.perform_format(
+            project.clone(),
+            FormatTrigger::Manual,
+            FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
+            window,
+            cx,
+        )
+    });
+    format.await.unwrap();
+
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        buffer_text.to_string()
+            + prettier_format_suffix
+            + "\ntypescript\n"
+            + prettier_format_suffix
+            + "\ntypescript",
+        "Autoformatting (via test prettier) was not applied to the original buffer text",
+    );
+}
+
 #[gpui::test]
 async fn test_addition_reverts(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/prettier/src/prettier.rs 🔗

@@ -2,7 +2,8 @@ use anyhow::Context as _;
 use collections::{HashMap, HashSet};
 use fs::Fs;
 use gpui::{AsyncApp, Entity};
-use language::{Buffer, Diff, language_settings::language_settings};
+use language::language_settings::PrettierSettings;
+use language::{Buffer, Diff, Language, language_settings::language_settings};
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use paths::default_prettier_dir;
@@ -349,7 +350,7 @@ impl Prettier {
             Self::Real(local) => {
                 let params = buffer
                     .update(cx, |buffer, cx| {
-                        let buffer_language = buffer.language();
+                        let buffer_language = buffer.language().map(|language| language.as_ref());
                         let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
                         let prettier_settings = &language_settings.prettier;
                         anyhow::ensure!(
@@ -449,15 +450,7 @@ impl Prettier {
                             })
                             .collect();
 
-                        let mut prettier_parser = prettier_settings.parser.as_deref();
-                        if buffer_path.is_none() {
-                            prettier_parser = prettier_parser.or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));
-                            if prettier_parser.is_none() {
-                                log::error!("Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}");
-                                anyhow::bail!("Cannot determine prettier parser for unsaved file");
-                            }
-
-                        }
+                        let parser = prettier_parser_name(buffer_path.as_deref(), buffer_language, prettier_settings).context("getting prettier parser")?;
 
                         let ignore_path = ignore_dir.and_then(|dir| {
                             let ignore_file = dir.join(".prettierignore");
@@ -475,15 +468,15 @@ impl Prettier {
                         anyhow::Ok(FormatParams {
                             text: buffer.text(),
                             options: FormatOptions {
-                                parser: prettier_parser.map(ToOwned::to_owned),
-                                plugins,
                                 path: buffer_path,
+                                parser,
+                                plugins,
                                 prettier_options,
                                 ignore_path,
                             },
                         })
-                    })?
-                    .context("building prettier request")?;
+                })?
+                .context("building prettier request")?;
 
                 let response = local
                     .server
@@ -503,7 +496,26 @@ impl Prettier {
                     {
                         Some("rust") => anyhow::bail!("prettier does not support Rust"),
                         Some(_other) => {
-                            let formatted_text = buffer.text() + FORMAT_SUFFIX;
+                            let mut formatted_text = buffer.text() + FORMAT_SUFFIX;
+
+                            let buffer_language =
+                                buffer.language().map(|language| language.as_ref());
+                            let language_settings = language_settings(
+                                buffer_language.map(|l| l.name()),
+                                buffer.file(),
+                                cx,
+                            );
+                            let prettier_settings = &language_settings.prettier;
+                            let parser = prettier_parser_name(
+                                buffer_path.as_deref(),
+                                buffer_language,
+                                prettier_settings,
+                            )?;
+
+                            if let Some(parser) = parser {
+                                formatted_text = format!("{formatted_text}\n{parser}");
+                            }
+
                             Ok(buffer.diff(formatted_text, cx))
                         }
                         None => panic!("Should not format buffer without a language with prettier"),
@@ -551,6 +563,40 @@ impl Prettier {
     }
 }
 
+fn prettier_parser_name(
+    buffer_path: Option<&Path>,
+    buffer_language: Option<&Language>,
+    prettier_settings: &PrettierSettings,
+) -> anyhow::Result<Option<String>> {
+    let parser = if buffer_path.is_none() {
+        let parser = prettier_settings
+            .parser
+            .as_deref()
+            .or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));
+        if parser.is_none() {
+            log::error!(
+                "Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}"
+            );
+            anyhow::bail!("Cannot determine prettier parser for unsaved file");
+        }
+        parser
+    } else if let (Some(buffer_language), Some(buffer_path)) = (buffer_language, buffer_path)
+        && buffer_path.extension().is_some_and(|extension| {
+            !buffer_language
+                .config()
+                .matcher
+                .path_suffixes
+                .contains(&extension.to_string_lossy().into_owned())
+        })
+    {
+        buffer_language.prettier_parser_name()
+    } else {
+        prettier_settings.parser.as_deref()
+    };
+
+    Ok(parser.map(ToOwned::to_owned))
+}
+
 async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {
     let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
     if let Some(node_modules_location_metadata) = fs