├── .gitignore ├── .eslintignore ├── img.png ├── .gitmodules ├── .vscodeignore ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── src ├── extension │ ├── tsconfig.json │ └── extension.ts └── renderer │ ├── tsconfig.json │ └── main.js ├── .eslintrc.json ├── samples └── test.regexnb ├── README.md ├── LICENSE.txt ├── package.json └── esbuild.js /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/out/** 3 | **/node_modules/** 4 | **/*.d.ts 5 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrieken/vscode-regex-notebook/HEAD/img.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "regexper-static"] 2 | path = src/renderer/regexper-static 3 | url = https://gitlab.com/javallone/regexper-static.git 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "../../out", 6 | "lib": [ 7 | "DOM", 8 | "es6" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": ".", 12 | "strict": true 13 | }, 14 | "exclude": [ 15 | "../../node_modules", 16 | ".vscode-test" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile", 9 | "group": "build", 10 | "problemMatcher": [], 11 | "label": "npm: compile", 12 | "detail": "node esbuild.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "out", 4 | "rootDir": ".", 5 | "allowJs": true, 6 | "module": "es6", 7 | "sourceMap": true, 8 | "lib": [ 9 | "ES2019", 10 | "DOM" 11 | ], 12 | "types": [ 13 | "vscode-notebook-renderer" 14 | ] 15 | }, 16 | "include": [ 17 | "*.js", 18 | "*.d.ts" 19 | ], 20 | "exclude": [ 21 | "webpack.config.js", 22 | "../../node_modules", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/test.regexnb: -------------------------------------------------------------------------------- 1 | MD: "### UUIDv4" 2 | RE: "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i" 3 | MD: "### Phone number\n\nSee [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions]()" 4 | RE: "/(?:\\d{3}|\\(\\d{3}\\))([-\\/\\.])\\d{3}\\1\\d{4}/" 5 | MD: "### JavaScript Comment Removal\n\nRemoves any and all comments from JavaScript, see: [https://regexr.com/3aeb7]()" 6 | RE: "/\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*/g" 7 | MD: "### E-Mail Validation\n\nSee [https://stackoverflow.com/a/46181]()" 8 | RE: "/^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Native notebook for VS Code that renders regular expressions using https://regexper.com. 2 | 3 | 4 | ![Sample showing rendered regular expression](https://github.com/jrieken/vscode-regex-notebook/raw/master/img.png) 5 | 6 | This samples makes use of the following APIs/concepts 7 | 8 | * `NotebookSerializer` to load and save notebook documents from disk 9 | * `NotebookController` to execute regular expressions cells 10 | * `NotebookRenderer` to present regular expressions in a nice way 11 | 12 | ### Running out of source 13 | 14 | * run `git submodule init` and `git submodule update` 15 | * run `npm install` in the terminal 16 | * also run `npm run compile` 17 | * select `F5` to debug 18 | 19 | ### Thanks 20 | 21 | The rendering uses the https://regexper.com source code which works like a charme 👏 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "pwa-extensionHost", 11 | "debugWebviews": true, 12 | "request": "launch", 13 | "runtimeExecutable": "${execPath}", 14 | "args": [ 15 | "--extensionDevelopmentPath=${workspaceFolder}", 16 | "${workspaceFolder}/samples", 17 | "${workspaceFolder}/samples/test.regexnb" 18 | ], 19 | "outFiles": [ 20 | "${workspaceFolder}/out/**/*.js" 21 | ], 22 | "preLaunchTask": "npm: compile" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regex-notebook", 3 | "displayName": "Regexper notebooks", 4 | "description": "A notebook that uses Regexper to render regular expressions.", 5 | "repository": { 6 | "url": "https://github.com/jrieken/vscode-regex-notebook" 7 | }, 8 | "publisher": "jrieken", 9 | "version": "0.0.6", 10 | "engines": { 11 | "vscode": "^1.61.0" 12 | }, 13 | "categories": [ 14 | "Notebooks", 15 | "Visualization", 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "onNotebook:regexpnb" 20 | ], 21 | "main": "./dist/extension.js", 22 | "browser": "./dist/extension.js", 23 | "capabilities": { 24 | "untrustedWorkspaces": { 25 | "supported": true 26 | }, 27 | "virtualWorkspaces": true 28 | }, 29 | "contributes": { 30 | "notebooks": [ 31 | { 32 | "type": "regexpnb", 33 | "displayName": "Regex Notebook", 34 | "priority": "default", 35 | "selector": [ 36 | { 37 | "filenamePattern": "*.regexnb" 38 | } 39 | ] 40 | } 41 | ], 42 | "notebookRenderer": [ 43 | { 44 | "id": "regexp-renderer", 45 | "entrypoint": "./dist/regexper-renderer.js", 46 | "displayName": "Regex Renderer", 47 | "mimeTypes": [ 48 | "application/x.regexp" 49 | ] 50 | } 51 | ], 52 | "commands": [ 53 | { 54 | "title": "New Regex Notebook", 55 | "shortTitle": "Regex Notebook", 56 | "command": "regexnb.new" 57 | } 58 | ], 59 | "menus": { 60 | "file/newFile": [ 61 | { 62 | "command": "regexnb.new" 63 | } 64 | ] 65 | } 66 | }, 67 | "scripts": { 68 | "vscode:prepublish": "npm run compile", 69 | "compile": "node esbuild.js", 70 | "lint": "eslint src --ext ts" 71 | }, 72 | "devDependencies": { 73 | "@types/vscode": "1.61.0", 74 | "@types/vscode-notebook-renderer": "^1.57.8", 75 | "@typescript-eslint/eslint-plugin": "^3.8.0", 76 | "@typescript-eslint/parser": "^3.8.0", 77 | "esbuild": "^0.12.6", 78 | "eslint": "^7.6.0", 79 | "typescript": "^4.3.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | 4 | let pegLoader = { 5 | name: 'peg-loader', 6 | setup(build) { 7 | let fs = require('fs'); 8 | let canopy = require('./src/renderer/regexper-static/node_modules/canopy'); 9 | 10 | // Load ".txt" files and return an array of words 11 | build.onLoad({ filter: /\.peg$/ }, async (args) => { 12 | let peg = await fs.promises.readFile(args.path, 'utf8'); 13 | let js = canopy.compile(peg); 14 | return { 15 | contents: js, 16 | loader: 'js', 17 | }; 18 | }); 19 | }, 20 | }; 21 | 22 | let fixer = { 23 | name: 'fixer', 24 | setup(build) { 25 | let fs = require('fs'); 26 | build.onEnd(async result => { 27 | const p = path.join(__dirname, 'dist/regexper-renderer.js'); 28 | const source = await fs.promises.readFile(p, 'utf8'); 29 | 30 | const modSource = String(source) 31 | .replace('eve = function(name, scope) {', 'var eve = function(name, scope) {') // esbuild issue? 32 | .replace(/export {\n activate\n\};/m, 'export { activate }'); // https://github.com/microsoft/vscode/issues/125519 33 | 34 | await fs.promises.writeFile(p, modSource); 35 | }); 36 | }, 37 | }; 38 | 39 | require('esbuild').build({ 40 | entryPoints: [path.join(__dirname, 'src/renderer/main.js')], 41 | bundle: true, 42 | format: 'esm', 43 | // minify: true, 44 | platform: 'browser', 45 | outfile: path.join(__dirname, 'dist/regexper-renderer.js'), 46 | plugins: [pegLoader, fixer], 47 | }).catch(() => process.exit(1)); 48 | 49 | require('esbuild').build({ 50 | entryPoints: [path.join(__dirname, 'src/extension/extension.ts')], 51 | bundle: true, 52 | format: 'cjs', 53 | // minify: true, 54 | platform: 'node', 55 | outfile: path.join(__dirname, 'dist/extension.js'), 56 | external: ['vscode'] 57 | }).catch(() => process.exit(1)); 58 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 4 | import Parser from './regexper-static/src/js/parser/javascript.js'; 5 | 6 | export const activate = () => { 7 | 8 | /** 9 | * 10 | * @param {import('vscode-notebook-renderer').OutputItem} item 11 | * @param {HTMLElement} element 12 | */ 13 | async function renderOutputItem(item, element) { 14 | 15 | element.innerHTML = ` 16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 |
`; 25 | 26 | try { 27 | const parser = new Parser(element, { keepContent: true }); 28 | await parser.parse(item.text()); 29 | 30 | // unset flags, rendered in status bar 31 | parser.parsed.flags = []; 32 | 33 | await parser.render(); 34 | 35 | } catch (err) { 36 | console.error(err); 37 | element.innerText = String(err); 38 | } 39 | } 40 | 41 | return { renderOutputItem }; 42 | }; 43 | 44 | // append base element 45 | // 46 | const containerBase = document.createElement('script'); 47 | document.head.appendChild(containerBase); 48 | containerBase.type = 'text/html'; 49 | containerBase.id = 'svg-container-base'; 50 | 51 | const style = document.createElement('style'); 52 | document.head.appendChild(style); 53 | style.innerText = ` 54 | svg { 55 | 56 | } 57 | .root text, 58 | .root tspan { 59 | font: 12px Arial; 60 | } 61 | .root path { 62 | fill-opacity: 0; 63 | stroke-width: 2px; 64 | stroke: var(--theme-foreground); 65 | } 66 | .root circle { 67 | fill: var(--theme-menu-hover-background); 68 | stroke-width: 2px; 69 | stroke: var(--theme-foreground); 70 | } 71 | .anchor text, 72 | .any-character text { 73 | fill: var(--theme-foreground); 74 | } 75 | .anchor rect, 76 | .any-character rect { 77 | fill: var(--theme-menu-hover-background); 78 | } 79 | .escape text, 80 | .charset-escape text, 81 | .literal text { 82 | fill: var(--theme-foreground); 83 | } 84 | .escape rect, 85 | .charset-escape rect { 86 | fill: #bada55; 87 | } 88 | .literal rect { 89 | fill: var(--theme-button-background); 90 | } 91 | .charset .charset-box { 92 | fill: var(--theme-menu-hover-background); 93 | } 94 | .subexp .subexp-label tspan, 95 | .charset .charset-label tspan, 96 | .match-fragment .repeat-label tspan { 97 | font-size: 10px; 98 | } 99 | .repeat-label { 100 | cursor: help; 101 | } 102 | .subexp .subexp-label tspan, 103 | .charset .charset-label tspan { 104 | dominant-baseline: text-after-edge; 105 | } 106 | .subexp .subexp-box { 107 | stroke: #908c83; 108 | stroke-dasharray: 6, 2; 109 | stroke-width: 2px; 110 | fill-opacity: 0; 111 | } 112 | .quote { 113 | fill: rgba(var(--theme-foreground), 0.5); 114 | } 115 | `; 116 | -------------------------------------------------------------------------------- /src/extension/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function activate(context: vscode.ExtensionContext) { 4 | 5 | // make notebook data from bytes and vice versa 6 | context.subscriptions.push(vscode.workspace.registerNotebookSerializer('regexpnb', new class implements vscode.NotebookSerializer { 7 | 8 | private readonly _decoder = new TextDecoder(); 9 | private readonly _encoder = new TextEncoder(); 10 | 11 | deserializeNotebook(data: Uint8Array): vscode.NotebookData { 12 | const cells: vscode.NotebookCellData[] = []; 13 | const str = this._decoder.decode(data); 14 | const lines = str.split('\n'); 15 | 16 | for (const line of lines) { 17 | let cell: vscode.NotebookCellData | undefined; 18 | let value: string; 19 | try { 20 | value = JSON.parse(line.substring(4)); 21 | } catch { 22 | continue; 23 | } 24 | if (line.startsWith('RE: ')) { 25 | // regex-cell 26 | cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, value, 'plaintext'); 27 | cell.outputs = [new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(cell.value, 'application/x.regexp')])]; 28 | 29 | } else if (line.startsWith('MD: ')) { 30 | // markdown-cell 31 | cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, value, 'markdown'); 32 | } 33 | 34 | if (cell) { 35 | cells.push(cell); 36 | } 37 | } 38 | return new vscode.NotebookData(cells); 39 | } 40 | 41 | serializeNotebook(data: vscode.NotebookData): Uint8Array { 42 | const lines: string[] = []; 43 | for (const cell of data.cells) { 44 | if (cell.kind === vscode.NotebookCellKind.Code) { 45 | lines.push(`RE: ${JSON.stringify(cell.value)}`); 46 | } else { 47 | lines.push(`MD: ${JSON.stringify(cell.value)}`); 48 | } 49 | } 50 | return this._encoder.encode(lines.join('\n')); 51 | } 52 | }, { transientOutputs: true })); 53 | 54 | 55 | // "execute" a regular expression 56 | const controller = vscode.notebooks.createNotebookController('regex-kernel', 'regexpnb', 'Regex'); 57 | controller.supportedLanguages = ['plaintext']; 58 | controller.executeHandler = (cells: vscode.NotebookCell[]) => { 59 | for (const cell of cells) { 60 | const execution = controller.createNotebookCellExecution(cell); 61 | execution.start(); 62 | const cellContent = execution.cell.document.getText(); 63 | 64 | try { 65 | // "validation" by parsing 66 | const match = /\/(.*)\/(.*)/.exec(cellContent); 67 | if (match) { 68 | new RegExp(match[1], match[2]); 69 | } else { 70 | new RegExp(cellContent); 71 | } 72 | } catch (err) { 73 | // show validation error and continue with next cell 74 | const errorItem = vscode.NotebookCellOutputItem.error(err as Error); 75 | const regexOutput = new vscode.NotebookCellOutput([errorItem]); 76 | execution.replaceOutput(regexOutput); 77 | execution.end(false); 78 | 79 | continue; 80 | } 81 | 82 | const regexItem = vscode.NotebookCellOutputItem.text(cellContent, 'application/x.regexp'); 83 | const regexOutput = new vscode.NotebookCellOutput([regexItem]); 84 | execution.replaceOutput(regexOutput); 85 | execution.end(undefined); 86 | } 87 | }; 88 | context.subscriptions.push(controller); 89 | 90 | // status bar provider 91 | context.subscriptions.push(vscode.notebooks.registerNotebookCellStatusBarItemProvider('regexpnb', new class implements vscode.NotebookCellStatusBarItemProvider { 92 | 93 | provideCellStatusBarItems(cell: vscode.NotebookCell): vscode.ProviderResult { 94 | const cellContent = cell.document.getText(); 95 | const flags = new Set(); 96 | try { 97 | const idx = cellContent.lastIndexOf('/'); 98 | if (idx > 0 && idx < cellContent.length) { 99 | cellContent.substring(idx + 1).split('').map(f => { 100 | switch (f) { 101 | case 'g': return flags.add('global'); 102 | case 'i': return flags.add('ignoreCase'); 103 | case 'm': return flags.add('multiline'); 104 | case 'u': return flags.add('unicode'); 105 | case 'y': return flags.add('sticky'); 106 | } 107 | return ''; 108 | }).sort().filter(s => !!s); 109 | } 110 | } catch { } 111 | 112 | if (flags.size > 0) { 113 | return [new vscode.NotebookCellStatusBarItem( 114 | 'Flags: ' + Array.from(flags).join(', '), 115 | vscode.NotebookCellStatusBarAlignment.Right 116 | )]; 117 | } 118 | } 119 | })); 120 | 121 | // new notebook command 122 | 123 | context.subscriptions.push(vscode.commands.registerCommand('regexnb.new', async function () { 124 | const newNotebook = await vscode.workspace.openNotebookDocument('regexpnb', new vscode.NotebookData( 125 | [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '/Hello{1,7} Notebooks/', 'plaintext')] 126 | )); 127 | 128 | await vscode.commands.executeCommand('vscode.open', newNotebook.uri); 129 | })); 130 | } 131 | --------------------------------------------------------------------------------