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",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snippets_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fuzzy",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"paths",
|
||||||
|
"picker",
|
||||||
|
"ui",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
|
@ -14468,6 +14482,7 @@ dependencies = [
|
||||||
"simplelog",
|
"simplelog",
|
||||||
"smol",
|
"smol",
|
||||||
"snippet_provider",
|
"snippet_provider",
|
||||||
|
"snippets_ui",
|
||||||
"supermaven",
|
"supermaven",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tab_switcher",
|
"tab_switcher",
|
||||||
|
|
|
@ -99,6 +99,7 @@ members = [
|
||||||
"crates/settings_ui",
|
"crates/settings_ui",
|
||||||
"crates/snippet",
|
"crates/snippet",
|
||||||
"crates/snippet_provider",
|
"crates/snippet_provider",
|
||||||
|
"crates/snippets_ui",
|
||||||
"crates/sqlez",
|
"crates/sqlez",
|
||||||
"crates/sqlez_macros",
|
"crates/sqlez_macros",
|
||||||
"crates/story",
|
"crates/story",
|
||||||
|
@ -275,6 +276,7 @@ settings = { path = "crates/settings" }
|
||||||
settings_ui = { path = "crates/settings_ui" }
|
settings_ui = { path = "crates/settings_ui" }
|
||||||
snippet = { path = "crates/snippet" }
|
snippet = { path = "crates/snippet" }
|
||||||
snippet_provider = { path = "crates/snippet_provider" }
|
snippet_provider = { path = "crates/snippet_provider" }
|
||||||
|
snippets_ui = { path = "crates/snippets_ui" }
|
||||||
sqlez = { path = "crates/sqlez" }
|
sqlez = { path = "crates/sqlez" }
|
||||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||||
story = { path = "crates/story" }
|
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
|
simplelog.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
snippet_provider.workspace = true
|
snippet_provider.workspace = true
|
||||||
|
snippets_ui.workspace = true
|
||||||
supermaven.workspace = true
|
supermaven.workspace = true
|
||||||
sysinfo.workspace = true
|
sysinfo.workspace = true
|
||||||
tab_switcher.workspace = true
|
tab_switcher.workspace = true
|
||||||
|
|
|
@ -256,6 +256,7 @@ fn init_ui(
|
||||||
project_panel::init(Assets, cx);
|
project_panel::init(Assets, cx);
|
||||||
outline_panel::init(Assets, cx);
|
outline_panel::init(Assets, cx);
|
||||||
tasks_ui::init(cx);
|
tasks_ui::init(cx);
|
||||||
|
snippets_ui::init(cx);
|
||||||
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
|
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
search::init(cx);
|
search::init(cx);
|
||||||
vim::init(cx);
|
vim::init(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue