diff --git a/.eslintignore b/.eslintignore index 76c7e79..78b2197 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,5 @@ node_modules dist coverage **/*.d.ts -tests +test _temp_extension \ No newline at end of file diff --git a/Design/add_custom_snippet.png b/Design/add_custom_snippet.png new file mode 100644 index 0000000..32127c2 Binary files /dev/null and b/Design/add_custom_snippet.png differ diff --git a/Design/add_snippet.png b/Design/add_snippet.png new file mode 100644 index 0000000..d314a09 Binary files /dev/null and b/Design/add_snippet.png differ diff --git a/Design/codeSnippetEditor.png b/Design/codeSnippetEditor.png new file mode 100644 index 0000000..17aa3bb Binary files /dev/null and b/Design/codeSnippetEditor.png differ diff --git a/Design/options.png b/Design/options.png new file mode 100644 index 0000000..71f12cf Binary files /dev/null and b/Design/options.png differ diff --git a/Design/search_filter.png b/Design/search_filter.png new file mode 100644 index 0000000..b5549af Binary files /dev/null and b/Design/search_filter.png differ diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..4f806f2 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current' + } + } + ], '@babel/preset-react' + ] +}; diff --git a/coverage/CodeSnippetContentsService.ts.html b/coverage/CodeSnippetContentsService.ts.html new file mode 100644 index 0000000..6fa726c --- /dev/null +++ b/coverage/CodeSnippetContentsService.ts.html @@ -0,0 +1,440 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 | 2x + + + + + + + + + + + + + + +2x + + + + +2x +2x +2x +2x + + + +4x +2x + +4x + + + + + + + + + + +2x +2x + + + + +1x + +1x + + + + + + + + + + + + + + + + + + + + +4x +4x +1x + +3x + + + + + + + + + + + + + + + +2x +2x +1x + +1x + + + + + + + + + + +1x +1x + +1x + + + + + + + + + + + + | import { ContentsManager, Drive, Contents } from '@jupyterlab/services'; + +export interface ICodeSnippet { + name: string; + description: string; + language: string; + // code separated by new line + code: string[]; + id: number; + tags?: string[]; +} + +/** + * Singleton contentsService class + */ +export class CodeSnippetContentsService { + drive: Drive; + contentsManager: ContentsManager; + private static instance: CodeSnippetContentsService; + private constructor() { + const drive = new Drive({ name: 'snippetDrive ' }); + const contentsManager = new ContentsManager({ defaultDrive: drive }); + this.drive = drive; + this.contentsManager = contentsManager; + } + + static getInstance(): CodeSnippetContentsService { + if (!this.instance) { + this.instance = new CodeSnippetContentsService(); + } + return this.instance; + } + + /** + * Get the metadata information in the given path + * @param path path to a file/directory + */ + async getData( + path: string, + type: Contents.ContentType + ): Promise<Contents.IModel> { + try { + const data = await this.contentsManager.get(path, { + type: type, + // format: 'text', + content: true + }); + return data; + } catch (error) { + return error; + } + // const data = await this.contentsManager.get(path, { + // type: type, + // // format: 'text', + // content: true + // }); + // return data; + } + + /** + * Create a file/directory if it does not exist. Otherwise, save the change in a file/directory in the given path + * @param path path to a file/directory + * @param options options that specify if it's a file or directory and additial information + * Usage: save('snippets', { type: 'directory' }) to create/save a directory + * save('snippets/test.json', {type: 'file', format: 'text', content: 'Lorem ipsum dolor sit amet'}) + */ + async save( + path: string, + options?: Partial<Contents.IModel> + ): Promise<Contents.IModel> { + try { + const changedModel = await this.contentsManager.save(path, options); + return changedModel; + } catch (error) { + return error; + } + } + + /** + * Change the order of snippets + * @param oldPath + * @param newPath + */ + + /** + * Rename the file or directory (not case sensitive) + * @param oldPath change from + * @param newPath change to + */ + async rename(oldPath: string, newPath: string): Promise<Contents.IModel> { + try { + const changedModel = await this.contentsManager.rename(oldPath, newPath); + return changedModel; + } catch (error) { + return error; + } + // const changedModel = await this.contentsManager.rename(oldPath, newPath); + // return changedModel; + } + + /** + * Delete the file/directory in the given path + * @param path path to a file/directory + */ + async delete(path: string): Promise<void> { + try { + await this.contentsManager.delete(path); + } catch (error) { + return; + } + } + + // async renameAndSave( + // oldPath: string, + // newPath: string + // ): Promise<Contents.IModel> { + // this.rename(oldPath, newPath); + // this.save(newPath); + // } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 | 1x + + + + + + + + + + + +1x + + + +1x + + + +17x +17x + + + +2x + + + +1x +1x +2x + + + + +1x +2x +1x +1x + + + +1x + + + + + + +3x +1x + +3x + + + +19x + + + + +2x +1x + +2x +1x + +1x +1x +1x + +1x +1x + + + + + + + +3x +3x +1x + + +2x +2x + +2x + + + + +1x + + + +1x +2x + + + + + + + + + + + + +4x + +4x +3x + + +1x +1x + +1x + + + + | import { + ICodeSnippet, + CodeSnippetContentsService +} from './CodeSnippetContentsService'; + +export interface ICodeSnippetWidgetModel { + /** + * The list of code snippets in the code snippet explorer + */ + readonly _snippets: ICodeSnippet[]; +} + +export class CodeSnippetWidgetModel implements ICodeSnippetWidgetModel { + _snippets: ICodeSnippet[]; + + constructor(snippets: ICodeSnippet[]) { + this._snippets = snippets; + } + + get snippets(): ICodeSnippet[] { + this.sortSnippets(); + return this._snippets; + } + + set snippets(snippetList: ICodeSnippet[]) { + this._snippets = snippetList; + } + + reorderSnippet(): void { + this.sortSnippets(); + for (let i = 0; i < this._snippets.length; i++) { + this._snippets[i].id = i; + } + } + + 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) { + newSnippet.id = this._snippets.length; + } + this.insertSnippet(newSnippet, index); + } + + sortSnippets(): void { + this._snippets.sort((a, b) => a.id - b.id); + } + + // move snippetes within explorer + moveSnippet(fromIdx: number, toIdx: number): void { + if (toIdx > fromIdx) { + toIdx = toIdx - 1; + } + if (toIdx === fromIdx) { + return; + } + const snippetToInsert = this._snippets[fromIdx]; + this.deleteSnippet(fromIdx); + snippetToInsert.id = toIdx; + + this.insertSnippet(snippetToInsert, toIdx); + this.updateSnippetContents(); + } + + /** + * Delete a snippet from the list + * @param index index to delete. If it's not given, the last one gets deleted. + */ + deleteSnippet(index = -1): void { + const numSnippets = this._snippets.length; + if (index < 0 || index > numSnippets) { + this._snippets.pop(); + } else { + // Update list + for (let i = index + 1; i < numSnippets; i++) { + this._snippets[i].id = this._snippets[i].id - 1; + } + this._snippets.splice(index, 1); + } + } + + clearSnippets(): void { + this._snippets = []; + } + + updateSnippetContents(): void { + this._snippets.forEach(snippet => { + CodeSnippetContentsService.getInstance().save( + 'snippets/' + snippet.name + '.json', + { type: 'file', format: 'text', content: JSON.stringify(snippet) } + ); + }); + } + + /** + * insert a snippet to the certain index of the snippet list + * @param newSnippet new snippet to insert + * @param index index to insert. If it's not given, the snippet is added at the end of the list. + */ + private insertSnippet(newSnippet: ICodeSnippet, index = -1): void { + const numSnippets = this._snippets.length; + // add it at the end of the list + if (index < 0 || index >= numSnippets) { + this._snippets.push(newSnippet); + } else { + // Update list + for (let i = index; i < numSnippets; i++) { + this._snippets[i].id = this._snippets[i].id + 1; + } + this._snippets.splice(index, 0, newSnippet); + } + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
CodeSnippetContentsService.ts | +
+
+ |
+ 100% | +24/24 | +100% | +2/2 | +100% | +6/6 | +100% | +24/24 | +
CodeSnippetWidgetModel.ts | +
+
+ |
+ 100% | +46/46 | +88.89% | +16/18 | +100% | +14/14 | +100% | +42/42 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 | 2x + + + + + + + + + + + + + + +2x + + + + +2x +2x +2x +2x + + + +4x +2x + +4x + + + + + + + + + + +2x +2x + + + + +1x + +1x + + + + + + + + + + + + + + + + + + + + +4x +4x +1x + +3x + + + + + + + + + + + + + + + +2x +2x +1x + +1x + + + + + + + + + + +1x +1x + +1x + + + + + + + + + + + + | import { ContentsManager, Drive, Contents } from '@jupyterlab/services'; + +export interface ICodeSnippet { + name: string; + description: string; + language: string; + // code separated by new line + code: string[]; + id: number; + tags?: string[]; +} + +/** + * Singleton contentsService class + */ +export class CodeSnippetContentsService { + drive: Drive; + contentsManager: ContentsManager; + private static instance: CodeSnippetContentsService; + private constructor() { + const drive = new Drive({ name: 'snippetDrive ' }); + const contentsManager = new ContentsManager({ defaultDrive: drive }); + this.drive = drive; + this.contentsManager = contentsManager; + } + + static getInstance(): CodeSnippetContentsService { + if (!this.instance) { + this.instance = new CodeSnippetContentsService(); + } + return this.instance; + } + + /** + * Get the metadata information in the given path + * @param path path to a file/directory + */ + async getData( + path: string, + type: Contents.ContentType + ): Promise<Contents.IModel> { + try { + const data = await this.contentsManager.get(path, { + type: type, + // format: 'text', + content: true + }); + return data; + } catch (error) { + return error; + } + // const data = await this.contentsManager.get(path, { + // type: type, + // // format: 'text', + // content: true + // }); + // return data; + } + + /** + * Create a file/directory if it does not exist. Otherwise, save the change in a file/directory in the given path + * @param path path to a file/directory + * @param options options that specify if it's a file or directory and additial information + * Usage: save('snippets', { type: 'directory' }) to create/save a directory + * save('snippets/test.json', {type: 'file', format: 'text', content: 'Lorem ipsum dolor sit amet'}) + */ + async save( + path: string, + options?: Partial<Contents.IModel> + ): Promise<Contents.IModel> { + try { + const changedModel = await this.contentsManager.save(path, options); + return changedModel; + } catch (error) { + return error; + } + } + + /** + * Change the order of snippets + * @param oldPath + * @param newPath + */ + + /** + * Rename the file or directory (not case sensitive) + * @param oldPath change from + * @param newPath change to + */ + async rename(oldPath: string, newPath: string): Promise<Contents.IModel> { + try { + const changedModel = await this.contentsManager.rename(oldPath, newPath); + return changedModel; + } catch (error) { + return error; + } + // const changedModel = await this.contentsManager.rename(oldPath, newPath); + // return changedModel; + } + + /** + * Delete the file/directory in the given path + * @param path path to a file/directory + */ + async delete(path: string): Promise<void> { + try { + await this.contentsManager.delete(path); + } catch (error) { + return; + } + } + + // async renameAndSave( + // oldPath: string, + // newPath: string + // ): Promise<Contents.IModel> { + // this.rename(oldPath, newPath); + // this.save(newPath); + // } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 | 1x + + + + + + + + + + + +1x + + + +1x + + + +17x +17x + + + +2x + + + +1x +1x +2x + + + + +1x +2x +1x +1x + + + +1x + + + + + + +3x +1x + +3x + + + +19x + + + + +2x +1x + +2x +1x + +1x +1x +1x + +1x +1x + + + + + + + +3x +3x +1x + + +2x +2x + +2x + + + + +1x + + + +1x +2x + + + + + + + + + + + + +4x + +4x +3x + + +1x +1x + +1x + + + + | import { + ICodeSnippet, + CodeSnippetContentsService +} from './CodeSnippetContentsService'; + +export interface ICodeSnippetWidgetModel { + /** + * The list of code snippets in the code snippet explorer + */ + readonly _snippets: ICodeSnippet[]; +} + +export class CodeSnippetWidgetModel implements ICodeSnippetWidgetModel { + _snippets: ICodeSnippet[]; + + constructor(snippets: ICodeSnippet[]) { + this._snippets = snippets; + } + + get snippets(): ICodeSnippet[] { + this.sortSnippets(); + return this._snippets; + } + + set snippets(snippetList: ICodeSnippet[]) { + this._snippets = snippetList; + } + + reorderSnippet(): void { + this.sortSnippets(); + for (let i = 0; i < this._snippets.length; i++) { + this._snippets[i].id = i; + } + } + + 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) { + newSnippet.id = this._snippets.length; + } + this.insertSnippet(newSnippet, index); + } + + sortSnippets(): void { + this._snippets.sort((a, b) => a.id - b.id); + } + + // move snippetes within explorer + moveSnippet(fromIdx: number, toIdx: number): void { + if (toIdx > fromIdx) { + toIdx = toIdx - 1; + } + if (toIdx === fromIdx) { + return; + } + const snippetToInsert = this._snippets[fromIdx]; + this.deleteSnippet(fromIdx); + snippetToInsert.id = toIdx; + + this.insertSnippet(snippetToInsert, toIdx); + this.updateSnippetContents(); + } + + /** + * Delete a snippet from the list + * @param index index to delete. If it's not given, the last one gets deleted. + */ + deleteSnippet(index = -1): void { + const numSnippets = this._snippets.length; + if (index < 0 || index > numSnippets) { + this._snippets.pop(); + } else { + // Update list + for (let i = index + 1; i < numSnippets; i++) { + this._snippets[i].id = this._snippets[i].id - 1; + } + this._snippets.splice(index, 1); + } + } + + clearSnippets(): void { + this._snippets = []; + } + + updateSnippetContents(): void { + this._snippets.forEach(snippet => { + CodeSnippetContentsService.getInstance().save( + 'snippets/' + snippet.name + '.json', + { type: 'file', format: 'text', content: JSON.stringify(snippet) } + ); + }); + } + + /** + * insert a snippet to the certain index of the snippet list + * @param newSnippet new snippet to insert + * @param index index to insert. If it's not given, the snippet is added at the end of the list. + */ + private insertSnippet(newSnippet: ICodeSnippet, index = -1): void { + const numSnippets = this._snippets.length; + // add it at the end of the list + if (index < 0 || index >= numSnippets) { + this._snippets.push(newSnippet); + } else { + // Update list + for (let i = index; i < numSnippets; i++) { + this._snippets[i].id = this._snippets[i].id + 1; + } + this._snippets.splice(index, 0, newSnippet); + } + } +} + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
CodeSnippetContentsService.ts | +
+
+ |
+ 100% | +24/24 | +100% | +2/2 | +100% | +6/6 | +100% | +24/24 | +
CodeSnippetWidgetModel.ts | +
+
+ |
+ 100% | +46/46 | +88.89% | +16/18 | +100% | +14/14 | +100% | +42/42 | +