Add editor::InsertSnippet action (#44428)
Closes #20036 This introduces new action `editor: insert snippet`. It supports three modes: ``` ["editor::InsertSnippet", {"name": "snippet_name"}] ["editor::InsertSnippet", {"language": "language_name", "name": "snippet_name"}] ["editor::InsertSnippet", {"snippet": "snippet with $1 tab stops"}] ``` ## Example usage ### `keymap.json` ```json { "context": "Editor", "bindings": { // named global snippet "cmd-k cmd-r": ["editor::InsertSnippet", {"name": "all rights reserved"}], // named language-specific snippet "cmd-k cmd-p": ["editor::InsertSnippet", {"language": "rust", "name": "debug-print a value"}], // inline snippet "cmd-k cmd-e": ["editor::InsertSnippet", {"snippet": "println!(\"This snippet has multiple lines.\")\nprintln!(\"It belongs to $1 and is very $2.\")"}], }, }, ``` ### `~/.config/zed/snippets/rust.json` ```json { "debug-print a value": { "body": "println!(\"$1 = {:?}\", $1)", }, } ``` ### `~/.config/zed/snippets/snippets.json` ```json { "all rights reserved": { "body": "Copyright © ${1:2025} ${2:your name}. All rights reserved.", }, } ``` ## Future extensions - Support multiline inline snippets using an array of strings using something similar to `ListOrDirect` in `snippet_provider::format::VsCodeSnippet` - When called with no arguments, open a modal to select a snippet to insert ## Release notes Release Notes: - Added `editor::InsertSnippet` action
This commit is contained in:
@@ -327,6 +327,23 @@ pub struct AddSelectionBelow {
|
||||
pub skip_soft_wrap: bool,
|
||||
}
|
||||
|
||||
/// Inserts a snippet at the cursor.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct InsertSnippet {
|
||||
/// Language name if using a named snippet, or `None` for a global snippet
|
||||
///
|
||||
/// This is typically lowercase and matches the filename containing the snippet, without the `.json` extension.
|
||||
pub language: Option<String>,
|
||||
/// Name if using a named snippet
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Snippet body, if not using a named snippet
|
||||
// todo(andrew): use `ListOrDirect` or similar for multiline snippet body
|
||||
pub snippet: Option<String>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
debugger,
|
||||
[
|
||||
|
||||
@@ -79,7 +79,7 @@ use ::git::{
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use blink_manager::BlinkManager;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use client::{Collaborator, ParticipantIndex, parse_zed_link};
|
||||
@@ -14811,6 +14811,52 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_snippet_at_selections(
|
||||
&mut self,
|
||||
action: &InsertSnippet,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.try_insert_snippet_at_selections(action, window, cx)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn try_insert_snippet_at_selections(
|
||||
&mut self,
|
||||
action: &InsertSnippet,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let insertion_ranges = self
|
||||
.selections
|
||||
.all::<MultiBufferOffset>(&self.display_snapshot(cx))
|
||||
.into_iter()
|
||||
.map(|selection| selection.range())
|
||||
.collect_vec();
|
||||
|
||||
let snippet = if let Some(snippet_body) = &action.snippet {
|
||||
if action.language.is_none() && action.name.is_none() {
|
||||
Snippet::parse(snippet_body)?
|
||||
} else {
|
||||
bail!("`snippet` is mutually exclusive with `language` and `name`")
|
||||
}
|
||||
} else if let Some(name) = &action.name {
|
||||
let project = self.project().context("no project")?;
|
||||
let snippet_store = project.read(cx).snippets().read(cx);
|
||||
let snippet = snippet_store
|
||||
.snippets_for(action.language.clone(), cx)
|
||||
.into_iter()
|
||||
.find(|snippet| snippet.name == *name)
|
||||
.context("snippet not found")?;
|
||||
Snippet::parse(&snippet.body)?
|
||||
} else {
|
||||
// todo(andrew): open modal to select snippet
|
||||
bail!("`name` or `snippet` is required")
|
||||
};
|
||||
|
||||
self.insert_snippet(&insertion_ranges, snippet, window, cx)
|
||||
}
|
||||
|
||||
fn select_match_ranges(
|
||||
&mut self,
|
||||
range: Range<MultiBufferOffset>,
|
||||
|
||||
@@ -26895,6 +26895,82 @@ async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_snippet(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.project().unwrap().update(cx, |project, cx| {
|
||||
project.snippets().update(cx, |snippets, _cx| {
|
||||
let snippet = project::snippet_provider::Snippet {
|
||||
prefix: vec![], // no prefix needed!
|
||||
body: "an Unspecified".to_string(),
|
||||
description: Some("shhhh it's a secret".to_string()),
|
||||
name: "super secret snippet".to_string(),
|
||||
};
|
||||
snippets.add_snippet_for_test(
|
||||
None,
|
||||
PathBuf::from("test_snippets.json"),
|
||||
vec![Arc::new(snippet)],
|
||||
);
|
||||
|
||||
let snippet = project::snippet_provider::Snippet {
|
||||
prefix: vec![], // no prefix needed!
|
||||
body: " Location".to_string(),
|
||||
description: Some("the word 'location'".to_string()),
|
||||
name: "location word".to_string(),
|
||||
};
|
||||
snippets.add_snippet_for_test(
|
||||
Some("Markdown".to_string()),
|
||||
PathBuf::from("test_snippets.json"),
|
||||
vec![Arc::new(snippet)],
|
||||
);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.insert_snippet_at_selections(
|
||||
&InsertSnippet {
|
||||
language: None,
|
||||
name: Some("super secret snippet".to_string()),
|
||||
snippet: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Language is specified in the action,
|
||||
// so the buffer language does not need to match
|
||||
editor.insert_snippet_at_selections(
|
||||
&InsertSnippet {
|
||||
language: Some("Markdown".to_string()),
|
||||
name: Some("location word".to_string()),
|
||||
snippet: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.insert_snippet_at_selections(
|
||||
&InsertSnippet {
|
||||
language: None,
|
||||
name: None,
|
||||
snippet: Some("$0 after".to_string()),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(
|
||||
r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_document_colors(cx: &mut TestAppContext) {
|
||||
let expected_color = Rgba {
|
||||
|
||||
@@ -365,6 +365,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::split_selection_into_lines);
|
||||
register_action(editor, window, Editor::add_selection_above);
|
||||
register_action(editor, window, Editor::add_selection_below);
|
||||
register_action(editor, window, Editor::insert_snippet_at_selections);
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
editor.select_next(action, window, cx).log_err();
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn init(cx: &mut App) {
|
||||
extension_snippet::init(cx);
|
||||
}
|
||||
|
||||
// Is `None` if the snippet file is global.
|
||||
/// Language name, or `None` if the snippet file is global.
|
||||
type SnippetKind = Option<String>;
|
||||
fn file_stem_to_key(stem: &str) -> SnippetKind {
|
||||
if stem == "snippets" {
|
||||
|
||||
Reference in New Issue
Block a user