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