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