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:
Andrew Farkas
2025-12-08 16:38:24 -05:00
committed by GitHub
parent 22e1bcccad
commit 6b2d1f153d
5 changed files with 142 additions and 2 deletions

View File

@@ -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,
[

View File

@@ -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>,

View File

@@ -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 {

View File

@@ -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();
});

View File

@@ -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" {