html: Improve settings, formatting and user binaries (#27524)

Peter Tripp created

Added support for using `language_server` as HTML formatter.
Added support for finding `vscode-html-language-server` in user's path.

Release Notes:

- N/A

Change summary

docs/src/languages/html.md  | 49 +++++++++++++++++++++++++++++++++++++++
extensions/html/src/html.rs | 30 +++++++++++++++++------
2 files changed, 71 insertions(+), 8 deletions(-)

Detailed changes

docs/src/languages/html.md 🔗

@@ -17,6 +17,55 @@ If you do not want to use the HTML extension, you can add the following to your
 }
 ```
 
+## Formatting
+
+By default Zed uses [Prettier](https://prettier.io/) for formatting HTML
+
+You can disable `format_on_save` by adding the following to your Zed settings:
+
+```json
+  "languages": {
+    "HTML": {
+      "format_on_save": "off",
+    }
+  }
+```
+
+You can still trigger formatting manually with {#kb editor::Format} or by opening the command palette ( {#kb commandPalette::Toggle} and selecting `Format Document`.
+
+### LSP Formatting
+
+If you prefer you can use `vscode-html-language-server` instead of Prettier for auto-formatting by adding the following to your Zed settings:
+
+```json
+  "languages": {
+    "HTML": {
+      "formatter": "language_server",
+    }
+  }
+```
+
+You can customize various [formatting options](https://code.visualstudio.com/docs/languages/html#_formatting) for `vscode-html-language-server` via Zed settings.json:
+
+```json
+  "lsp": {
+    "vscode-html-language-server": {
+      "settings": {
+        "html": {
+          "format": {
+            // Indent under <html> and <head> (default: false)
+            "indentInnerHtml": true,
+            // Disable formatting inside <svg> or <script>
+            "contentUnformatted": "svg,script",
+            // Add an extra newline before <div> and <p>
+            "extraLiners": "div,p"
+          }
+        }
+      }
+    }
+  }
+```
+
 ## See also:
 
 - [CSS](./css.md)

extensions/html/src/html.rs 🔗

@@ -1,13 +1,14 @@
 use std::{env, fs};
 use zed::settings::LspSettings;
-use zed_extension_api::{self as zed, LanguageServerId, Result};
+use zed_extension_api::{self as zed, serde_json::json, LanguageServerId, Result};
 
+const BINARY_NAME: &str = "vscode-html-language-server";
 const SERVER_PATH: &str =
     "node_modules/@zed-industries/vscode-langservers-extracted/bin/vscode-html-language-server";
 const PACKAGE_NAME: &str = "@zed-industries/vscode-langservers-extracted";
 
 struct HtmlExtension {
-    did_find_server: bool,
+    cached_binary_path: Option<String>,
 }
 
 impl HtmlExtension {
@@ -17,7 +18,7 @@ impl HtmlExtension {
 
     fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
         let server_exists = self.server_exists();
-        if self.did_find_server && server_exists {
+        if self.cached_binary_path.is_some() && server_exists {
             return Ok(SERVER_PATH.to_string());
         }
 
@@ -50,8 +51,6 @@ impl HtmlExtension {
                 }
             }
         }
-
-        self.did_find_server = true;
         Ok(SERVER_PATH.to_string())
     }
 }
@@ -59,16 +58,22 @@ impl HtmlExtension {
 impl zed::Extension for HtmlExtension {
     fn new() -> Self {
         Self {
-            did_find_server: false,
+            cached_binary_path: None,
         }
     }
 
     fn language_server_command(
         &mut self,
         language_server_id: &LanguageServerId,
-        _worktree: &zed::Worktree,
+        worktree: &zed::Worktree,
     ) -> Result<zed::Command> {
-        let server_path = self.server_script_path(language_server_id)?;
+        let server_path = if let Some(path) = worktree.which(BINARY_NAME) {
+            path
+        } else {
+            self.server_script_path(language_server_id)?
+        };
+        self.cached_binary_path = Some(server_path.clone());
+
         Ok(zed::Command {
             command: zed::node_binary_path()?,
             args: vec![
@@ -94,6 +99,15 @@ impl zed::Extension for HtmlExtension {
             .unwrap_or_default();
         Ok(Some(settings))
     }
+
+    fn language_server_initialization_options(
+        &mut self,
+        _server_id: &LanguageServerId,
+        _worktree: &zed_extension_api::Worktree,
+    ) -> Result<Option<zed_extension_api::serde_json::Value>> {
+        let initialization_options = json!({"provideFormatter": true });
+        Ok(Some(initialization_options))
+    }
 }
 
 zed::register_extension!(HtmlExtension);