Skip to content

Instantly share code, notes, and snippets.

@krassowski
Created April 10, 2026 17:05
Show Gist options
  • Select an option

  • Save krassowski/be569444ca3199552bdb696023b42158 to your computer and use it in GitHub Desktop.

Select an option

Save krassowski/be569444ca3199552bdb696023b42158 to your computer and use it in GitHub Desktop.
Code snippets demo created in plugin-playground
import {
JupyterFrontEnd,
JupyterFrontEndPlugin,
ILabShell
} from '@jupyterlab/application';
import { INotebookTracker } from '@jupyterlab/notebook';
import { ICommandPalette, ReactWidget } from '@jupyterlab/apputils';
import { codeIcon } from '@jupyterlab/ui-components';
import { Widget } from '@lumino/widgets';
import React, { useState, useEffect, useCallback } from 'react';
interface Snippet {
id: string;
name: string;
language: string;
code: string;
createdAt: number;
}
const STORAGE_KEY = 'jupyterlab-code-snippets';
function loadSnippets(): Snippet[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
}
function saveSnippets(snippets: Snippet[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(snippets));
}
const STYLE_ID = 'code-snippets-styles';
const STYLE = `
.cs-root {
display: flex; flex-direction: column; height: 100%;
font-family: var(--jp-ui-font-family);
font-size: var(--jp-ui-font-size1);
color: var(--jp-ui-font-color0);
padding: 8px;
overflow-y: auto;
}
.cs-root h2 {
margin: 0 0 8px; font-size: var(--jp-ui-font-size2);
}
.cs-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.cs-form input, .cs-form select, .cs-form textarea {
padding: 6px 8px;
border: 1px solid var(--jp-border-color1);
border-radius: 4px;
background: var(--jp-layout-color1);
color: var(--jp-ui-font-color0);
font-family: var(--jp-ui-font-family);
font-size: var(--jp-ui-font-size1);
}
.cs-form textarea {
font-family: var(--jp-code-font-family);
font-size: var(--jp-code-font-size);
min-height: 80px; resize: vertical;
}
.cs-form input:focus, .cs-form select:focus, .cs-form textarea:focus {
outline: none; border-color: var(--jp-brand-color1);
}
.cs-btn {
padding: 6px 12px; border: none; border-radius: 4px;
cursor: pointer; font-size: var(--jp-ui-font-size1);
font-weight: 500; transition: opacity .15s;
}
.cs-btn:hover { opacity: .85; }
.cs-btn-primary {
background: var(--jp-brand-color1); color: #fff;
}
.cs-btn-danger {
background: var(--jp-error-color1); color: #fff;
}
.cs-btn-secondary {
background: var(--jp-layout-color3); color: var(--jp-ui-font-color0);
}
.cs-btn-sm { padding: 3px 8px; font-size: var(--jp-ui-font-size0); }
.cs-btn-row { display: flex; gap: 6px; }
.cs-search { margin-bottom: 10px; }
.cs-search input {
width: 100%; box-sizing: border-box;
padding: 6px 8px;
border: 1px solid var(--jp-border-color1);
border-radius: 4px;
background: var(--jp-layout-color1);
color: var(--jp-ui-font-color0);
font-size: var(--jp-ui-font-size1);
}
.cs-list { display: flex; flex-direction: column; gap: 8px; }
.cs-card {
border: 1px solid var(--jp-border-color1);
border-radius: 6px; padding: 10px;
background: var(--jp-layout-color1);
transition: box-shadow .15s;
}
.cs-card:hover {
box-shadow: 0 1px 4px rgba(0,0,0,.12);
}
.cs-card-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 6px;
}
.cs-card-name { font-weight: 600; }
.cs-card-lang {
font-size: var(--jp-ui-font-size0);
background: var(--jp-brand-color3);
color: var(--jp-brand-color1);
padding: 1px 6px; border-radius: 3px;
}
.cs-card pre {
margin: 6px 0;
padding: 8px;
background: var(--jp-layout-color2);
border-radius: 4px;
font-family: var(--jp-code-font-family);
font-size: var(--jp-code-font-size);
white-space: pre-wrap;
word-break: break-all;
max-height: 150px; overflow-y: auto;
}
.cs-empty {
text-align: center; color: var(--jp-ui-font-color2);
padding: 24px 0;
}
.cs-section-title {
font-size: var(--jp-ui-font-size1); font-weight: 600;
margin: 0 0 6px; cursor: pointer; user-select: none;
}
`;
function ensureStyles(): void {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = STYLE;
document.head.appendChild(style);
}
interface SnippetPanelProps {
insertSnippet: (code: string) => void;
}
function SnippetPanel({ insertSnippet }: SnippetPanelProps) {
const [snippets, setSnippets] = useState<Snippet[]>(loadSnippets);
const [search, setSearch] = useState('');
const [showForm, setShowForm] = useState(false);
const [name, setName] = useState('');
const [language, setLanguage] = useState('python');
const [code, setCode] = useState('');
useEffect(() => { saveSnippets(snippets); }, [snippets]);
const addSnippet = useCallback(() => {
if (!name.trim() || !code.trim()) return;
const snippet: Snippet = {
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
name: name.trim(),
language,
code,
createdAt: Date.now()
};
setSnippets(prev => [snippet, ...prev]);
setName(''); setCode(''); setShowForm(false);
}, [name, language, code]);
const removeSnippet = useCallback((id: string) => {
setSnippets(prev => prev.filter(s => s.id !== id));
}, []);
const filtered = snippets.filter(s => {
if (!search.trim()) return true;
const q = search.toLowerCase();
return (
s.name.toLowerCase().includes(q) ||
s.language.toLowerCase().includes(q) ||
s.code.toLowerCase().includes(q)
);
});
return (
<div className="cs-root">
<h2>✂️ Code Snippets</h2>
<div style={{ marginBottom: 10 }}>
<button
className="cs-btn cs-btn-primary"
style={{ width: '100%' }}
onClick={() => setShowForm(f => !f)}
>
{showForm ? '✕ Cancel' : '+ New Snippet'}
</button>
</div>
{showForm && (
<div className="cs-form">
<input
placeholder="Snippet name"
value={name}
onChange={e => setName(e.target.value)}
/>
<select value={language} onChange={e => setLanguage(e.target.value)}>
{['python', 'javascript', 'typescript', 'r', 'julia', 'sql', 'bash', 'markdown', 'other'].map(l => (
<option key={l} value={l}>{l}</option>
))}
</select>
<textarea
placeholder="Paste your code here…"
value={code}
onChange={e => setCode(e.target.value)}
/>
<button className="cs-btn cs-btn-primary" onClick={addSnippet}>
💾 Save Snippet
</button>
</div>
)}
<div className="cs-search">
<input
placeholder="🔍 Search snippets…"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
{filtered.length === 0 ? (
<div className="cs-empty">
{snippets.length === 0
? 'No snippets yet. Create one above!'
: 'No snippets match your search.'}
</div>
) : (
<div className="cs-list">
{filtered.map(s => (
<div key={s.id} className="cs-card">
<div className="cs-card-header">
<span className="cs-card-name">{s.name}</span>
<span className="cs-card-lang">{s.language}</span>
</div>
<pre>{s.code}</pre>
<div className="cs-btn-row">
<button
className="cs-btn cs-btn-primary cs-btn-sm"
onClick={() => insertSnippet(s.code)}
title="Insert at cursor (also via Ctrl+Shift+V)"
>
⎘ Insert
</button>
<button
className="cs-btn cs-btn-secondary cs-btn-sm"
onClick={() => navigator.clipboard.writeText(s.code)}
title="Copy to clipboard"
>
📋 Copy
</button>
<button
className="cs-btn cs-btn-danger cs-btn-sm"
onClick={() => removeSnippet(s.id)}
title="Delete snippet"
>
🗑
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}
class SnippetSidebarWidget extends ReactWidget {
private _insertSnippet: (code: string) => void;
constructor(insertFn: (code: string) => void) {
super();
this._insertSnippet = insertFn;
this.id = 'code-snippets-sidebar';
this.title.label = 'Snippets';
this.title.icon = codeIcon;
this.title.caption = 'Code Snippets';
this.addClass('jp-code-snippets');
}
render(): React.JSX.Element {
return <SnippetPanel insertSnippet={this._insertSnippet} />;
}
}
namespace CommandIDs {
export const toggle = 'code-snippets:toggle-panel';
export const insertLast = 'code-snippets:insert-last';
export const saveSelection = 'code-snippets:save-selection';
}
const plugin: JupyterFrontEndPlugin<void> = {
id: 'code-snippets',
description: 'A sidebar for saving, searching, and inserting code snippets.',
autoStart: true,
requires: [INotebookTracker],
optional: [ICommandPalette, ILabShell],
activate: (
app: JupyterFrontEnd,
notebooks: INotebookTracker,
palette: ICommandPalette | null,
shell: ILabShell | null
) => {
ensureStyles();
const { commands } = app;
const insertAtCursor = (code: string) => {
commands.execute('notebook:replace-selection', { text: code });
};
const sidebar = new SnippetSidebarWidget(insertAtCursor);
app.shell.add(sidebar, 'left', { rank: 900 });
commands.addCommand(CommandIDs.toggle, {
label: 'Toggle Code Snippets Panel',
icon: codeIcon,
execute: () => {
if (sidebar.isVisible) {
sidebar.close();
} else {
app.shell.add(sidebar, 'left', { rank: 900 });
app.shell.activateById(sidebar.id);
}
}
});
commands.addCommand(CommandIDs.insertLast, {
label: 'Insert Last Saved Snippet',
execute: () => {
const all = loadSnippets();
if (all.length === 0) return;
insertAtCursor(all[0].code);
}
});
commands.addCommand(CommandIDs.saveSelection, {
label: 'Save Selection as Snippet',
execute: () => {
const cell = notebooks.activeCell;
if (!cell) return;
const editor = cell.editor;
if (!editor) return;
const selection = editor.getSelection();
const start = editor.getOffsetAt(selection.start);
const end = editor.getOffsetAt(selection.end);
const text = cell.model.sharedModel.source.slice(start, end);
if (!text.trim()) return;
const snippet: Snippet = {
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
name: 'Selection ' + new Date().toLocaleTimeString(),
language: notebooks.currentWidget?.sessionContext?.kernelDisplayName?.toLowerCase() ?? 'python',
code: text,
createdAt: Date.now()
};
const all = loadSnippets();
all.unshift(snippet);
saveSnippets(all);
sidebar.update();
app.shell.activateById(sidebar.id);
}
});
app.commands.addKeyBinding({
command: CommandIDs.insertLast,
keys: ['Accel Shift V'],
selector: '.jp-Notebook'
});
app.commands.addKeyBinding({
command: CommandIDs.saveSelection,
keys: ['Accel Shift S'],
selector: '.jp-Notebook'
});
app.commands.addKeyBinding({
command: CommandIDs.toggle,
keys: ['Accel Shift L'],
selector: 'body'
});
if (palette) {
const category = 'Code Snippets';
palette.addItem({ command: CommandIDs.toggle, category });
palette.addItem({ command: CommandIDs.insertLast, category });
palette.addItem({ command: CommandIDs.saveSelection, category });
}
}
};
export default plugin;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment