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
This commit is contained in:
parent
b3cdd2ccff
commit
0ee1d7ab26
7 changed files with 268 additions and 0 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
22
crates/snippets_ui/Cargo.toml
Normal file
22
crates/snippets_ui/Cargo.toml
Normal file
|
@ -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
|
1
crates/snippets_ui/LICENSE-GPL
Symbolic link
1
crates/snippets_ui/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
226
crates/snippets_ui/src/snippets_ui.rs
Normal file
226
crates/snippets_ui/src/snippets_ui.rs
Normal file
|
@ -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>) {
|
||||
workspace.register_action(configure_snippets);
|
||||
workspace.register_action(open_folder);
|
||||
}
|
||||
|
||||
fn configure_snippets(
|
||||
workspace: &mut Workspace,
|
||||
_: &ConfigureSnippets,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
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<Workspace>) {
|
||||
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<Picker<ScopeSelectorDelegate>>,
|
||||
}
|
||||
|
||||
impl ScopeSelector {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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<DismissEvent> 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<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScopeSelectorDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
scope_selector: WeakView<ScopeSelector>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl ScopeSelectorDelegate {
|
||||
fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
scope_selector: WeakView<ScopeSelector>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) -> 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::<Vec<_>>();
|
||||
|
||||
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<str> {
|
||||
"Select snippet scope...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
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<Picker<Self>>) {
|
||||
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<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> 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<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
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())),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue