From d2342b68f609af391bdbb83df4882799f9b5d88f Mon Sep 17 00:00:00 2001 From: loczek <30776250+loczek@users.noreply.github.com> Date: Thu, 8 May 2025 09:32:09 +0200 Subject: [PATCH 1/2] improve snippets ui --- Cargo.lock | 3 + crates/snippets_ui/Cargo.toml | 5 +- crates/snippets_ui/src/snippets_ui.rs | 127 ++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f06b86159187dc..675ae713fe44db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13490,11 +13490,14 @@ dependencies = [ name = "snippets_ui" version = "0.1.0" dependencies = [ + "file_finder", + "file_icons", "fuzzy", "gpui", "language", "paths", "picker", + "settings", "ui", "util", "workspace", diff --git a/crates/snippets_ui/Cargo.toml b/crates/snippets_ui/Cargo.toml index 212eff8312eeac..102374fc73cf8d 100644 --- a/crates/snippets_ui/Cargo.toml +++ b/crates/snippets_ui/Cargo.toml @@ -12,12 +12,15 @@ workspace = true path = "src/snippets_ui.rs" [dependencies] +file_finder.workspace = true +file_icons.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true paths.workspace = true picker.workspace = true +settings.workspace = true ui.workspace = true util.workspace = true -workspace.workspace = true workspace-hack.workspace = true +workspace.workspace = true diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index eb2c0b2030bce2..2fb08b72352013 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -1,16 +1,50 @@ +use file_finder::file_finder_settings::FileFinderSettings; +use file_icons::FileIcons; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{ App, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled, WeakEntity, Window, actions, }; -use language::LanguageRegistry; +use language::{LanguageMatcher, LanguageName, LanguageRegistry}; use paths::config_dir; use picker::{Picker, PickerDelegate}; -use std::{borrow::Borrow, fs, sync::Arc}; +use settings::Settings; +use std::{borrow::Borrow, collections::HashSet, fs, path::Path, sync::Arc}; use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{ModalView, OpenOptions, OpenVisible, Workspace, notifications::NotifyResultExt}; +#[derive(Eq, Hash, PartialEq)] +struct ScopeName(String); + +struct ScopeFileName(String); + +impl ScopeFileName { + fn with_extension(self) -> String { + self.0 + ".json" + } +} + +impl From for ScopeFileName { + fn from(value: ScopeName) -> Self { + if value.0 == "global" { + ScopeFileName("snippets".to_string()) + } else { + ScopeFileName(value.0) + } + } +} + +impl From for ScopeName { + fn from(value: ScopeFileName) -> Self { + if value.0 == "snippets" { + ScopeName("global".to_string()) + } else { + ScopeName(value.0) + } + } +} + actions!(snippets, [ConfigureSnippets, OpenFolder]); pub fn init(cx: &mut App) { @@ -89,6 +123,7 @@ pub struct ScopeSelectorDelegate { candidates: Vec, matches: Vec, selected_index: usize, + existing_scopes: HashSet, } impl ScopeSelectorDelegate { @@ -106,6 +141,24 @@ impl ScopeSelectorDelegate { .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name)) .collect::>(); + let mut existing_scopes = HashSet::new(); + + if let Some(read_dir) = fs::read_dir(config_dir().join("snippets")).log_err() { + for entry in read_dir { + if let Some(entry) = entry.log_err() { + let path = entry.path(); + if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) { + if extension.to_os_string().into_string().unwrap() != "json" { + continue; + } + if let Ok(file_name) = stem.to_os_string().into_string() { + existing_scopes.insert(ScopeFileName(file_name).into()); + } + } + } + } + } + Self { workspace, scope_selector, @@ -113,8 +166,48 @@ impl ScopeSelectorDelegate { candidates, matches: vec![], selected_index: 0, + existing_scopes, } } + + fn scope_data_for_match(&self, mat: &StringMatch, cx: &App) -> (Option, Option) { + let need_icon = FileFinderSettings::get_global(cx).file_icons; + let scope_name = + ScopeName(LanguageName::new(&self.candidates[mat.candidate_id].string).lsp_id()); + + let file_label = if self.existing_scopes.contains(&scope_name) { + Some(ScopeFileName::from(scope_name).with_extension()) + } else { + None + }; + + let icon = need_icon + .then(|| { + let language_name = LanguageName::new(mat.string.as_str()); + self.language_registry + .available_language_for_name(language_name.as_ref()) + .and_then(|available_language| { + self.scope_icon(available_language.matcher(), cx) + }) + .or(Some( + Icon::from_path(IconName::Globe.path()) + .map(|icon| icon.color(Color::Muted)), + )) + }) + .flatten(); + + (file_label, icon) + } + + fn scope_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option { + matcher + .path_suffixes + .iter() + .find_map(|extension| FileIcons::get_icon(Path::new(extension), cx)) + .or(FileIcons::get(cx).get_icon_for_type("default", cx)) + .map(Icon::from_path) + .map(|icon| icon.color(Color::Muted)) + } } impl PickerDelegate for ScopeSelectorDelegate { @@ -135,15 +228,17 @@ impl PickerDelegate for ScopeSelectorDelegate { if let Some(workspace) = self.workspace.upgrade() { cx.spawn_in(window, async move |_, cx| { - let scope = match scope_name.as_str() { - "Global" => "snippets".to_string(), + let scope_file_name = ScopeFileName(match scope_name.to_lowercase().as_str() { + "global" => "snippets".to_string(), _ => language.await?.lsp_id(), - }; + }); workspace.update_in(cx, |workspace, window, cx| { workspace .open_abs_path( - config_dir().join("snippets").join(scope + ".json"), + config_dir() + .join("snippets") + .join(scope_file_name.with_extension()), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() @@ -228,17 +323,31 @@ impl PickerDelegate for ScopeSelectorDelegate { ix: usize, selected: bool, _window: &mut Window, - _: &mut Context>, + cx: &mut Context>, ) -> Option { let mat = &self.matches[ix]; - let label = mat.string.clone(); + let name_label = mat.string.clone(); + let (file_label, language_icon) = self.scope_data_for_match(mat, cx); + + let mut item = h_flex() + .gap_x_2() + .child(HighlightedLabel::new(name_label, mat.positions.clone())); + + if let Some(path_label) = file_label { + item = item.child( + Label::new(path_label) + .color(Color::Muted) + .size(LabelSize::Small), + ); + } Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) - .child(HighlightedLabel::new(label, mat.positions.clone())), + .start_slot::(language_icon) + .child(item), ) } } From a27bc275edd6900b93fe11040196ffbb6b553ec1 Mon Sep 17 00:00:00 2001 From: loczek <30776250+loczek@users.noreply.github.com> Date: Thu, 8 May 2025 09:46:34 +0200 Subject: [PATCH 2/2] replace `config_dir` with `snippets_dir` --- crates/project/src/project.rs | 2 +- crates/snippet_provider/src/lib.rs | 6 ++---- crates/snippets_ui/src/snippets_ui.rs | 12 +++++------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1d950ed651d101..ec262da1f22c3a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -996,7 +996,7 @@ impl Project { let (tx, rx) = mpsc::unbounded(); cx.spawn(async move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx).await) .detach(); - let global_snippets_dir = paths::config_dir().join("snippets"); + let global_snippets_dir = paths::snippets_dir().to_owned(); let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index 2404f3f86ac824..be4541c79e2394 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -141,15 +141,13 @@ struct GlobalSnippetWatcher(Entity); impl GlobalSnippetWatcher { fn new(fs: Arc, cx: &mut App) -> Self { - let global_snippets_dir = paths::config_dir().join("snippets"); + let global_snippets_dir = paths::snippets_dir(); let provider = cx.new(|_cx| SnippetProvider { fs, snippets: Default::default(), watch_tasks: vec![], }); - provider.update(cx, |this, cx| { - this.watch_directory(&global_snippets_dir, cx) - }); + provider.update(cx, |this, cx| this.watch_directory(global_snippets_dir, cx)); Self(provider) } } diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index 2fb08b72352013..81770638e99a0e 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -6,7 +6,7 @@ use gpui::{ WeakEntity, Window, actions, }; use language::{LanguageMatcher, LanguageName, LanguageRegistry}; -use paths::config_dir; +use paths::snippets_dir; use picker::{Picker, PickerDelegate}; use settings::Settings; use std::{borrow::Borrow, collections::HashSet, fs, path::Path, sync::Arc}; @@ -76,8 +76,8 @@ fn open_folder( _: &mut Window, cx: &mut Context, ) { - fs::create_dir_all(config_dir().join("snippets")).notify_err(workspace, cx); - cx.open_with_system(config_dir().join("snippets").borrow()); + fs::create_dir_all(snippets_dir()).notify_err(workspace, cx); + cx.open_with_system(snippets_dir().borrow()); } pub struct ScopeSelector { @@ -143,7 +143,7 @@ impl ScopeSelectorDelegate { let mut existing_scopes = HashSet::new(); - if let Some(read_dir) = fs::read_dir(config_dir().join("snippets")).log_err() { + if let Some(read_dir) = fs::read_dir(snippets_dir()).log_err() { for entry in read_dir { if let Some(entry) = entry.log_err() { let path = entry.path(); @@ -236,9 +236,7 @@ impl PickerDelegate for ScopeSelectorDelegate { workspace.update_in(cx, |workspace, window, cx| { workspace .open_abs_path( - config_dir() - .join("snippets") - .join(scope_file_name.with_extension()), + snippets_dir().join(scope_file_name.with_extension()), OpenOptions { visible: Some(OpenVisible::None), ..Default::default()