diff options
author | Chad Norvell <chadnorvell@google.com> | 2023-08-04 06:12:57 +0000 |
---|---|---|
committer | CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-08-04 06:12:57 +0000 |
commit | 2278b06a38f49b752e8c560be2ce8caad389bd88 (patch) | |
tree | 15536e0105054b4d8a70defd4561c8771c007dd1 /pw_ide | |
parent | 3ffc5bd7fb215c23bc7a9c8fffc930034c5aeab7 (diff) | |
download | pigweed-2278b06a38f49b752e8c560be2ce8caad389bd88.tar.gz |
pw_ide: Prototype VS Code extension
Start a VS Code extension to provide functionality that we can't provide
with the current pw_ide + VS Code integration paradigm.
Change-Id: If37d4cb911fecf8bf919831299c83564c90357b1
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/151653
Reviewed-by: Asad Memon <asadmemon@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Commit-Queue: Chad Norvell <chadnorvell@google.com>
Diffstat (limited to 'pw_ide')
-rw-r--r-- | pw_ide/vscode/.vscode/launch.json | 30 | ||||
-rw-r--r-- | pw_ide/vscode/.vscode/settings.json | 12 | ||||
-rw-r--r-- | pw_ide/vscode/.vscode/tasks.json | 18 | ||||
-rw-r--r-- | pw_ide/vscode/CHANGELOG.md | 9 | ||||
-rw-r--r-- | pw_ide/vscode/README.md | 33 | ||||
-rw-r--r-- | pw_ide/vscode/package.json | 45 | ||||
-rw-r--r-- | pw_ide/vscode/pigweed-ide-0.0.1.vsix | bin | 0 -> 19152 bytes | |||
-rw-r--r-- | pw_ide/vscode/src/config.ts | 93 | ||||
-rw-r--r-- | pw_ide/vscode/src/extension.ts | 238 | ||||
-rw-r--r-- | pw_ide/vscode/tsconfig.json | 13 |
10 files changed, 491 insertions, 0 deletions
diff --git a/pw_ide/vscode/.vscode/launch.json b/pw_ide/vscode/.vscode/launch.json new file mode 100644 index 000000000..d21d86d25 --- /dev/null +++ b/pw_ide/vscode/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/pw_ide/vscode/.vscode/settings.json b/pw_ide/vscode/.vscode/settings.json new file mode 100644 index 000000000..3eba7c2be --- /dev/null +++ b/pw_ide/vscode/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "files.exclude": { + "out": false + }, + "search.exclude": { + "out": true + }, + "typescript.tsc.autoDetect": "off", + "editor.rulers": [ + 80 + ] +} diff --git a/pw_ide/vscode/.vscode/tasks.json b/pw_ide/vscode/.vscode/tasks.json new file mode 100644 index 000000000..8a491bd31 --- /dev/null +++ b/pw_ide/vscode/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/pw_ide/vscode/CHANGELOG.md b/pw_ide/vscode/CHANGELOG.md new file mode 100644 index 000000000..5f86168fb --- /dev/null +++ b/pw_ide/vscode/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "pigweed-ide" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release
\ No newline at end of file diff --git a/pw_ide/vscode/README.md b/pw_ide/vscode/README.md new file mode 100644 index 000000000..e23ea3136 --- /dev/null +++ b/pw_ide/vscode/README.md @@ -0,0 +1,33 @@ +# Pigweed Extension for Visual Studio Code + +This is highly experimental! + +## Developing + +- Ensure that you have `npm` installed globally; this doesn't use the + distribution provided by Pigweed yet. + +- Open the `pigweed/pw_ide/vscode` directory directly in Visual Studio Code. + +- Run `npm install` to add all dependencies. + +- Run "Run Extension" in the "Run and Debug" sidebar, or simply hit F5. A new + Visual Studio Code window will open with the extension installed. + +- Make changes. The build will update automatically. Click the little green + circle-with-an-arrow icon at the top of your development window to update + the extension development host with the new build. + +## Building + +- Install the build tool: `npm install -g @vscode/vsce` + +- Build the VSIX: `vsce package` + +## Changelog + +### 0.0.1 + +- Adds the "Pigweed: Check Extensions" command, which prompts the user to + install all recommended extensions and disable all unwanted extensions, as + defined by the project's `extensions.json`. diff --git a/pw_ide/vscode/package.json b/pw_ide/vscode/package.json new file mode 100644 index 000000000..3528cae56 --- /dev/null +++ b/pw_ide/vscode/package.json @@ -0,0 +1,45 @@ +{ + "name": "pigweed-ide", + "displayName": "Pigweed IDE", + "description": "IDE features for Pigweed projects", + "version": "0.0.1", + "engines": { + "vscode": "^1.79.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "pigweed.check-extensions", + "title": "Pigweed: Check Extensions" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "npm run compile && npm run lint", + "lint": "eslint src --ext ts", + "test": "node ./out/test/runTest.js" + }, + "devDependencies": { + "@types/vscode": "^1.79.0", + "@types/glob": "^8.1.0", + "@types/hjson": "2.4.3", + "@types/mocha": "^10.0.1", + "@types/node": "20.2.5", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", + "eslint": "^8.41.0", + "glob": "^8.1.0", + "hjson": "3.2.2", + "mocha": "^10.2.0", + "typescript": "^5.1.3", + "@vscode/test-electron": "^2.3.2" + } +} diff --git a/pw_ide/vscode/pigweed-ide-0.0.1.vsix b/pw_ide/vscode/pigweed-ide-0.0.1.vsix Binary files differnew file mode 100644 index 000000000..084e59aea --- /dev/null +++ b/pw_ide/vscode/pigweed-ide-0.0.1.vsix diff --git a/pw_ide/vscode/src/config.ts b/pw_ide/vscode/src/config.ts new file mode 100644 index 000000000..50c918f75 --- /dev/null +++ b/pw_ide/vscode/src/config.ts @@ -0,0 +1,93 @@ +// Copyright 2023 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import * as hjson from 'hjson'; +import * as vscode from 'vscode'; + +/** + * Schema for extensions.json + */ +export interface ExtensionsJson { + recommendations?: string[]; + unwantedRecommendations?: string[]; +} + +/** + * Partial schema for the workspace config file + */ +interface WorkspaceConfig { + extensions?: ExtensionsJson; +} + +// When the project is opened directly (i.e., by opening the repo directory), +// we have direct access to extensions.json. But if the project is part of a +// workspace (https://code.visualstudio.com/docs/editor/workspaces), we'll get +// a combined config that includes the equivalent of extensions.json associated +// with the "extensions" key. This is taken into consideration only for the sake +// of completeness; Pigweed doesn't currently support the use of workspaces. +type LoadableConfig = ExtensionsJson & WorkspaceConfig; + +/** + * Load a config file that contains extensions.json data. This could be + * extensions.json itself, or a workspace file that contains the equivalent. + * @param uri - A file path to load + * @returns - The extensions.json file data + */ +export async function loadExtensionsJson( + uri: vscode.Uri +): Promise<ExtensionsJson> { + const buffer = await vscode.workspace.fs.readFile(uri); + const config: LoadableConfig = hjson.parse(buffer.toString()); + + if (config.extensions) { + return config.extensions; + } + + return config as ExtensionsJson; +} + +/** + * Find and return the extensions.json data for the project. + * @param includeWorkspace - Also search workspace files + * @returns The extensions.json file data + */ +export async function getExtensionsJson( + includeWorkspace = false +): Promise<ExtensionsJson> { + const files = await vscode.workspace.findFiles( + '.vscode/extensions.json', '**/node_modules/**' + ); + + if (includeWorkspace) { + const workspaceFile = vscode.workspace.workspaceFile; + + if (workspaceFile) { + files.push(workspaceFile); + } + } + + if (files.length == 0) { + // TODO(chadnorvell): Improve this + vscode.window.showErrorMessage('extensions.json is missing!') + throw new Error('extensions.json is missing!') + } else { + if (files.length > 1) { + vscode.window.showWarningMessage( + 'Found multiple extensions.json! Will only use the first.' + ) + } + + return await loadExtensionsJson(files[0]) + } +} diff --git a/pw_ide/vscode/src/extension.ts b/pw_ide/vscode/src/extension.ts new file mode 100644 index 000000000..45fa854b0 --- /dev/null +++ b/pw_ide/vscode/src/extension.ts @@ -0,0 +1,238 @@ +// Copyright 2023 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import * as vscode from 'vscode'; + +import { getExtensionsJson } from './config'; + +/** + * Open the extensions sidebar and show the provided extensions. + * @param extensions - A list of extension IDs + */ +function showExtensions(extensions: string[]) { + vscode.commands.executeCommand( + 'workbench.extensions.search', '@id:' + extensions.join(', @id:'), + ); +} + +/** + * Given a list of extensions, return the subset that are not installed or are + * disabled. + * @param extensions - A list of extension IDs + * @returns A list of extension IDs + */ +function getUnavailableExtensions(extensions: string[]): string[] { + let unavailableExtensions: string[] = []; + const available = vscode.extensions.all; + + // TODO(chadnorvell): Verify that this includes disabled extensions + extensions.map(async extId => { + const ext = available.find(ext => ext.id == extId); + + if (!(ext)) { + unavailableExtensions.push(extId); + } + }); + + return unavailableExtensions; +} + +/** + * If there are recommended extensions that are not installed or enabled in the + * current workspace, prompt the user to install them. This is "sticky" in the + * sense that it will keep bugging the user to enable those extensions until + * they enable them all, or until they explicitly cancel. + * @param recs - A list of extension IDs + */ +async function installRecommendedExtensions(recs: string[]): Promise<void> { + let unavailableRecs = getUnavailableExtensions(recs); + const totalNumUnavailableRecs = unavailableRecs.length; + let numUnavailableRecs = totalNumUnavailableRecs; + + const update = () => { + unavailableRecs = getUnavailableExtensions(recs); + numUnavailableRecs = unavailableRecs.length; + } + + const wait = async () => new Promise(resolve => setTimeout(resolve, 2500)); + + const progressIncrement = (num: number) => + 1 - (num / totalNumUnavailableRecs) * 100; + + // All recommendations are installed; we're done. + if (totalNumUnavailableRecs == 0) { + console.log( + 'User has all recommended extensions' + ); + + return; + } + + showExtensions(unavailableRecs); + + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + // TODO(chadnorvell): Make this look better + title: 'Install these extensions! This Pigweed project needs these recommended extensions to be installed.', + cancellable: true, + }, async (progress, token) => { + + while (numUnavailableRecs > 0) { + // TODO(chadnorvell): Wait for vscode.extensions.onDidChange + await wait(); + update(); + + progress.report({ + increment: progressIncrement(numUnavailableRecs), + }); + + if (numUnavailableRecs > 0) { + console.log( + `User lacks ${numUnavailableRecs} recommended extensions` + ); + + showExtensions(unavailableRecs); + } + + if (token.isCancellationRequested) { + console.log( + 'User cancelled recommended extensions check' + ); + + break; + } + } + + console.log('All recommended extensions are enabled'); + progress.report({ increment: 100 }); + }); +} + +/** + * Given a list of extensions, return the subset that are enabled. + * @param extensions - A list of extension IDs + * @returns A list of extension IDs + */ +function getEnabledExtensions(extensions: string[]): string[] { + let enabledExtensions: string[] = []; + const available = vscode.extensions.all; + + // TODO(chadnorvell): Verify that this excludes disabled extensions + extensions.map(async extId => { + const ext = available.find(ext => ext.id == extId); + + if (ext) { + enabledExtensions.push(extId); + } + }); + + return enabledExtensions; +} + +/** + * If there are unwanted extensions that are enabled in the current workspace, + * prompt the user to disable them. This is "sticky" in the sense that it will + * keep bugging the user to disable those extensions until they disable them + * all, or until they explicitly cancel. + * @param recs - A list of extension IDs + */ +async function disableUnwantedExtensions(unwanted: string[]) { + let enabledUnwanted = getEnabledExtensions(unwanted); + const totalNumEnabledUnwanted = enabledUnwanted.length; + let numEnabledUnwanted = totalNumEnabledUnwanted; + + const update = () => { + enabledUnwanted = getEnabledExtensions(unwanted); + numEnabledUnwanted = enabledUnwanted.length; + } + + const wait = async () => new Promise(resolve => setTimeout(resolve, 2500)); + + const progressIncrement = (num: number) => + 1 - (num / totalNumEnabledUnwanted) * 100; + + // All unwanted are disabled; we're done. + if (totalNumEnabledUnwanted == 0) { + console.log( + 'User has no unwanted extensions enabled' + ); + + return; + } + + showExtensions(enabledUnwanted); + + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + // TODO(chadnorvell): Make this look better + title: 'Disable these extensions! This Pigweed project needs these extensions to be disabled.', + cancellable: true, + }, async (progress, token) => { + + while (numEnabledUnwanted > 0) { + // TODO(chadnorvell): Wait for vscode.extensions.onDidChange + await wait(); + update(); + + progress.report({ + increment: progressIncrement(numEnabledUnwanted), + }); + + if (numEnabledUnwanted > 0) { + console.log( + `User has ${numEnabledUnwanted} unwanted extensions enabled` + ); + + showExtensions(enabledUnwanted); + } + + if (token.isCancellationRequested) { + console.log( + 'User cancelled unwanted extensions check' + ); + + break; + } + } + + console.log('All unwanted extensions are disabled'); + progress.report({ increment: 100 }); + }); +} + +async function checkExtensions(context: vscode.ExtensionContext) { + const extensions = await getExtensionsJson(); + + const num_recommendations = extensions.recommendations?.length ?? 0; + const num_unwanted = extensions.unwantedRecommendations?.length ?? 0; + + if (num_recommendations > 0) { + await installRecommendedExtensions(extensions.recommendations as string[]); + } + + if (num_unwanted > 0) { + await disableUnwantedExtensions(extensions.unwantedRecommendations as string[]); + } +} + +export function activate(context: vscode.ExtensionContext) { + let pwCheckExtensions = vscode.commands.registerCommand( + 'pigweed.check-extensions', + () => checkExtensions(context) + ); + + context.subscriptions.push(pwCheckExtensions); +} + +export function deactivate() {} diff --git a/pw_ide/vscode/tsconfig.json b/pw_ide/vscode/tsconfig.json new file mode 100644 index 000000000..f73dc0acc --- /dev/null +++ b/pw_ide/vscode/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": [ + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true + } +} |