extension_suggest.rs

  1use std::collections::HashMap;
  2use std::path::Path;
  3use std::sync::{Arc, OnceLock};
  4
  5use db::kvp::KEY_VALUE_STORE;
  6use editor::Editor;
  7use extension::ExtensionStore;
  8use gpui::{Entity, Model, VisualContext};
  9use language::Buffer;
 10use ui::ViewContext;
 11use workspace::{notifications::simple_message_notification, Workspace};
 12
 13fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
 14    static SUGGESTED: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
 15    SUGGESTED.get_or_init(|| {
 16        [
 17            ("astro", "astro"),
 18            ("beancount", "beancount"),
 19            ("clojure", "bb"),
 20            ("clojure", "clj"),
 21            ("clojure", "cljc"),
 22            ("clojure", "cljs"),
 23            ("clojure", "edn"),
 24            ("csharp", "cs"),
 25            ("dockerfile", "Dockerfile"),
 26            ("elisp", "el"),
 27            ("erlang", "erl"),
 28            ("erlang", "hrl"),
 29            ("fish", "fish"),
 30            ("git-firefly", ".gitconfig"),
 31            ("git-firefly", ".gitignore"),
 32            ("git-firefly", "COMMIT_EDITMSG"),
 33            ("git-firefly", "EDIT_DESCRIPTION"),
 34            ("git-firefly", "MERGE_MSG"),
 35            ("git-firefly", "NOTES_EDITMSG"),
 36            ("git-firefly", "TAG_EDITMSG"),
 37            ("git-firefly", "git-rebase-todo"),
 38            ("gleam", "gleam"),
 39            ("graphql", "gql"),
 40            ("graphql", "graphql"),
 41            ("haskell", "hs"),
 42            ("java", "java"),
 43            ("kotlin", "kt"),
 44            ("latex", "tex"),
 45            ("make", "Makefile"),
 46            ("nix", "nix"),
 47            ("php", "php"),
 48            ("prisma", "prisma"),
 49            ("purescript", "purs"),
 50            ("r", "r"),
 51            ("r", "R"),
 52            ("sql", "sql"),
 53            ("svelte", "svelte"),
 54            ("swift", "swift"),
 55            ("templ", "templ"),
 56            ("toml", "Cargo.lock"),
 57            ("toml", "toml"),
 58            ("wgsl", "wgsl"),
 59            ("zig", "zig"),
 60        ]
 61        .into_iter()
 62        .map(|(name, file)| (file, name.into()))
 63        .collect()
 64    })
 65}
 66
 67#[derive(Debug, PartialEq, Eq, Clone)]
 68struct SuggestedExtension {
 69    pub extension_id: Arc<str>,
 70    pub file_name_or_extension: Arc<str>,
 71}
 72
 73/// Returns the suggested extension for the given [`Path`].
 74fn suggested_extension(path: impl AsRef<Path>) -> Option<SuggestedExtension> {
 75    let path = path.as_ref();
 76
 77    let file_extension: Option<Arc<str>> = path
 78        .extension()
 79        .and_then(|extension| Some(extension.to_str()?.into()));
 80    let file_name: Option<Arc<str>> = path
 81        .file_name()
 82        .and_then(|file_name| Some(file_name.to_str()?.into()));
 83
 84    let (file_name_or_extension, extension_id) = None
 85        // We suggest against file names first, as these suggestions will be more
 86        // specific than ones based on the file extension.
 87        .or_else(|| {
 88            file_name.clone().zip(
 89                file_name
 90                    .as_deref()
 91                    .and_then(|file_name| suggested_extensions().get(file_name)),
 92            )
 93        })
 94        .or_else(|| {
 95            file_extension.clone().zip(
 96                file_extension
 97                    .as_deref()
 98                    .and_then(|file_extension| suggested_extensions().get(file_extension)),
 99            )
100        })?;
101
102    Some(SuggestedExtension {
103        extension_id: extension_id.clone(),
104        file_name_or_extension,
105    })
106}
107
108fn language_extension_key(extension_id: &str) -> String {
109    format!("{}_extension_suggest", extension_id)
110}
111
112pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
113    let Some(file) = buffer.read(cx).file().cloned() else {
114        return;
115    };
116
117    let Some(SuggestedExtension {
118        extension_id,
119        file_name_or_extension,
120    }) = suggested_extension(file.path())
121    else {
122        return;
123    };
124
125    let key = language_extension_key(&extension_id);
126    let Ok(None) = KEY_VALUE_STORE.read_kvp(&key) else {
127        return;
128    };
129
130    cx.on_next_frame(move |workspace, cx| {
131        let Some(editor) = workspace.active_item_as::<Editor>(cx) else {
132            return;
133        };
134
135        if editor.read(cx).buffer().read(cx).as_singleton().as_ref() != Some(&buffer) {
136            return;
137        }
138
139        workspace.show_notification(buffer.entity_id().as_u64() as usize, cx, |cx| {
140            cx.new_view(move |_cx| {
141                simple_message_notification::MessageNotification::new(format!(
142                    "Do you want to install the recommended '{}' extension for '{}' files?",
143                    extension_id, file_name_or_extension
144                ))
145                .with_click_message("Yes")
146                .on_click({
147                    let extension_id = extension_id.clone();
148                    move |cx| {
149                        let extension_id = extension_id.clone();
150                        let extension_store = ExtensionStore::global(cx);
151                        extension_store.update(cx, move |store, cx| {
152                            store.install_latest_extension(extension_id, cx);
153                        });
154                    }
155                })
156                .with_secondary_click_message("No")
157                .on_secondary_click(move |cx| {
158                    let key = language_extension_key(&extension_id);
159                    db::write_and_log(cx, move || {
160                        KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string())
161                    });
162                })
163            })
164        });
165    })
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    pub fn test_suggested_extension() {
174        assert_eq!(
175            suggested_extension("Cargo.toml"),
176            Some(SuggestedExtension {
177                extension_id: "toml".into(),
178                file_name_or_extension: "toml".into()
179            })
180        );
181        assert_eq!(
182            suggested_extension("Cargo.lock"),
183            Some(SuggestedExtension {
184                extension_id: "toml".into(),
185                file_name_or_extension: "Cargo.lock".into()
186            })
187        );
188        assert_eq!(
189            suggested_extension("Dockerfile"),
190            Some(SuggestedExtension {
191                extension_id: "dockerfile".into(),
192                file_name_or_extension: "Dockerfile".into()
193            })
194        );
195        assert_eq!(
196            suggested_extension("a/b/c/d/.gitignore"),
197            Some(SuggestedExtension {
198                extension_id: "git-firefly".into(),
199                file_name_or_extension: ".gitignore".into()
200            })
201        );
202        assert_eq!(
203            suggested_extension("a/b/c/d/test.gleam"),
204            Some(SuggestedExtension {
205                extension_id: "gleam".into(),
206                file_name_or_extension: "gleam".into()
207            })
208        );
209    }
210}