diff --git a/package.json b/package.json index bf1a707..906a1dd 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "author": "Jay Ahn, Kiran Pinnipati", "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -43,7 +44,6 @@ "@jupyterlab/application": "^2.1.2", "@jupyterlab/apputils": "^2.2.4", "@jupyterlab/cells": "^2.2.4", - "@jupyterlab/coreutils": "^4.1.0", "@jupyterlab/docmanager": "^2.1.2", "@jupyterlab/docregistry": "^2.1.2", "@jupyterlab/fileeditor": "^2.1.2", @@ -77,7 +77,8 @@ "style/*.css" ], "jupyterlab": { - "extension": true + "extension": true, + "schemaDir": "schema" }, "husky": { "hooks": { diff --git a/schema/settings.json b/schema/settings.json new file mode 100644 index 0000000..88710a4 --- /dev/null +++ b/schema/settings.json @@ -0,0 +1,15 @@ +{ + "jupyter.lab.shortcuts": [ + { + "command": "codeSnippet:save-as-snippet", + "keys": ["Accel Shift A"], + "selector": ".jp-Cell" + }, + { + "command": "codeSnippet:save-as-snippet", + "keys": ["Accel Shift A"], + "selector": ".jp-FileEditor" + } + ], + "type": "object" +} diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index 006ae82..ded4d5f 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -168,6 +168,7 @@ export class CodeSnippetDisplay extends React.Component< this._dragData = null; this.handleDragMove = this.handleDragMove.bind(this); this._evtMouseUp = this._evtMouseUp.bind(this); + this.handleRenameSnippet = this.handleRenameSnippet.bind(this); } // Handle code snippet insert into an editor @@ -321,9 +322,85 @@ export class CodeSnippetDisplay extends React.Component< ); } } - return {name}; + return {name}; }; + // rename snippet on double click + // TODO: duplicate name check! + private async handleRenameSnippet( + event: React.MouseEvent + ): Promise { + const contentsService = CodeSnippetContentsService.getInstance(); + console.log(event.currentTarget); + console.log(event.target); + const target = event.target as HTMLElement; + const oldPath = 'snippets/' + target.innerHTML + '.json'; + + const new_element = document.createElement('input'); + new_element.setAttribute('type', 'text'); + new_element.id = 'jp-codeSnippet-rename'; + new_element.innerHTML = target.innerHTML; + + target.replaceWith(new_element); + new_element.value = target.innerHTML; + + new_element.focus(); + new_element.setSelectionRange(0, new_element.value.length); + + new_element.onblur = async (): Promise => { + console.log(target.innerHTML); + console.log(new_element.value); + if (target.innerHTML !== new_element.value) { + const newPath = 'snippets/' + new_element.value + '.json'; + try { + await contentsService.rename(oldPath, newPath); + } catch (error) { + new_element.replaceWith(target); + + await showDialog({ + title: 'Duplicate Name of Code Snippet', + body:

{`"${newPath}" already exists.`}

, + buttons: [Dialog.okButton({ label: 'Dismiss' })] + }); + return; + } + this.props._codeSnippetWidgetModel.renameSnippet( + target.innerHTML, + new_element.value + ); + target.innerHTML = new_element.value; + } + new_element.replaceWith(target); + }; + new_element.onkeydown = (event: KeyboardEvent): void => { + switch (event.code) { + case 'Enter' || 'NumpadEnter': // Enter + event.stopPropagation(); + event.preventDefault(); + new_element.blur(); + break; + case 'Escape': // Escape + event.stopPropagation(); + event.preventDefault(); + new_element.blur(); + break; + case 'ArrowUp': // Up arrow + event.stopPropagation(); + event.preventDefault(); + new_element.selectionStart = new_element.selectionEnd = 0; + break; + case 'ArrowDown': // Down arrow + event.stopPropagation(); + event.preventDefault(); + new_element.selectionStart = new_element.selectionEnd = + new_element.value.length; + break; + default: + break; + } + }; + } + private handleDragSnippet( event: React.MouseEvent ): void { diff --git a/src/CodeSnippetWidgetModel.ts b/src/CodeSnippetWidgetModel.ts index d4dc7e7..8998c1e 100644 --- a/src/CodeSnippetWidgetModel.ts +++ b/src/CodeSnippetWidgetModel.ts @@ -33,6 +33,19 @@ export class CodeSnippetWidgetModel implements ICodeSnippetWidgetModel { } } + renameSnippet(oldName: string, newName: string): void { + for (const snippet of this._snippets) { + if (snippet.name === oldName) { + snippet.name = newName; + CodeSnippetContentsService.getInstance().save( + 'snippets/' + snippet.name + '.json', + { type: 'file', format: 'text', content: JSON.stringify(snippet) } + ); + break; + } + } + } + addSnippet(newSnippet: ICodeSnippet, index: number): void { // append a new snippet created from input form to the end if (newSnippet.id === -1) { diff --git a/src/index.ts b/src/index.ts index 67a226d..8dd8f1d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,8 @@ import { ILayoutRestorer } from '@jupyterlab/application'; import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; + import { IEditorServices } from '@jupyterlab/codeeditor'; import { LabIcon } from '@jupyterlab/ui-components'; @@ -23,6 +25,7 @@ import { const CODE_SNIPPET_EXTENSION_ID = 'code-snippet-extension'; +const CODE_SNIPPET_SETTING_ID = 'jupyterlab-code-snippets:settings'; /** * Snippet Editor Icon */ @@ -150,7 +153,7 @@ function activateCodeSnippet( }); //Add an application command - const saveCommand = 'save as code snippet'; + const saveCommand = 'codeSnippet:save-as-snippet'; const toggled = false; app.commands.addCommand(saveCommand, { label: 'Save As Code Snippet', @@ -169,18 +172,16 @@ function activateCodeSnippet( } }); - //Put the command above in context menu + // Put the saveCommand above in context menu app.contextMenu.addItem({ command: saveCommand, selector: '.jp-Cell' }); - // Add keybinding to save - app.commands.addKeyBinding({ + // Put the saveCommand in non-notebook file context menu + app.contextMenu.addItem({ command: saveCommand, - args: {}, - keys: ['Accel Shift S'], - selector: '.jp-Cell' + selector: '.jp-FileEditor' }); // Track and restore the widget state @@ -211,6 +212,18 @@ function activateCodeSnippet( }); } +const codeSnippetExtensionSetting: JupyterFrontEndPlugin = { + id: CODE_SNIPPET_SETTING_ID, + autoStart: true, + requires: [ISettingRegistry], + activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => { + void settingRegistry + .load(CODE_SNIPPET_SETTING_ID) + .then(_ => console.log('settingRegistry successfully loaded!')) + .catch(e => console.log(e)); + } +}; + function getSelectedText(): string { let selectedText; // window.getSelection @@ -224,4 +237,4 @@ function getSelectedText(): string { return selectedText.toString(); } -export default code_snippet_extension; +export default [code_snippet_extension, codeSnippetExtensionSetting]; diff --git a/style/index.css b/style/index.css index 60bc2c4..e9a8857 100644 --- a/style/index.css +++ b/style/index.css @@ -42,6 +42,7 @@ text-overflow: ellipsis; color: var(--jp-ui-font-color0); display: flex; + align-items: center; } .jp-codeSnippetsContainer-button { @@ -69,6 +70,15 @@ background-color: var(--jp-layout-color2); } +#jp-codeSnippet-rename { + background-color: var(--jp-layout-color2); + border: 1px solid var(--jp-layout-color1); + border-radius: 4px; + font-size: var(--jp-ui-font-size1); + box-sizing: border-box; + margin: 0px; +} + .jp-codeSnippet-metadata { flex-basis: 95%; width: 100%;