aboutsummaryrefslogtreecommitdiff
path: root/pw_ide
diff options
context:
space:
mode:
authorChad Norvell <chadnorvell@google.com>2023-08-04 06:12:57 +0000
committerCQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-08-04 06:12:57 +0000
commit2278b06a38f49b752e8c560be2ce8caad389bd88 (patch)
tree15536e0105054b4d8a70defd4561c8771c007dd1 /pw_ide
parent3ffc5bd7fb215c23bc7a9c8fffc930034c5aeab7 (diff)
downloadpigweed-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.json30
-rw-r--r--pw_ide/vscode/.vscode/settings.json12
-rw-r--r--pw_ide/vscode/.vscode/tasks.json18
-rw-r--r--pw_ide/vscode/CHANGELOG.md9
-rw-r--r--pw_ide/vscode/README.md33
-rw-r--r--pw_ide/vscode/package.json45
-rw-r--r--pw_ide/vscode/pigweed-ide-0.0.1.vsixbin0 -> 19152 bytes
-rw-r--r--pw_ide/vscode/src/config.ts93
-rw-r--r--pw_ide/vscode/src/extension.ts238
-rw-r--r--pw_ide/vscode/tsconfig.json13
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
new file mode 100644
index 000000000..084e59aea
--- /dev/null
+++ b/pw_ide/vscode/pigweed-ide-0.0.1.vsix
Binary files differ
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
+ }
+}