Skip to content

snippets: Add icons and file names to snippet scope selector #30212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/project/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 2 additions & 4 deletions crates/snippet_provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,13 @@ struct GlobalSnippetWatcher(Entity<SnippetProvider>);

impl GlobalSnippetWatcher {
fn new(fs: Arc<dyn Fs>, 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)
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/snippets_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
131 changes: 119 additions & 12 deletions crates/snippets_ui/src/snippets_ui.rs
Original file line number Diff line number Diff line change
@@ -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 paths::config_dir;
use language::{LanguageMatcher, LanguageName, LanguageRegistry};
use paths::snippets_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<ScopeName> for ScopeFileName {
fn from(value: ScopeName) -> Self {
if value.0 == "global" {
ScopeFileName("snippets".to_string())
} else {
ScopeFileName(value.0)
}
}
}

impl From<ScopeFileName> 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) {
Expand Down Expand Up @@ -42,8 +76,8 @@ fn open_folder(
_: &mut Window,
cx: &mut Context<Workspace>,
) {
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 {
Expand Down Expand Up @@ -89,6 +123,7 @@ pub struct ScopeSelectorDelegate {
candidates: Vec<StringMatchCandidate>,
matches: Vec<StringMatch>,
selected_index: usize,
existing_scopes: HashSet<ScopeName>,
}

impl ScopeSelectorDelegate {
Expand All @@ -106,15 +141,73 @@ impl ScopeSelectorDelegate {
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
.collect::<Vec<_>>();

let mut existing_scopes = HashSet::new();

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();
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,
language_registry,
candidates,
matches: vec![],
selected_index: 0,
existing_scopes,
}
}

fn scope_data_for_match(&self, mat: &StringMatch, cx: &App) -> (Option<String>, Option<Icon>) {
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<Icon> {
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 {
Expand All @@ -135,15 +228,15 @@ 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"),
snippets_dir().join(scope_file_name.with_extension()),
OpenOptions {
visible: Some(OpenVisible::None),
..Default::default()
Expand Down Expand Up @@ -228,17 +321,31 @@ impl PickerDelegate for ScopeSelectorDelegate {
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
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::<Icon>(language_icon)
.child(item),
)
}
}
Loading