From 0ee1d7ab2634521e11cb7f221074b0287763359d Mon Sep 17 00:00:00 2001 From: loczek <30776250+loczek@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:27:16 +0200 Subject: [PATCH] Add snippet commands (#18453) Closes #17860 Closes #15403 Release Notes: - Added `snippets: configure snippets` command to create and modify snippets - Added `snippets: open folder` command for opening the `~/.config/zed/snippets` directory https://github.com/user-attachments/assets/fd9e664c-44b1-49bf-87a8-42b9e516f12f --- Cargo.lock | 15 ++ Cargo.toml | 2 + crates/snippets_ui/Cargo.toml | 22 +++ crates/snippets_ui/LICENSE-GPL | 1 + crates/snippets_ui/src/snippets_ui.rs | 226 ++++++++++++++++++++++++++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 7 files changed, 268 insertions(+) create mode 100644 crates/snippets_ui/Cargo.toml create mode 120000 crates/snippets_ui/LICENSE-GPL create mode 100644 crates/snippets_ui/src/snippets_ui.rs diff --git a/Cargo.lock b/Cargo.lock index 123141d188..7c92ef0f52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10500,6 +10500,20 @@ dependencies = [ "util", ] +[[package]] +name = "snippets_ui" +version = "0.1.0" +dependencies = [ + "fuzzy", + "gpui", + "language", + "paths", + "picker", + "ui", + "util", + "workspace", +] + [[package]] name = "socket2" version = "0.4.10" @@ -14468,6 +14482,7 @@ dependencies = [ "simplelog", "smol", "snippet_provider", + "snippets_ui", "supermaven", "sysinfo", "tab_switcher", diff --git a/Cargo.toml b/Cargo.toml index c72fec020f..1ef14dae70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ members = [ "crates/settings_ui", "crates/snippet", "crates/snippet_provider", + "crates/snippets_ui", "crates/sqlez", "crates/sqlez_macros", "crates/story", @@ -275,6 +276,7 @@ settings = { path = "crates/settings" } settings_ui = { path = "crates/settings_ui" } snippet = { path = "crates/snippet" } snippet_provider = { path = "crates/snippet_provider" } +snippets_ui = { path = "crates/snippets_ui" } sqlez = { path = "crates/sqlez" } sqlez_macros = { path = "crates/sqlez_macros" } story = { path = "crates/story" } diff --git a/crates/snippets_ui/Cargo.toml b/crates/snippets_ui/Cargo.toml new file mode 100644 index 0000000000..da9eff4ae5 --- /dev/null +++ b/crates/snippets_ui/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "snippets_ui" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/snippets_ui.rs" + +[dependencies] +fuzzy.workspace = true +gpui.workspace = true +language.workspace = true +paths.workspace = true +picker.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true diff --git a/crates/snippets_ui/LICENSE-GPL b/crates/snippets_ui/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/snippets_ui/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs new file mode 100644 index 0000000000..c8ab6febda --- /dev/null +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -0,0 +1,226 @@ +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Styled, + View, ViewContext, VisualContext, WeakView, +}; +use language::LanguageRegistry; +use paths::config_dir; +use picker::{Picker, PickerDelegate}; +use std::{borrow::Borrow, fs, sync::Arc}; +use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, WindowContext}; +use util::ResultExt; +use workspace::{notifications::NotifyResultExt, ModalView, Workspace}; + +actions!(snippets, [ConfigureSnippets, OpenFolder]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(register).detach(); +} + +fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(configure_snippets); + workspace.register_action(open_folder); +} + +fn configure_snippets( + workspace: &mut Workspace, + _: &ConfigureSnippets, + cx: &mut ViewContext, +) { + let language_registry = workspace.app_state().languages.clone(); + let workspace_handle = workspace.weak_handle(); + + workspace.toggle_modal(cx, move |cx| { + ScopeSelector::new(language_registry, workspace_handle, cx) + }); +} + +fn open_folder(workspace: &mut Workspace, _: &OpenFolder, cx: &mut ViewContext) { + fs::create_dir_all(config_dir().join("snippets")).notify_err(workspace, cx); + cx.open_with_system(config_dir().join("snippets").borrow()); +} + +pub struct ScopeSelector { + picker: View>, +} + +impl ScopeSelector { + fn new( + language_registry: Arc, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let delegate = + ScopeSelectorDelegate::new(workspace, cx.view().downgrade(), language_registry); + + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + + Self { picker } + } +} + +impl ModalView for ScopeSelector {} + +impl EventEmitter for ScopeSelector {} + +impl FocusableView for ScopeSelector { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for ScopeSelector { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + v_flex().w(rems(34.)).child(self.picker.clone()) + } +} + +pub struct ScopeSelectorDelegate { + workspace: WeakView, + scope_selector: WeakView, + language_registry: Arc, + candidates: Vec, + matches: Vec, + selected_index: usize, +} + +impl ScopeSelectorDelegate { + fn new( + workspace: WeakView, + scope_selector: WeakView, + language_registry: Arc, + ) -> Self { + let candidates = Vec::from(["Global".to_string()]).into_iter(); + let languages = language_registry.language_names().into_iter(); + + let candidates = candidates + .chain(languages) + .enumerate() + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) + .collect::>(); + + Self { + workspace, + scope_selector, + language_registry, + candidates, + matches: vec![], + selected_index: 0, + } + } +} + +impl PickerDelegate for ScopeSelectorDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self, _: &mut WindowContext) -> Arc { + "Select snippet scope...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if let Some(mat) = self.matches.get(self.selected_index) { + let scope_name = self.candidates[mat.candidate_id].string.clone(); + let language = self.language_registry.language_for_name(&scope_name); + + if let Some(workspace) = self.workspace.upgrade() { + cx.spawn(|_, mut cx| async move { + let scope = match scope_name.as_str() { + "Global" => "snippets".to_string(), + _ => language.await?.lsp_id(), + }; + + workspace.update(&mut cx, |workspace, cx| { + workspace + .open_abs_path( + config_dir().join("snippets").join(scope + ".json"), + false, + cx, + ) + .detach(); + }) + }) + .detach_and_log_err(cx); + }; + } + self.dismissed(cx); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.scope_selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let background = cx.background_executor().clone(); + let candidates = self.candidates.clone(); + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + let delegate = &mut this.delegate; + delegate.matches = matches; + delegate.selected_index = delegate + .selected_index + .min(delegate.matches.len().saturating_sub(1)); + cx.notify(); + }) + .log_err(); + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let mat = &self.matches[ix]; + let label = mat.string.clone(); + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child(HighlightedLabel::new(label, mat.positions.clone())), + ) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5422f8b29a..775a59e475 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -96,6 +96,7 @@ shellexpand.workspace = true simplelog.workspace = true smol.workspace = true snippet_provider.workspace = true +snippets_ui.workspace = true supermaven.workspace = true sysinfo.workspace = true tab_switcher.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 186805d12c..06f1d926ae 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -256,6 +256,7 @@ fn init_ui( project_panel::init(Assets, cx); outline_panel::init(Assets, cx); tasks_ui::init(cx); + snippets_ui::init(cx); channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx); search::init(cx); vim::init(cx);