1use std::{
2 collections::HashMap,
3 sync::{Arc, OnceLock},
4};
5
6use db::kvp::KEY_VALUE_STORE;
7
8use editor::Editor;
9use extension::ExtensionStore;
10use gpui::{Entity, Model, VisualContext};
11use language::Buffer;
12use ui::ViewContext;
13use workspace::{notifications::simple_message_notification, Workspace};
14
15pub fn suggested_extension(file_extension_or_name: &str) -> Option<Arc<str>> {
16 static SUGGESTED: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
17 SUGGESTED
18 .get_or_init(|| {
19 [
20 ("astro", "astro"),
21 ("beancount", "beancount"),
22 ("dockerfile", "Dockerfile"),
23 ("elisp", "el"),
24 ("fish", "fish"),
25 ("git-firefly", ".gitconfig"),
26 ("git-firefly", ".gitignore"),
27 ("git-firefly", "COMMIT_EDITMSG"),
28 ("git-firefly", "EDIT_DESCRIPTION"),
29 ("git-firefly", "git-rebase-todo"),
30 ("git-firefly", "MERGE_MSG"),
31 ("git-firefly", "NOTES_EDITMSG"),
32 ("git-firefly", "TAG_EDITMSG"),
33 ("gleam", "gleam"),
34 ("graphql", "gql"),
35 ("graphql", "graphql"),
36 ("haskell", "hs"),
37 ("java", "java"),
38 ("kotlin", "kt"),
39 ("latex", "tex"),
40 ("make", "Makefile"),
41 ("nix", "nix"),
42 ("prisma", "prisma"),
43 ("purescript", "purs"),
44 ("r", "r"),
45 ("r", "R"),
46 ("sql", "sql"),
47 ("svelte", "svelte"),
48 ("swift", "swift"),
49 ("templ", "templ"),
50 ("wgsl", "wgsl"),
51 ("zig", "zig"),
52 ]
53 .into_iter()
54 .map(|(name, file)| (file, name.into()))
55 .collect::<HashMap<&str, Arc<str>>>()
56 })
57 .get(file_extension_or_name)
58 .map(|str| str.clone())
59}
60
61fn language_extension_key(extension_id: &str) -> String {
62 format!("{}_extension_suggest", extension_id)
63}
64
65pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
66 let Some(file_name_or_extension) = buffer.read(cx).file().and_then(|file| {
67 Some(match file.path().extension() {
68 Some(extension) => extension.to_str()?.to_string(),
69 None => file.path().to_str()?.to_string(),
70 })
71 }) else {
72 return;
73 };
74
75 let Some(extension_id) = suggested_extension(&file_name_or_extension) else {
76 return;
77 };
78
79 let key = language_extension_key(&extension_id);
80 let value = KEY_VALUE_STORE.read_kvp(&key);
81
82 if value.is_err() || value.unwrap().is_some() {
83 return;
84 }
85
86 cx.on_next_frame(move |workspace, cx| {
87 let Some(editor) = workspace.active_item_as::<Editor>(cx) else {
88 return;
89 };
90
91 if editor.read(cx).buffer().read(cx).as_singleton().as_ref() != Some(&buffer) {
92 return;
93 }
94
95 workspace.show_notification(buffer.entity_id().as_u64() as usize, cx, |cx| {
96 cx.new_view(move |_cx| {
97 simple_message_notification::MessageNotification::new(format!(
98 "Do you want to install the recommended '{}' extension?",
99 file_name_or_extension
100 ))
101 .with_click_message("Yes")
102 .on_click({
103 let extension_id = extension_id.clone();
104 move |cx| {
105 let extension_id = extension_id.clone();
106 let extension_store = ExtensionStore::global(cx);
107 extension_store.update(cx, move |store, cx| {
108 store.install_latest_extension(extension_id, cx);
109 });
110 }
111 })
112 .with_secondary_click_message("No")
113 .on_secondary_click(move |cx| {
114 let key = language_extension_key(&extension_id);
115 db::write_and_log(cx, move || {
116 KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string())
117 });
118 })
119 })
120 });
121 })
122}