├── .gitignore ├── media ├── logo.png ├── screenshots │ ├── main.png │ └── side.png ├── logo.svg └── icons │ ├── dark.svg │ └── light.svg ├── .vscodeignore ├── CHANGELOG.md ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── tsconfig.json ├── README.md ├── eslint.config.mjs ├── LICENSE ├── assets └── webview.css ├── src ├── webview.ts └── extension.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | *.vsix 5 | -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/workspace-notepad/main/media/logo.png -------------------------------------------------------------------------------- /media/screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/workspace-notepad/main/media/screenshots/main.png -------------------------------------------------------------------------------- /media/screenshots/side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/workspace-notepad/main/media/screenshots/side.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | **/tsconfig.json 6 | **/eslint.config.mjs 7 | **/*.map 8 | **/*.ts 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.0.1 4 | 5 | - Corrected readme image sizes 6 | - Improve accessibility 7 | 8 | ## 1.0.0 9 | 10 | - Initial release 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 | "ms-vscode.extension-test-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2022", 5 | "outDir": "out", 6 | "lib": ["ES2022", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true, 10 | "removeComments": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /media/icons/dark.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /media/icons/light.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.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": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.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 | "svg.preview.background": "transparent" 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workspace Notepad 2 | 3 | Minimal and convenient notepad text box unique to each workspace/opened folder. 4 | 5 | ## Features 6 | 7 | ### Side Panel Notepad 8 | 9 | Minimal and convenient editor that can be accessed from the activity bar. Can be repositioned to the bottom panel for further convenience. 10 | 11 | 12 | 13 | ### Tab Notepad 14 | 15 | Larger editor that can be opened just like a normal file. The tab is persisted across reloads. 16 | 17 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | languageOptions: { 11 | parser: tsParser, 12 | ecmaVersion: 2022, 13 | sourceType: "module", 14 | }, 15 | rules: { 16 | "@typescript-eslint/naming-convention": ["warn", { 17 | selector: "import", 18 | format: ["camelCase", "PascalCase"], 19 | }], 20 | 21 | curly: "warn", 22 | eqeqeq: "warn", 23 | "no-throw-literal": "warn", 24 | semi: "warn", 25 | }, 26 | }]; -------------------------------------------------------------------------------- /.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": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Brandon Fowler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /assets/webview.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | padding: 0.5rem; 12 | padding-top: 0; 13 | } 14 | 15 | body[data-type="mainEditor"] { 16 | padding: 0; 17 | } 18 | 19 | #disabled-dialog { 20 | padding: 0; 21 | background: transparent; 22 | border: none; 23 | } 24 | 25 | #disabled-dialog::backdrop { 26 | background: var(--vscode-widget-shadow); 27 | } 28 | 29 | #activate { 30 | background: var(--vscode-button-background); 31 | color: var(--vscode-button-foreground); 32 | box-shadow: 0px 0px 7px 0px var(--vscode-widget-shadow); 33 | padding: 0.5rem 1rem; 34 | margin: 1em; 35 | border: none; 36 | border-radius: 2px; 37 | cursor: pointer; 38 | } 39 | 40 | #activate:focus, 41 | #activate:hover { 42 | background: var(--vscode-button-hoverBackground); 43 | } 44 | 45 | #notepad { 46 | color-scheme: light dark; 47 | width: 100%; 48 | height: 100%; 49 | resize: none; 50 | padding: 0.5rem; 51 | background: var(--vscode-editor-background); 52 | color: var(--vscode-editor-foreground); 53 | } 54 | 55 | #notepad:focus { 56 | outline: none; 57 | } -------------------------------------------------------------------------------- /src/webview.ts: -------------------------------------------------------------------------------- 1 | const vscode = acquireVsCodeApi(); 2 | const notepad = document.getElementById('notepad')!; 3 | const disabledDialog = document.getElementById('disabled-dialog')! as HTMLDialogElement; 4 | const enable = document.getElementById('activate')!; 5 | 6 | function setUpNotepad() { 7 | notepad.focus(); 8 | 9 | const range = document.createRange(); 10 | range.selectNodeContents(notepad); 11 | range.collapse(false); // Collapse to end of selection 12 | 13 | const selection = window.getSelection()!; 14 | selection.removeAllRanges(); 15 | selection.addRange(range); 16 | } 17 | 18 | function makeActive() { 19 | vscode.postMessage({ 20 | action: 'activate', 21 | type: document.body.getAttribute('data-type') 22 | }); 23 | } 24 | 25 | makeActive(); 26 | 27 | notepad.addEventListener('input', () => vscode.postMessage({ 28 | action: 'input', 29 | text: notepad.innerText 30 | })); 31 | 32 | enable.addEventListener('click', makeActive); 33 | 34 | window.addEventListener('message', e => { 35 | switch (e.data.action) { 36 | case 'disable': 37 | disabledDialog.showModal(); 38 | notepad.setAttribute('contenteditable', 'false'); 39 | notepad.setAttribute('aria-disabled', 'true'); 40 | 41 | break; 42 | case 'activate': 43 | disabledDialog.close(); 44 | notepad.setAttribute('contenteditable', 'plaintext-only'); 45 | notepad.setAttribute('aria-disabled', 'false'); 46 | 47 | notepad.innerText = e.data.text; 48 | setUpNotepad(); 49 | 50 | break; 51 | } 52 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace-notepad", 3 | "displayName": "Workspace Notepad", 4 | "description": "Minimal and convenient notepad text box unique to each workspace/opened folder.", 5 | "publisher": "brandonfowler", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/BrandonXLF/workspace-notepad" 10 | }, 11 | "homepage": "https://github.com/BrandonXLF/workspace-notepad", 12 | "bugs": { 13 | "url": "https://github.com/BrandonXLF/workspace-notepad/issues" 14 | }, 15 | "icon": "media/logo.png", 16 | "version": "1.0.1", 17 | "engines": { 18 | "vscode": "^1.74.0" 19 | }, 20 | "categories": [ 21 | "Other" 22 | ], 23 | "keywords": [ 24 | "notepad", 25 | "notes", 26 | "workspace", 27 | "folder" 28 | ], 29 | "activationEvents": [ 30 | "onWebviewPanel:workspace-notepad-main-editor" 31 | ], 32 | "main": "./out/extension.js", 33 | "scripts": { 34 | "vscode:prepublish": "npm run compile", 35 | "compile": "tsc -p ./", 36 | "watch": "tsc -watch -p ./", 37 | "lint": "eslint src" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "20.x", 41 | "@types/vscode": "^1.74.0", 42 | "@types/vscode-webview": "^1.57.5", 43 | "@typescript-eslint/eslint-plugin": "^8.25.0", 44 | "@typescript-eslint/parser": "^8.25.0", 45 | "eslint": "^9.21.0", 46 | "typescript": "^5.7.3" 47 | }, 48 | "contributes": { 49 | "commands": [ 50 | { 51 | "command": "workspace-notepad.download", 52 | "category": "Workspace Notepad", 53 | "title": "Download Notepad", 54 | "icon": "$(desktop-download)" 55 | }, 56 | { 57 | "command": "workspace-notepad.open-main-editor", 58 | "category": "Workspace Notepad", 59 | "title": "Open in Main Editor", 60 | "icon": "$(window)" 61 | }, 62 | { 63 | "command": "workspace-notepad.move-to-side-view", 64 | "category": "Workspace Notepad", 65 | "title": "Move to Side Panel", 66 | "icon": "$(layout-sidebar-left)" 67 | } 68 | ], 69 | "viewsContainers": { 70 | "activitybar": [ 71 | { 72 | "id": "workspace-notepad-container", 73 | "title": "Workspace Notepad", 74 | "icon": "media/icons/light.svg" 75 | } 76 | ] 77 | }, 78 | "views": { 79 | "workspace-notepad-container": [ 80 | { 81 | "id": "workspace-notepad-side-view", 82 | "type": "webview", 83 | "name": "Workspace Notepad", 84 | "icon": "media/icons/light.svg" 85 | } 86 | ] 87 | }, 88 | "menus": { 89 | "view/title": [ 90 | { 91 | "command": "workspace-notepad.download", 92 | "group": "navigation@1", 93 | "when": "view == workspace-notepad-side-view" 94 | 95 | }, 96 | { 97 | "command": "workspace-notepad.open-main-editor", 98 | "group": "navigation@2", 99 | "when": "view == workspace-notepad-side-view" 100 | 101 | } 102 | ], 103 | "editor/title": [ 104 | { 105 | "command": "workspace-notepad.download", 106 | "group": "navigation@1", 107 | "when": "activeWebviewPanelId == workspace-notepad-main-editor" 108 | }, 109 | { 110 | "command": "workspace-notepad.move-to-side-view", 111 | "group": "navigation@2", 112 | "when": "activeWebviewPanelId == workspace-notepad-main-editor" 113 | } 114 | ], 115 | "webview/context": [ 116 | { 117 | "command": "workspace-notepad.open-main-editor", 118 | "group": "z_commands@1", 119 | "when": "webviewId == workspace-notepad-side-view" 120 | }, 121 | { 122 | "command": "workspace-notepad.move-to-side-view", 123 | "group": "z_commands@1", 124 | "when": "webviewId == workspace-notepad-main-editor" 125 | }, 126 | { 127 | "command": "workspace-notepad.download", 128 | "group": "z_commands@2", 129 | "when": "webviewId == workspace-notepad-side-view || webviewId == workspace-notepad-main-editor" 130 | } 131 | ], 132 | "commandPalette": [ 133 | { 134 | "command": "workspace-notepad.move-to-side-view", 135 | "when": "false" 136 | } 137 | ] 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | type KeysForType = { [K in keyof T]: T[K] extends U ? K : never; }[keyof T]; 4 | 5 | const enum WebviewType { 6 | MainEditor = 'mainEditor', 7 | SideView = 'sideView' 8 | }; 9 | 10 | interface WebviewContainers { 11 | [WebviewType.MainEditor]?: vscode.WebviewPanel; 12 | [WebviewType.SideView]?: vscode.WebviewView; 13 | } 14 | 15 | class NotepadStorage { 16 | constructor(private readonly ctx: vscode.ExtensionContext) {} 17 | 18 | getText() { 19 | return this.ctx.workspaceState.get('workspace-notepad') ?? ''; 20 | } 21 | 22 | setText(txt: string) { 23 | this.ctx.workspaceState.update('workspace-notepad', txt); 24 | } 25 | 26 | async download() { 27 | const txt = this.getText(); 28 | 29 | const uri = await vscode.window.showSaveDialog({ 30 | defaultUri: vscode.Uri.file('workspace-notepad.txt') 31 | }); 32 | 33 | if (!uri) { 34 | return; 35 | } 36 | 37 | await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(txt)); 38 | } 39 | } 40 | 41 | class ActiveHistory { 42 | private readonly history = new Set(); 43 | 44 | push(type: T) { 45 | this.history.delete(type); // Move to the end 46 | this.history.add(type); 47 | } 48 | 49 | remove(type: T) { 50 | this.history.delete(type); 51 | } 52 | 53 | get empty() { 54 | return this.history.size === 0; 55 | } 56 | 57 | get active() { 58 | return [...this.history].pop(); 59 | } 60 | } 61 | 62 | class WebviewProvider implements vscode.WebviewViewProvider { 63 | private readonly containers: WebviewContainers = {}; 64 | private readonly activeHistory = new ActiveHistory(); 65 | 66 | constructor( 67 | private readonly ctx: vscode.ExtensionContext, 68 | private readonly storage: NotepadStorage 69 | ) {} 70 | 71 | private getHtml(webview: vscode.Webview, type: WebviewType) { 72 | const styleURI = webview.asWebviewUri( 73 | vscode.Uri.joinPath(this.ctx.extensionUri, 'assets', 'webview.css') 74 | ); 75 | const scriptURI = webview.asWebviewUri( 76 | vscode.Uri.joinPath(this.ctx.extensionUri, 'out', 'webview.js') 77 | ); 78 | 79 | return ` 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
96 | Loading... 97 |
98 | 99 | 100 | 101 | `; 102 | } 103 | 104 | private makeActive(type: WebviewType) { 105 | if (!this.containers[type]) { 106 | return; 107 | } 108 | 109 | for (const [otherType, container] of Object.entries(this.containers)) { 110 | if (otherType !== type) { 111 | container.webview.postMessage({ action: 'disable' }); 112 | } 113 | } 114 | 115 | this.activeHistory.push(type); 116 | 117 | this.containers[type].webview.postMessage({ 118 | action: 'activate', 119 | text: this.storage.getText() 120 | }); 121 | } 122 | 123 | private onInactive(type: WebviewType) { 124 | const isActive = this.activeHistory.active === type; 125 | 126 | this.activeHistory.remove(type); 127 | 128 | if (isActive && !this.activeHistory.empty) { 129 | this.makeActive(this.activeHistory.active); 130 | } 131 | } 132 | 133 | private handleMessage(msg: any) { 134 | switch (msg.action) { 135 | case 'input': 136 | this.storage.setText(msg.text); 137 | break; 138 | case 'activate': 139 | this.makeActive(msg.type); 140 | break; 141 | } 142 | } 143 | 144 | private handleViewStateChanges[T]>( 145 | type: T, webviewContainer: U, eventName: KeysForType 146 | ) { 147 | let lastVisible = true; 148 | 149 | (webviewContainer[eventName] as Function)(() => { 150 | if (webviewContainer.visible === lastVisible) { 151 | return; 152 | } 153 | 154 | lastVisible = webviewContainer.visible; 155 | 156 | if (webviewContainer.visible) { 157 | this.makeActive(type); 158 | } else { 159 | this.onInactive(type); 160 | } 161 | }); 162 | } 163 | 164 | private resolveWebview[T]>( 165 | type: T, webviewContainer: U, viewStateChangeEvent: KeysForType 166 | ) { 167 | webviewContainer.webview.options = { enableScripts: true }; 168 | webviewContainer.webview.html = this.getHtml(webviewContainer.webview, type); 169 | 170 | this.containers[type] = webviewContainer; 171 | webviewContainer.webview.onDidReceiveMessage(this.handleMessage, this); 172 | this.handleViewStateChanges(type, webviewContainer, viewStateChangeEvent); 173 | 174 | webviewContainer.onDidDispose(() => { 175 | delete this.containers[type]; 176 | this.onInactive(type); 177 | }); 178 | } 179 | 180 | resolveWebviewView(webviewView: vscode.WebviewView) { 181 | this.resolveWebview(WebviewType.SideView, webviewView, 'onDidChangeVisibility'); 182 | } 183 | 184 | resolveMainEditorWebview(webviewPanel: vscode.WebviewPanel) { 185 | this.resolveWebview(WebviewType.MainEditor, webviewPanel, 'onDidChangeViewState'); 186 | } 187 | 188 | showIfExists(type: WebviewType) { 189 | if (!this.containers[type]) { 190 | return false; 191 | } 192 | 193 | switch (type) { 194 | case WebviewType.MainEditor: 195 | this.containers[type].reveal(); 196 | break; 197 | case WebviewType.SideView: 198 | this.containers[type].show(); 199 | break; 200 | } 201 | 202 | this.makeActive(type); 203 | return true; 204 | } 205 | 206 | disposeMainEditorWebview() { 207 | this.containers[WebviewType.MainEditor]?.dispose(); 208 | } 209 | } 210 | 211 | function createMainEditorWebviewPanel(ctx: vscode.ExtensionContext) { 212 | const panel = vscode.window.createWebviewPanel( 213 | 'workspace-notepad-main-editor', 214 | 'Workspace Notepad', 215 | vscode.ViewColumn.Active 216 | ); 217 | 218 | panel.iconPath = { 219 | light: vscode.Uri.joinPath(ctx.extensionUri, 'media', 'icons', 'light.svg'), 220 | dark: vscode.Uri.joinPath(ctx.extensionUri, 'media', 'icons', 'dark.svg') 221 | }; 222 | 223 | return panel; 224 | } 225 | 226 | export function activate(ctx: vscode.ExtensionContext) { 227 | const storage = new NotepadStorage(ctx); 228 | const webViewProvider = new WebviewProvider(ctx, storage); 229 | 230 | ctx.subscriptions.push( 231 | vscode.commands.registerCommand('workspace-notepad.download', () => storage.download()), 232 | vscode.commands.registerCommand('workspace-notepad.open-main-editor', () => { 233 | if (!webViewProvider.showIfExists(WebviewType.MainEditor)) { 234 | webViewProvider.resolveMainEditorWebview( 235 | createMainEditorWebviewPanel(ctx) 236 | ); 237 | } 238 | }), 239 | vscode.commands.registerCommand('workspace-notepad.move-to-side-view', () => { 240 | if (!webViewProvider.showIfExists(WebviewType.SideView)) { 241 | vscode.commands.executeCommand('workspace-notepad-side-view.focus'); 242 | } 243 | 244 | webViewProvider.disposeMainEditorWebview(); 245 | }), 246 | vscode.window.registerWebviewViewProvider('workspace-notepad-side-view', webViewProvider), 247 | vscode.window.registerWebviewPanelSerializer('workspace-notepad-main-editor', { 248 | async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel) { 249 | webViewProvider.resolveMainEditorWebview(webviewPanel); 250 | } 251 | }) 252 | ); 253 | } --------------------------------------------------------------------------------