Skip to content

Commit fde0ec9

Browse files
Alexey Perovcursoragent
andcommitted
Add Markdown snippet model, insertion, picker, and settings.
Ship reusable Markdown templates with tab stops, persisted user/builtin preferences, and discoverable insert UX without migration shims. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 271bdc0 commit fde0ec9

49 files changed

Lines changed: 2368 additions & 40 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/lib/commands/definitions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,16 @@ export const commandDefinitions: CommandDefinition[] = [
481481
availability: "document",
482482
binding: { mac: "Ctrl+Space", windows: "Ctrl+Space" },
483483
},
484+
{
485+
id: "edit.insertSnippet",
486+
label: "Insert Snippet",
487+
menuPath: "Edit/Insert Snippet",
488+
category: "Edit",
489+
searchTerms: ["snippet", "template", "markdown", "placeholder"],
490+
paletteIntent: "palette",
491+
availability: "markdownEdit",
492+
binding: { mac: "none", windows: "none" },
493+
},
484494
{
485495
id: "edit.toggleBookmark",
486496
label: "Toggle Bookmark",

app/src/lib/commands/handlers/appViewHandlers.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ function createEditorRunnerMock(): EditorCommandRunner {
188188
unfoldAll: vi.fn(() => false),
189189
jumpToHeading: vi.fn(() => false),
190190
completeWord: vi.fn(() => false),
191+
insertSnippet: vi.fn(() => false),
191192
toggleBookmark: vi.fn(() => false),
192193
nextBookmark: vi.fn(() => false),
193194
previousBookmark: vi.fn(() => false),
@@ -212,6 +213,7 @@ function createCommandContext(overrides?: {
212213
openQuickOpen?: () => void;
213214
openHeadingJump?: () => void;
214215
openBookmarkList?: () => void;
216+
openSnippetInsert?: () => void;
215217
openCommandPalette?: () => void;
216218
setConsoleOpen?: (open: boolean) => void;
217219
}) {
@@ -221,6 +223,7 @@ function createCommandContext(overrides?: {
221223
const openQuickOpen = overrides?.openQuickOpen ?? vi.fn();
222224
const openHeadingJump = overrides?.openHeadingJump ?? vi.fn();
223225
const openBookmarkList = overrides?.openBookmarkList ?? vi.fn();
226+
const openSnippetInsert = overrides?.openSnippetInsert ?? vi.fn();
224227
const openCommandPalette = overrides?.openCommandPalette ?? vi.fn();
225228
const setConsoleOpen = overrides?.setConsoleOpen ?? vi.fn();
226229
const editorTools = createEditorToolController({
@@ -240,6 +243,7 @@ function createCommandContext(overrides?: {
240243
openQuickOpen,
241244
openHeadingJump,
242245
openBookmarkList,
246+
openSnippetInsert,
243247
openCommandPalette,
244248
setConsoleOpen,
245249
},
@@ -250,6 +254,7 @@ function createCommandContext(overrides?: {
250254
openQuickOpen,
251255
openHeadingJump,
252256
openBookmarkList,
257+
openSnippetInsert,
253258
openCommandPalette,
254259
setConsoleOpen,
255260
};
@@ -697,6 +702,12 @@ describe("edit commands", () => {
697702
expect(editorRunner.completeWord).toHaveBeenCalled();
698703
});
699704

705+
it("edit.insertSnippet opens the snippet picker", () => {
706+
const { context, openSnippetInsert } = createCommandContext();
707+
dispatchCommand("edit.insertSnippet", context);
708+
expect(openSnippetInsert).toHaveBeenCalledTimes(1);
709+
});
710+
700711
it("no-ops when no editor runner is available", () => {
701712
const { context } = createCommandContext({ editorRunner: null });
702713

@@ -897,6 +908,7 @@ describe("command dispatch coverage", () => {
897908
"edit.foldAll",
898909
"edit.unfoldAll",
899910
"edit.triggerCompletion",
911+
"edit.insertSnippet",
900912
"edit.toggleBookmark",
901913
"edit.nextBookmark",
902914
"edit.previousBookmark",

app/src/lib/commands/handlers/edit.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export const editHandlers: CommandHandlerMap = {
5555
"edit.triggerCompletion": ({ getEditorRunner }) => {
5656
getEditorRunner()?.completeWord();
5757
},
58+
"edit.insertSnippet": ({ openSnippetInsert }) => {
59+
openSnippetInsert?.();
60+
},
5861
"edit.toggleBookmark": ({ getEditorRunner, notify }) => {
5962
const toggled = getEditorRunner()?.toggleBookmark() ?? false;
6063
if (toggled) {

app/src/lib/commands/handlers/fileHandlers.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ function createEditorRunnerMock(): EditorCommandRunner {
189189
unfoldAll: vi.fn(() => false),
190190
jumpToHeading: vi.fn(() => false),
191191
completeWord: vi.fn(() => false),
192+
insertSnippet: vi.fn(() => false),
192193
toggleBookmark: vi.fn(() => false),
193194
nextBookmark: vi.fn(() => false),
194195
previousBookmark: vi.fn(() => false),

app/src/lib/commands/handlers/keymapHandlers.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ function createEditorRunnerMock(): EditorCommandRunner {
188188
unfoldAll: vi.fn(() => false),
189189
jumpToHeading: vi.fn(() => false),
190190
completeWord: vi.fn(() => false),
191+
insertSnippet: vi.fn(() => false),
191192
toggleBookmark: vi.fn(() => false),
192193
nextBookmark: vi.fn(() => false),
193194
previousBookmark: vi.fn(() => false),

app/src/lib/commands/handlers/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export type CommandContext = {
1919
openHeadingJump?: () => void;
2020
/** Opens (or focuses) the bookmark list picker. No-op when not registered. */
2121
openBookmarkList?: () => void;
22+
/** Opens (or focuses) the insert-snippet picker. No-op when not registered. */
23+
openSnippetInsert?: () => void;
2224
openCommandPalette?: () => void;
2325
/** Sets console panel visibility. No-op when not registered. */
2426
setConsoleOpen?: (open: boolean) => void;

app/src/lib/commands/handlers/workspaceHandlers.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ function createEditorRunnerMock(): EditorCommandRunner {
187187
unfoldAll: vi.fn(() => false),
188188
jumpToHeading: vi.fn(() => false),
189189
completeWord: vi.fn(() => false),
190+
insertSnippet: vi.fn(() => false),
190191
toggleBookmark: vi.fn(() => false),
191192
nextBookmark: vi.fn(() => false),
192193
previousBookmark: vi.fn(() => false),

app/src/lib/components/AppShell.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import CommandPalettePicker from "./CommandPalettePicker.svelte";
3131
import HeadingJumpPicker from "./HeadingJumpPicker.svelte";
3232
import BookmarkListPicker from "./BookmarkListPicker.svelte";
33+
import SnippetInsertPicker from "./SnippetInsertPicker.svelte";
3334
import type { ProjectTreeControllerState } from "../services/projectTreeController";
3435
import type { ProjectTreeNode } from "../services/projectTree";
3536
import TitleBar from "./TitleBar.svelte";
@@ -362,6 +363,14 @@
362363
onQueryInput?: (query: string) => void;
363364
}
364365
366+
export interface AppShellSnippetInsertProps {
367+
open: boolean;
368+
results: import("../picker/snippetRanking").RankedSnippetsResult;
369+
onSelect: (snippetId: string) => void;
370+
onClose: () => void;
371+
onQueryInput?: (query: string) => void;
372+
}
373+
365374
let {
366375
activityRail,
367376
sessionsSidebar,
@@ -380,6 +389,7 @@
380389
commandPalette,
381390
headingJump,
382391
bookmarkList,
392+
snippetInsert,
383393
onConsoleHeightCommit,
384394
consoleOpen = false,
385395
consoleHeightPx = $bindable(0),
@@ -405,6 +415,7 @@
405415
commandPalette?: AppShellCommandPaletteProps;
406416
headingJump?: AppShellHeadingJumpProps;
407417
bookmarkList?: AppShellBookmarkListProps;
418+
snippetInsert?: AppShellSnippetInsertProps;
408419
onConsoleHeightCommit: () => void;
409420
consoleOpen?: boolean;
410421
consoleHeightPx?: number;
@@ -758,6 +769,16 @@
758769
/>
759770
{/if}
760771

772+
{#if snippetInsert}
773+
<SnippetInsertPicker
774+
open={snippetInsert.open}
775+
results={snippetInsert.results}
776+
onSelect={snippetInsert.onSelect}
777+
onClose={snippetInsert.onClose}
778+
onQueryInput={snippetInsert.onQueryInput}
779+
/>
780+
{/if}
781+
761782
{#if timelineDialog}
762783
<SessionTimelineDialog
763784
open={timelineDialog.open}

app/src/lib/components/DocumentEditor.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import EditorSurface from "./EditorSurface.svelte";
33
import { appState } from "../state/appState";
44
import type { EditorLanguageId } from "../editor/editorLanguage";
5+
import { listEnabledMarkdownSnippets } from "../editor/markdownSnippetSettings";
56
67
let {
78
content = "",
@@ -39,6 +40,13 @@
3940
onScrollTopChange?: (documentId: string, scrollTop: number) => void;
4041
} = $props();
4142
43+
const snapshot = $derived($appState);
44+
const enabledSnippets = $derived(
45+
language === "markdown"
46+
? listEnabledMarkdownSnippets(snapshot.settings.markdownSnippets)
47+
: [],
48+
);
49+
4250
function handleDocumentDirty(nextContent: string): void {
4351
if (!documentId) {
4452
return;
@@ -61,6 +69,7 @@
6169
{showFoldGutter}
6270
{autoClosePairs}
6371
{autoSuggest}
72+
{enabledSnippets}
6473
{onStatusMessage}
6574
onDocumentDirty={handleDocumentDirty}
6675
{onScrollTopChange}

app/src/lib/components/EditorSurface.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
showFoldGutter?: boolean;
2323
autoClosePairs?: boolean;
2424
autoSuggest?: boolean;
25+
enabledSnippets?: import("../domain/snippets").ResolvedMarkdownSnippet[];
2526
onStatusMessage?: (message: string) => void;
2627
onDocumentDirty?: (nextContent: string) => void;
2728
onScrollTopChange?: (documentId: string, scrollTop: number) => void;
@@ -40,6 +41,7 @@
4041
showFoldGutter = true,
4142
autoClosePairs = true,
4243
autoSuggest = false,
44+
enabledSnippets = [],
4345
onStatusMessage = () => {},
4446
onDocumentDirty = () => {},
4547
onScrollTopChange = () => {},
@@ -76,6 +78,7 @@
7678
showFoldGutter,
7779
autoClosePairs,
7880
autoSuggest,
81+
enabledSnippets,
7982
});
8083
controller.mount(hostEl);
8184
@@ -115,6 +118,7 @@
115118
showFoldGutter,
116119
autoClosePairs,
117120
autoSuggest,
121+
enabledSnippets,
118122
});
119123
});
120124
</script>

0 commit comments

Comments
 (0)