Created
April 10, 2026 17:05
-
-
Save krassowski/be569444ca3199552bdb696023b42158 to your computer and use it in GitHub Desktop.
Code snippets demo created in plugin-playground
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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