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