From 0ede211d4f35a706bd0bf1875a7c3f9d9760f19d Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:40:50 -0400 Subject: [PATCH] Add vscode-webview-ui-toolkit and follow tutorial to get started --- .eslintrc.json | 2 +- .prettierrc.json | 7 +++ esbuild.js | 90 ++++++++++++++++------------ package-lock.json | 123 ++++++++++++++++++++++++++++++++++++++ package.json | 16 +++-- src/HelloWorldPanel.ts | 112 ++++++++++++++++++++++++++++++++++ src/extension.ts | 24 +++++--- src/utilities/getNonce.ts | 8 +++ src/utilities/getUri.ts | 5 ++ src/webview/main.ts | 25 ++++++++ tsconfig.json | 31 ++++++---- 11 files changed, 378 insertions(+), 65 deletions(-) create mode 100644 .prettierrc.json create mode 100644 src/HelloWorldPanel.ts create mode 100644 src/utilities/getNonce.ts create mode 100644 src/utilities/getUri.ts create mode 100644 src/webview/main.ts diff --git a/.eslintrc.json b/.eslintrc.json index 86c86f3..6cdfaf3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,7 @@ "format": [ "camelCase", "PascalCase" ] } ], - "@typescript-eslint/semi": "warn", + "@typescript-eslint/semi": "off", "curly": "warn", "eqeqeq": "warn", "no-throw-literal": "warn", diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..6815451 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": true, + "printWidth": 120, + "semi": false, + "jsxBracketSameLine": true +} \ No newline at end of file diff --git a/esbuild.js b/esbuild.js index cc2be59..464050f 100644 --- a/esbuild.js +++ b/esbuild.js @@ -1,56 +1,72 @@ -const esbuild = require("esbuild"); +const esbuild = require("esbuild") -const production = process.argv.includes('--production'); -const watch = process.argv.includes('--watch'); +const production = process.argv.includes("--production") +const watch = process.argv.includes("--watch") /** * @type {import('esbuild').Plugin} */ const esbuildProblemMatcherPlugin = { - name: 'esbuild-problem-matcher', + name: "esbuild-problem-matcher", setup(build) { build.onStart(() => { - console.log('[watch] build started'); - }); + console.log("[watch] build started") + }) build.onEnd((result) => { result.errors.forEach(({ text, location }) => { - console.error(`✘ [ERROR] ${text}`); - console.error(` ${location.file}:${location.line}:${location.column}:`); - }); - console.log('[watch] build finished'); - }); + console.error(`✘ [ERROR] ${text}`) + console.error(` ${location.file}:${location.line}:${location.column}:`) + }) + console.log("[watch] build finished") + }) }, -}; +} + +const baseConfig = { + bundle: true, + minify: production, + sourcemap: !production, + logLevel: "silent", + plugins: [ + /* add to the end of plugins array */ + esbuildProblemMatcherPlugin, + ], +} + +const extensionConfig = { + ...baseConfig, + entryPoints: ["src/extension.ts"], + format: "cjs", + sourcesContent: false, + platform: "node", + outfile: "dist/extension.js", + external: ["vscode"], +} + +const webviewConfig = { + ...baseConfig, + target: "es2020", + format: "esm", + entryPoints: ["src/webview/main.ts"], + outfile: "dist/webview.js", +} async function main() { - const ctx = await esbuild.context({ - entryPoints: [ - 'src/extension.ts' - ], - bundle: true, - format: 'cjs', - minify: production, - sourcemap: !production, - sourcesContent: false, - platform: 'node', - outfile: 'dist/extension.js', - external: ['vscode'], - logLevel: 'silent', - plugins: [ - /* add to the end of plugins array */ - esbuildProblemMatcherPlugin, - ], - }); + const extensionCtx = await esbuild.context(extensionConfig) + const webviewCtx = await esbuild.context(webviewConfig) if (watch) { - await ctx.watch(); + await extensionCtx.watch() + await webviewCtx.watch() } else { - await ctx.rebuild(); - await ctx.dispose(); + await extensionCtx.rebuild() + await extensionCtx.dispose() + await webviewCtx.rebuild() + await webviewCtx.dispose() } } -main().catch(e => { - console.error(e); - process.exit(1); -}); +main().catch((e) => { + console.error(e) + process.exit(1) +}) diff --git a/package-lock.json b/package-lock.json index c4322d2..63888b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,14 @@ "": { "name": "claude-dev", "version": "0.0.1", + "dependencies": { + "@vscode/webview-ui-toolkit": "^1.4.0" + }, "devDependencies": { "@types/mocha": "^10.0.7", "@types/node": "20.x", "@types/vscode": "^1.82.0", + "@types/vscode-webview": "^1.57.5", "@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/parser": "^7.11.0", "@vscode/test-cli": "^0.0.9", @@ -653,6 +657,52 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/fast-element": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.13.0.tgz", + "integrity": "sha512-iFhzKbbD0cFRo9cEzLS3Tdo9BYuatdxmCEKCpZs1Cro/93zNMpZ/Y9/Z7SknmW6fhDZbpBvtO8lLh9TFEcNVAQ==", + "license": "MIT" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.49.6", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.49.6.tgz", + "integrity": "sha512-DZVr+J/NIoskFC1Y6xnAowrMkdbf2d5o7UyWK6gW5AiQ6S386Ql8dw4KcC4kHaeE1yL2CKvweE79cj6ZhJhTvA==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.13.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + } + }, + "node_modules/@microsoft/fast-foundation/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@microsoft/fast-react-wrapper": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.3.24.tgz", + "integrity": "sha512-sRnSBIKaO42p4mYoYR60spWVkg89wFxFAgQETIMazAm2TxtlsnsGszJnTwVhXq2Uz+XNiD8eKBkfzK5c/i6/Kw==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.13.0", + "@microsoft/fast-foundation": "^2.49.6" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "license": "MIT", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -733,6 +783,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/vscode-webview": { + "version": "1.57.5", + "resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.5.tgz", + "integrity": "sha512-iBAUYNYkz+uk1kdsq05fEcoh8gJmwT3lqqFPN7MGyjQ3HVloViMdo7ZJ8DFIP8WOK74PjOEilosqAyxV2iUFUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz", @@ -974,6 +1031,21 @@ "node": ">=16" } }, + "node_modules/@vscode/webview-ui-toolkit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.4.0.tgz", + "integrity": "sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.4", + "@microsoft/fast-react-wrapper": "^0.3.22", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -2118,6 +2190,12 @@ "node": ">=0.10.0" } }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3159,6 +3237,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3303,6 +3388,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", @@ -4275,6 +4373,19 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -4980,6 +5091,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==", + "license": "MIT" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -5084,6 +5201,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 0aadce0..cf6c694 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,20 @@ "test": "vscode-test" }, "devDependencies": { - "@types/vscode": "^1.82.0", "@types/mocha": "^10.0.7", "@types/node": "20.x", + "@types/vscode": "^1.82.0", + "@types/vscode-webview": "^1.57.5", "@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/parser": "^7.11.0", - "eslint": "^8.57.0", - "esbuild": "^0.21.5", - "npm-run-all": "^4.1.5", - "typescript": "^5.4.5", "@vscode/test-cli": "^0.0.9", - "@vscode/test-electron": "^2.4.0" + "@vscode/test-electron": "^2.4.0", + "esbuild": "^0.21.5", + "eslint": "^8.57.0", + "npm-run-all": "^4.1.5", + "typescript": "^5.4.5" + }, + "dependencies": { + "@vscode/webview-ui-toolkit": "^1.4.0" } } diff --git a/src/HelloWorldPanel.ts b/src/HelloWorldPanel.ts new file mode 100644 index 0000000..05a6cd4 --- /dev/null +++ b/src/HelloWorldPanel.ts @@ -0,0 +1,112 @@ +/* +Example of vscode-webview-ui-toolkit +https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/docs/getting-started.md +https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/docs/components.md +*/ + + +import * as vscode from "vscode" +import { getUri } from "./utilities/getUri" +import { getNonce } from "./utilities/getNonce" + +export class HelloWorldPanel { + /* + - public can be access outside of class + - private can only be accessed by class itself (_ is a convention not required) + - readonly means var can only be set during declaration or in constructor + - static means var is shared among all instances of class + */ + public static currentPanel: HelloWorldPanel | undefined + private readonly panel: vscode.WebviewPanel + private disposables: vscode.Disposable[] = [] + + private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { + this.panel = panel + + // the method can be triggered when the webview panel is closed + this.panel.onDidDispose(() => this.dispose(), null, this.disposables) + + this.panel.webview.html = this.getWebviewContent(this.panel.webview, extensionUri) + this.setWebviewMessageListener(this.panel.webview); + } + + // This will be responsible for rendering the current webview panel – if it exists – or creating and displaying a new webview panel. + public static render(extensionUri: vscode.Uri) { + if (HelloWorldPanel.currentPanel) { + HelloWorldPanel.currentPanel.panel.reveal(vscode.ViewColumn.One) + } else { + const panel = vscode.window.createWebviewPanel("helloworld", "Hello World", vscode.ViewColumn.One, { + // Enable javascript in the webview + enableScripts: true, + // Restrict the webview to only load resources from the `out` directory + localResourceRoots: [vscode.Uri.joinPath(extensionUri, "dist")], + }) + + HelloWorldPanel.currentPanel = new HelloWorldPanel(panel, extensionUri) + } + } + + // webview resources are cleaned up when the webview panel is closed by the user or closed programmatically. + public dispose() { + HelloWorldPanel.currentPanel = undefined + + this.panel.dispose() + + while (this.disposables.length) { + const disposable = this.disposables.pop() + if (disposable) { + disposable.dispose() + } + } + } + + // where the UI of the extension will be defined. This is also where references to CSS and JavaScript files are created and inserted into the webview HTML. + private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { + const webviewUri = getUri(webview, extensionUri, ["dist", "webview.js"]) + /* + content security policy of your webview to only allow scripts that have a specific nonce + create a content security policy meta tag so that only loading scripts with a nonce is allowed + As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g. + + + + in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial. + */ + const nonce = getNonce() + + return /*html*/ ` + + + + + + + Hello World! + + +

Hello World!

+ Howdy! + + + + ` + } + + // responsible for setting up an event listener that listens for messages passed from the webview context and executes code based on the received message. + private setWebviewMessageListener(webview: vscode.Webview) { + webview.onDidReceiveMessage( + (message: any) => { + const command = message.command + const text = message.text + + switch (command) { + case "hello": + vscode.window.showInformationMessage(text) + return + } + }, + undefined, + this.disposables + ) + } +} diff --git a/src/extension.ts b/src/extension.ts index 8dca176..c004365 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,25 +1,31 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode'; +import * as vscode from "vscode" +import { HelloWorldPanel } from "./HelloWorldPanel" // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { - // Use the console to output diagnostic information (console.log) and errors (console.error) // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "claude-dev" is now active!'); + console.log('Congratulations, your extension "claude-dev" is now active!') // The command has been defined in the package.json file // Now provide the implementation of the command with registerCommand // The commandId parameter must match the command field in package.json - const disposable = vscode.commands.registerCommand('claude-dev.helloWorld', () => { - // The code you place here will be executed every time your command is executed - // Display a message box to the user - vscode.window.showInformationMessage('Hello World from claude-dev!'); - }); + // const disposable = vscode.commands.registerCommand("claude-dev.helloWorld", () => { + // // The code you place here will be executed every time your command is executed + // // Display a message box to the user + // vscode.window.showInformationMessage("Hello World from claude-dev!") + // }) - context.subscriptions.push(disposable); + // context.subscriptions.push(disposable) + + const helloCommand = vscode.commands.registerCommand("claude-dev.helloWorld", () => { + HelloWorldPanel.render(context.extensionUri) + }) + + context.subscriptions.push(helloCommand) } // This method is called when your extension is deactivated diff --git a/src/utilities/getNonce.ts b/src/utilities/getNonce.ts new file mode 100644 index 0000000..63eecfd --- /dev/null +++ b/src/utilities/getNonce.ts @@ -0,0 +1,8 @@ +export function getNonce() { + let text = "" + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text +} diff --git a/src/utilities/getUri.ts b/src/utilities/getUri.ts new file mode 100644 index 0000000..9a2a5b3 --- /dev/null +++ b/src/utilities/getUri.ts @@ -0,0 +1,5 @@ +import { Uri, Webview } from "vscode" + +export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)) +} diff --git a/src/webview/main.ts b/src/webview/main.ts new file mode 100644 index 0000000..7244f8a --- /dev/null +++ b/src/webview/main.ts @@ -0,0 +1,25 @@ +import { provideVSCodeDesignSystem, vsCodeButton, vsCodeCheckbox } from "@vscode/webview-ui-toolkit" +// const toolkit = require("@vscode/webview-ui-toolkit") +// /* +// You must register the components you want to use +// */ +provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeCheckbox()) + +const vscode = acquireVsCodeApi(); + +window.addEventListener("load", main); + +function main() { + // To get improved type annotations/IntelliSense the associated class for + // a given toolkit component can be imported and used to type cast a reference + // to the element (i.e. the `as Button` syntax) + const howdyButton = document.getElementById("howdy") as Button; + howdyButton?.addEventListener("click", handleHowdyClick); +} + +function handleHowdyClick() { + vscode.postMessage({ + command: "hello", + text: "Hey there partner! 🤠", + }); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8a79f20..0ca80e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,23 @@ { "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "lib": [ - "ES2022" - ], - "sourceMap": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["es2022", "esnext.disposable", "DOM"], + "module": "esnext", + "moduleResolution": "Bundler", + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "resolveJsonModule": true, "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2022", + "useDefineForClassFields": true, + "useUnknownInCatchVariables": false } -} +} \ No newline at end of file