gleam: Add `/gleam-docs` (#13721)

Marshall Bowers created

This PR adds a `/gleam-docs` slash command to the Gleam extension, which
can be used to fetch docs from HexDocs.

Release Notes:

- N/A

Change summary

Cargo.lock                      | 17 ++++++++++-
extensions/gleam/Cargo.toml     |  1 
extensions/gleam/extension.toml |  5 +++
extensions/gleam/src/gleam.rs   | 52 +++++++++++++++++++++++++++++++++-
4 files changed, 71 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -388,7 +388,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "heed",
- "html_to_markdown",
+ "html_to_markdown 0.1.0",
  "http 0.1.0",
  "indoc",
  "language",
@@ -5237,6 +5237,18 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "html_to_markdown"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e608e8dd0939bfb6b516d96a5919751b835297a02230aecb88d2fc84ebebaa8a"
+dependencies = [
+ "anyhow",
+ "html5ever",
+ "markup5ever_rcdom",
+ "regex",
+]
+
 [[package]]
 name = "http"
 version = "0.1.0"
@@ -9019,7 +9031,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "heed",
- "html_to_markdown",
+ "html_to_markdown 0.1.0",
  "http 0.1.0",
  "indexmap 1.9.3",
  "indoc",
@@ -13779,6 +13791,7 @@ dependencies = [
 name = "zed_gleam"
 version = "0.1.3"
 dependencies = [
+ "html_to_markdown 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "zed_extension_api 0.0.7",
 ]
 

extensions/gleam/Cargo.toml 🔗

@@ -13,4 +13,5 @@ path = "src/gleam.rs"
 crate-type = ["cdylib"]
 
 [dependencies]
+html_to_markdown = "0.1.0"
 zed_extension_api = { path = "../../crates/extension_api" }

extensions/gleam/extension.toml 🔗

@@ -18,3 +18,8 @@ commit = "8432ffe32ccd360534837256747beb5b1c82fca1"
 description = "Returns information about the current Gleam project."
 requires_argument = false
 tooltip_text = "Insert Gleam project data"
+
+[slash_commands.gleam-docs]
+description = "Returns Gleam docs."
+requires_argument = true
+tooltip_text = "Insert Gleam docs"

extensions/gleam/src/gleam.rs 🔗

@@ -1,10 +1,13 @@
+use html_to_markdown::{convert_html_to_markdown, TagHandler};
+use std::cell::RefCell;
 use std::fs;
+use std::rc::Rc;
 use zed::lsp::CompletionKind;
 use zed::{
     CodeLabel, CodeLabelSpan, LanguageServerId, SlashCommand, SlashCommandOutput,
     SlashCommandOutputSection,
 };
-use zed_extension_api::{self as zed, Result};
+use zed_extension_api::{self as zed, fetch, HttpRequest, Result};
 
 struct GleamExtension {
     cached_binary_path: Option<String>,
@@ -164,10 +167,55 @@ impl zed::Extension for GleamExtension {
     fn run_slash_command(
         &self,
         command: SlashCommand,
-        _argument: Option<String>,
+        argument: Option<String>,
         worktree: &zed::Worktree,
     ) -> Result<SlashCommandOutput, String> {
         match command.name.as_str() {
+            "gleam-docs" => {
+                let argument = argument.ok_or_else(|| "missing argument".to_string())?;
+
+                let mut components = argument.split('/');
+                let package_name = components
+                    .next()
+                    .ok_or_else(|| "missing package name".to_string())?;
+                let module_path = components.map(ToString::to_string).collect::<Vec<_>>();
+
+                let response = fetch(&HttpRequest {
+                    url: format!(
+                        "https://hexdocs.pm/{package_name}{maybe_path}",
+                        maybe_path = if !module_path.is_empty() {
+                            format!("/{}.html", module_path.join("/"))
+                        } else {
+                            String::new()
+                        }
+                    ),
+                })?;
+
+                let mut handlers: Vec<TagHandler> = vec![
+                    Rc::new(RefCell::new(
+                        html_to_markdown::markdown::WebpageChromeRemover,
+                    )),
+                    Rc::new(RefCell::new(html_to_markdown::markdown::ParagraphHandler)),
+                    Rc::new(RefCell::new(html_to_markdown::markdown::HeadingHandler)),
+                    Rc::new(RefCell::new(html_to_markdown::markdown::ListHandler)),
+                    Rc::new(RefCell::new(html_to_markdown::markdown::TableHandler::new())),
+                    Rc::new(RefCell::new(html_to_markdown::markdown::StyledTextHandler)),
+                ];
+
+                let markdown = convert_html_to_markdown(response.body.as_bytes(), &mut handlers)
+                    .map_err(|err| format!("failed to convert docs to Markdown {err}"))?;
+
+                let mut text = String::new();
+                text.push_str(&markdown);
+
+                Ok(SlashCommandOutput {
+                    sections: vec![SlashCommandOutputSection {
+                        range: (0..text.len()).into(),
+                        label: format!("gleam-docs: {package_name} {}", module_path.join("/")),
+                    }],
+                    text,
+                })
+            }
             "gleam-project" => {
                 let mut text = String::new();
                 text.push_str("You are in a Gleam project.\n");