From d93d9eafb48dfdc082e8a6e61ede2e04877daa7b Mon Sep 17 00:00:00 2001 From: 10thfloor Date: Wed, 10 Jun 2020 14:26:11 -0700 Subject: [PATCH 1/2] Add launch.json for extension development --- tools/vscode-extension/.vscode/launch.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tools/vscode-extension/.vscode/launch.json diff --git a/tools/vscode-extension/.vscode/launch.json b/tools/vscode-extension/.vscode/launch.json new file mode 100644 index 0000000000..8bc46cdceb --- /dev/null +++ b/tools/vscode-extension/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "extensionHost", + "request": "launch", + "name": "Launch Extension", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"] + } + ] +} From 83afda9773b0a37bef3aeeca74c481fa35a58312 Mon Sep 17 00:00:00 2001 From: 10thfloor Date: Thu, 18 Jun 2020 10:04:02 -0700 Subject: [PATCH 2/2] Update extension UI. --- tools/vscode-extension/README.md | 23 +- tools/vscode-extension/images/add.svg | 12 + tools/vscode-extension/images/flow.svg | 13 + .../out/test/extension.test.js | 21 -- .../out/test/extension.test.js.map | 1 - tools/vscode-extension/out/test/index.js | 23 -- tools/vscode-extension/out/test/index.js.map | 1 - tools/vscode-extension/package-lock.json | 2 +- tools/vscode-extension/package.json | 29 ++ tools/vscode-extension/src/accounts.ts | 78 ++++++ tools/vscode-extension/src/commands.ts | 263 ++++++++++-------- tools/vscode-extension/src/config.ts | 230 +++++++-------- .../src/explorer/accounts-data-provider.ts | 92 ++++++ tools/vscode-extension/src/explorer/index.ts | 23 ++ tools/vscode-extension/src/extension.ts | 95 ++++--- tools/vscode-extension/src/language-server.ts | 124 +++++---- tools/vscode-extension/src/status-bar.ts | 28 +- 17 files changed, 639 insertions(+), 419 deletions(-) create mode 100644 tools/vscode-extension/images/add.svg create mode 100644 tools/vscode-extension/images/flow.svg delete mode 100644 tools/vscode-extension/out/test/extension.test.js delete mode 100644 tools/vscode-extension/out/test/extension.test.js.map delete mode 100644 tools/vscode-extension/out/test/index.js delete mode 100644 tools/vscode-extension/out/test/index.js.map create mode 100644 tools/vscode-extension/src/accounts.ts create mode 100644 tools/vscode-extension/src/explorer/accounts-data-provider.ts create mode 100644 tools/vscode-extension/src/explorer/index.ts diff --git a/tools/vscode-extension/README.md b/tools/vscode-extension/README.md index 63c8e56ca2..eda7f87895 100644 --- a/tools/vscode-extension/README.md +++ b/tools/vscode-extension/README.md @@ -14,17 +14,20 @@ and have configured the [`code` command line interface](https://code.visualstudi ### Using the Flow CLI -The recommended way to install the latest released version is to use the Flow CLI. +The recommended way to install the latest released version is to use the Flow CLI. + ```shell script brew tap dapperlabs/homebrew && brew install flow-cli ``` Check that it's been installed correctly. + ```shell script flow version ``` Next, use the CLI to install the VS Code extension. + ```shell script flow cadence install-vscode-extension ``` @@ -33,49 +36,55 @@ Restart VS Code and the extension should be installed! ### Building -If you are building the extension from source, you need to build both the +If you are building the extension from source, you need to build both the extension itself and the Flow CLI (if you don't already have a version installed). -Unless you're developing the extension or need access to unreleased features, +Unless you're developing the extension or need access to unreleased features, you should use the Flow CLI option. It's much easier! #### VS Code Extension -Make sure you are in this `vscode-extension` directory. + +Make sure you are in this `vscode-extension` directory. If you haven't already, install dependencies. + ```shell script npm install ``` Next, build and package the extension. + ```shell script npm run package ``` -This will result in a `.vsix` file containing the packaged extension. +This will result in a `.vsix` file containing the packaged extension. Install the packaged extension. + ```shell script code --install-extension cadence-*.vsix ``` Restart VS Code and the extension should be installed! -#### FLow CLI +#### Flow CLI Make sure you are in the root directory. Build the Flow CLI. + ```shell script make cmd/flow/flow ``` Move the resulting binary (`cmd/flow/flow`) into your `$PATH`. For example: + ```shell script mv ./cmd/flow/flow /usr/local/bin/ ``` Restart your terminal and check to ensure it was installed correctly. + ```shell script flow version ``` - diff --git a/tools/vscode-extension/images/add.svg b/tools/vscode-extension/images/add.svg new file mode 100644 index 0000000000..8db7c82a9a --- /dev/null +++ b/tools/vscode-extension/images/add.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/tools/vscode-extension/images/flow.svg b/tools/vscode-extension/images/flow.svg new file mode 100644 index 0000000000..a41626adc8 --- /dev/null +++ b/tools/vscode-extension/images/flow.svg @@ -0,0 +1,13 @@ + + Монтажная область 64 + + + + + + + + + + \ No newline at end of file diff --git a/tools/vscode-extension/out/test/extension.test.js b/tools/vscode-extension/out/test/extension.test.js deleted file mode 100644 index 2d35c9d45f..0000000000 --- a/tools/vscode-extension/out/test/extension.test.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -Object.defineProperty(exports, "__esModule", { value: true }); -// The module 'assert' provides assertion methods from node -const assert = require("assert"); -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -// import * as vscode from 'vscode'; -// import * as myExtension from '../extension'; -// Defines a Mocha test suite to group tests of similar kind together -suite("Extension Tests", function () { - // Defines a Mocha unit test - test("Something 1", function () { - assert.equal(-1, [1, 2, 3].indexOf(5)); - assert.equal(-1, [1, 2, 3].indexOf(0)); - }); -}); -//# sourceMappingURL=extension.test.js.map \ No newline at end of file diff --git a/tools/vscode-extension/out/test/extension.test.js.map b/tools/vscode-extension/out/test/extension.test.js.map deleted file mode 100644 index b4c6ac5c9d..0000000000 --- a/tools/vscode-extension/out/test/extension.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"extension.test.js","sourceRoot":"","sources":["../../src/test/extension.test.ts"],"names":[],"mappings":";AAAA,EAAE;AACF,kEAAkE;AAClE,wEAAwE;AACxE,EAAE;;AAEF,2DAA2D;AAC3D,iCAAiC;AAEjC,0DAA0D;AAC1D,8CAA8C;AAC9C,oCAAoC;AACpC,+CAA+C;AAE/C,qEAAqE;AACrE,KAAK,CAAC,iBAAiB,EAAE;IAErB,4BAA4B;IAC5B,IAAI,CAAC,aAAa,EAAE;QAChB,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/tools/vscode-extension/out/test/index.js b/tools/vscode-extension/out/test/index.js deleted file mode 100644 index 7608a29f78..0000000000 --- a/tools/vscode-extension/out/test/index.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING -// -// This file is providing the test runner to use when running extension tests. -// By default the test runner in use is Mocha based. -// -// You can provide your own test runner if you want to override it by exporting -// a function run(testsRoot: string, clb: (error: Error, failures?: number) => void): void -// that the extension host can call to run the tests. The test runner is expected to use console.log -// to report the results back to the caller. When the tests are finished, return -// a possible error to the callback or null if none. -Object.defineProperty(exports, "__esModule", { value: true }); -const testRunner = require("vscode/lib/testrunner"); -// You can directly control Mocha options by configuring the test runner below -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options -// for more info -testRunner.configure({ - ui: 'tdd', - useColors: true // colored output from test results -}); -module.exports = testRunner; -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/tools/vscode-extension/out/test/index.js.map b/tools/vscode-extension/out/test/index.js.map deleted file mode 100644 index c4c218baf3..0000000000 --- a/tools/vscode-extension/out/test/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":";AAAA,EAAE;AACF,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,oDAAoD;AACpD,EAAE;AACF,+EAA+E;AAC/E,0FAA0F;AAC1F,oGAAoG;AACpG,gFAAgF;AAChF,oDAAoD;;AAEpD,oDAAoD;AAEpD,8EAA8E;AAC9E,qFAAqF;AACrF,gBAAgB;AAChB,UAAU,CAAC,SAAS,CAAC;IACjB,EAAE,EAAE,KAAK;IACT,SAAS,EAAE,IAAI,CAAC,mCAAmC;CACtD,CAAC,CAAC;AAEH,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC"} \ No newline at end of file diff --git a/tools/vscode-extension/package-lock.json b/tools/vscode-extension/package-lock.json index da6e2f60d5..78a021059c 100644 --- a/tools/vscode-extension/package-lock.json +++ b/tools/vscode-extension/package-lock.json @@ -1,6 +1,6 @@ { "name": "cadence", - "version": "0.1.0", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/vscode-extension/package.json b/tools/vscode-extension/package.json index 627e57cd95..ac3bd9d5f9 100644 --- a/tools/vscode-extension/package.json +++ b/tools/vscode-extension/package.json @@ -20,6 +20,27 @@ ], "main": "./out/extension.js", "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "flow-explorer", + "title": "Flow Explorer", + "icon": "images/flow.svg" + } + ] + }, + "views": { + "flow-explorer": [ + { + "id": "flowAccounts", + "name": "Flow Accounts" + }, + { + "id": "flowContracts", + "name": "Deployed Contract" + } + ] + }, "commands": [ { "command": "cadence.restartServer", @@ -47,6 +68,14 @@ "title": "Switch account" } ], + "menus": { + "view/title": [ + { + "command": "cadence.createAccount", + "when": "view == flowAccounts" + } + ] + }, "configuration": { "title": "Cadence", "properties": { diff --git a/tools/vscode-extension/src/accounts.ts b/tools/vscode-extension/src/accounts.ts new file mode 100644 index 0000000000..ccfb733908 --- /dev/null +++ b/tools/vscode-extension/src/accounts.ts @@ -0,0 +1,78 @@ +import { addAddressPrefix } from "./address"; +import { Extension } from "./extension"; +export const SERVICE_ADDR: string = "f8d6e0586b0a20c7"; + +export class AccountsService { + numAccounts: number; + // Set of created accounts for which we can submit transactions. + // Mapping from account address to account object. + list: Array; + // Index of the currently active account. + activeAccount: number; + + ext: Extension | undefined; + + constructor(numAccounts: number) { + this.numAccounts = numAccounts; + this.list = [new Account(0, SERVICE_ADDR)]; + this.activeAccount = 0; + this.ext = undefined; + } + + init(ext: Extension) { + this.ext = ext; + } + + addAccount(address: string) { + const index = this.list.length; + const account = new Account(index, address); + this.list.push(account); + this.ext && + this.ext.accountsTreeView.accountsTreeViewDataProvider.refresh(); + } + + setActiveAccount(index: number) { + this.activeAccount = index; + } + + getActiveAccount(): Account { + return this.list[this.activeAccount]; + } + + getAccount(index: number): Account | null { + if (index < 0 || index >= this.list.length) { + return null; + } + + return this.list[index]; + } + + // Resets account state + resetAccounts() { + this.list = [new Account(0, SERVICE_ADDR)]; + this.activeAccount = 0; + } +} + +export class Account { + index: number; + address: string; + + constructor(index: number, address: string) { + this.index = index; + this.address = address; + } + + name(): string { + return this.index === 0 ? "Service Account" : `Account ${this.index}`; + } + + fullName(): string { + return `${this.name()} (${addAddressPrefix(this.address)})`; + } +} + +export function createAccountsService(numAccounts: number) { + const accountsService = new AccountsService(numAccounts); + return accountsService; +} diff --git a/tools/vscode-extension/src/commands.ts b/tools/vscode-extension/src/commands.ts index 024b8f9d8d..85928e8cd5 100644 --- a/tools/vscode-extension/src/commands.ts +++ b/tools/vscode-extension/src/commands.ts @@ -1,8 +1,19 @@ -import {commands, ExtensionContext, Position, Range, window, workspace} from "vscode"; -import {Extension, renderExtension} from "./extension"; -import {LanguageServerAPI} from "./language-server"; -import {createTerminal} from "./terminal"; -import {removeAddressPrefix} from "./address"; +import { + commands, + ExtensionContext, + Position, + Range, + window, + workspace, +} from "vscode"; +import { Extension, renderExtension } from "./extension"; +import { LanguageServerAPI } from "./language-server"; +import { createTerminal } from "./terminal"; +import { removeAddressPrefix } from "./address"; +import { + FlowAccountTreeItem, + FlowAccountDetailTreeItem, +} from "./explorer/accounts-data-provider"; // Command identifiers for locally handled commands export const RESTART_SERVER = "cadence.restartServer"; @@ -13,148 +24,164 @@ export const SWITCH_ACCOUNT = "cadence.switchActiveAccount"; // Command identifies for commands handled by the Language server export const CREATE_ACCOUNT_SERVER = "cadence.server.createAccount"; -export const CREATE_DEFAULT_ACCOUNTS_SERVER = "cadence.server.createDefaultAccounts"; +export const CREATE_DEFAULT_ACCOUNTS_SERVER = + "cadence.server.createDefaultAccounts"; export const SWITCH_ACCOUNT_SERVER = "cadence.server.switchActiveAccount"; // Registers a command with VS Code so it can be invoked by the user. -function registerCommand(ctx: ExtensionContext, command: string, callback: (...args: any[]) => any) { - ctx.subscriptions.push(commands.registerCommand(command, callback)); +function registerCommand( + ctx: ExtensionContext, + command: string, + callback: (...args: any[]) => any +) { + ctx.subscriptions.push(commands.registerCommand(command, callback)); } // Registers all commands that are handled by the extension (as opposed to // those handled by the Language Server). export function registerCommands(ext: Extension) { - registerCommand(ext.ctx, RESTART_SERVER, restartServer(ext)); - registerCommand(ext.ctx, START_EMULATOR, startEmulator(ext)); - registerCommand(ext.ctx, STOP_EMULATOR, stopEmulator(ext)); - registerCommand(ext.ctx, CREATE_ACCOUNT, createAccount(ext)); - registerCommand(ext.ctx, SWITCH_ACCOUNT, switchActiveAccount(ext)); + registerCommand(ext.ctx, RESTART_SERVER, restartServer(ext)); + registerCommand(ext.ctx, START_EMULATOR, startEmulator(ext)); + registerCommand(ext.ctx, STOP_EMULATOR, stopEmulator(ext)); + registerCommand(ext.ctx, CREATE_ACCOUNT, createAccount(ext)); + registerCommand(ext.ctx, SWITCH_ACCOUNT, switchActiveAccount(ext)); } // Restarts the language server, updating the client in the extension object. const restartServer = (ext: Extension) => async () => { - await ext.api.client.stop(); - ext.api = new LanguageServerAPI(ext.ctx, ext.config); + await ext.api.client.stop(); + ext.api = new LanguageServerAPI(ext.ctx, ext.config); }; // Starts the emulator in a terminal window. const startEmulator = (ext: Extension) => async () => { - // Start the emulator with the service key we gave to the language server. - const {serverConfig} = ext.config - - ext.terminal.sendText( - [ - ext.config.flowCommand, - `emulator`, `start`, `--init`, `--verbose`, - `--service-priv-key`, serverConfig.servicePrivateKey, - `--service-sig-algo`, serverConfig.serviceKeySignatureAlgorithm, - `--service-hash-algo`, serverConfig.serviceKeyHashAlgorithm, - ].join(" ") - ); - ext.terminal.show(); - - // create default accounts after the emulator has started - // skip service account since it is already created - setTimeout(async () => { - try { - const accounts = await ext.api.createDefaultAccounts(ext.config.numAccounts - 1); - accounts.forEach(address => ext.config.addAccount(address)); - } catch (err) { - console.error("Failed to create default accounts", err); - window.showWarningMessage("Failed to create default accounts"); - } - }, 3000); + // Start the emulator with the service key we gave to the language server. + const { serverConfig } = ext.config; + + ext.terminal.sendText( + [ + ext.config.flowCommand, + `emulator`, + `start`, + `--init`, + `--verbose`, + `--service-priv-key`, + serverConfig.servicePrivateKey, + `--service-sig-algo`, + serverConfig.serviceKeySignatureAlgorithm, + `--service-hash-algo`, + serverConfig.serviceKeyHashAlgorithm, + ].join(" ") + ); + ext.terminal.show(); + + // create default accounts after the emulator has started + // skip service account since it is already created + setTimeout(async () => { + try { + const accounts = await ext.api.createDefaultAccounts( + ext.config.accounts.numAccounts - 1 + ); + accounts.forEach((address) => ext.config.accounts.addAccount(address)); + } catch (err) { + console.error("Failed to create default accounts", err); + window.showWarningMessage("Failed to create default accounts"); + } + }, 3000); }; // Stops emulator, exits the terminal, and removes all config/db files. const stopEmulator = (ext: Extension) => async () => { - ext.terminal.dispose(); - ext.terminal = createTerminal(ext.ctx); - - // Clear accounts and restart language server to ensure account - // state is in sync. - ext.config.resetAccounts(); - renderExtension(ext); - await ext.api.client.stop(); - ext.api = new LanguageServerAPI(ext.ctx, ext.config); + ext.terminal.dispose(); + ext.terminal = createTerminal(ext.ctx); + + // Clear accounts and restart language server to ensure account + // state is in sync. + ext.config.accounts.resetAccounts(); + renderExtension(ext); + await ext.api.client.stop(); + ext.api = new LanguageServerAPI(ext.ctx, ext.config); }; // Creates a new account by requesting that the Language Server submit // a "create account" transaction from the currently active account. const createAccount = (ext: Extension) => async () => { - try { - const addr = await ext.api.createAccount(); - ext.config.addAccount(addr); - } catch (err) { - window.showErrorMessage("Failed to create account: " + err); - return; - } + try { + const addr = await ext.api.createAccount(); + ext.config.accounts.addAccount(addr); + } catch (err) { + window.showErrorMessage("Failed to create account: " + err); + return; + } }; // Switches the active account to the option selected by the user. The selection // is propagated to the Language Server. const switchActiveAccount = (ext: Extension) => async () => { - // Suffix to indicate which account is active - const activeSuffix = "(active)"; - // Create the options (mark the active account with an 'active' prefix) - const accountOptions = Object - .values(ext.config.accounts) - // Mark the active account with a `*` in the dialog - .map((account) => { - const suffix: String = account.index === ext.config.activeAccount ? ` ${activeSuffix}` : ""; - const label = `${account.fullName()}${suffix}`; - - return { - label: label, - target: account.index - } - }) - - window.showQuickPick(accountOptions) - .then(selected => { - // `selected` is undefined if the QuickPick is dismissed, and the - // string value of the selected option otherwise. - if (selected === undefined) { - return; - } - - const activeIndex = selected.target; - const activeAccount = ext.config.getAccount(activeIndex); - - if (!activeAccount) { - console.error('Switched to invalid account'); - return; - } - - try { - ext.api.switchActiveAccount(removeAddressPrefix(activeAccount.address)); - window.visibleTextEditors.forEach(editor => { - if (!editor.document.lineCount) { - return; - } - // NOTE: We add a space to the end of the last line to force - // Codelens to refresh. - const lineCount = editor.document.lineCount; - const lastLine = editor.document.lineAt(lineCount-1); - editor.edit(edit => { - if (lastLine.isEmptyOrWhitespace) { - edit.insert(new Position(lineCount-1, 0), ' '); - edit.delete(new Range(lineCount-1, 0, lineCount-1, 1000)); - } else { - edit.insert(new Position(lineCount-1, 1000), '\n'); - } - }); - }); - } catch (err) { - window.showWarningMessage("Failed to switch active account"); - console.error(err); - return; - } - - ext.config.setActiveAccount(activeIndex) - - window.showInformationMessage(`Switched to account ${activeAccount.fullName()}`); - - renderExtension(ext); + // Suffix to indicate which account is active + const activeSuffix = "(active)"; + // Create the options (mark the active account with an 'active' prefix) + const accountOptions = Object.values(ext.config.accounts.list) + // Mark the active account with a `*` in the dialog + .map((account) => { + const suffix: String = + account.index === ext.config.accounts.activeAccount + ? ` ${activeSuffix}` + : ""; + const label = `${account.fullName()}${suffix}`; + + return { + label: label, + target: account.index, + }; + }); + + window.showQuickPick(accountOptions).then((selected) => { + // `selected` is undefined if the QuickPick is dismissed, and the + // string value of the selected option otherwise. + if (selected === undefined) { + return; + } + + const activeIndex = selected.target; + const activeAccount = ext.config.accounts.getAccount(activeIndex); + + if (!activeAccount) { + console.error("Switched to invalid account"); + return; + } + + try { + ext.api.switchActiveAccount(removeAddressPrefix(activeAccount.address)); + window.visibleTextEditors.forEach((editor) => { + if (!editor.document.lineCount) { + return; + } + // NOTE: We add a space to the end of the last line to force + // Codelens to refresh. + const lineCount = editor.document.lineCount; + const lastLine = editor.document.lineAt(lineCount - 1); + editor.edit((edit) => { + if (lastLine.isEmptyOrWhitespace) { + edit.insert(new Position(lineCount - 1, 0), " "); + edit.delete(new Range(lineCount - 1, 0, lineCount - 1, 1000)); + } else { + edit.insert(new Position(lineCount - 1, 1000), "\n"); + } }); + }); + } catch (err) { + window.showWarningMessage("Failed to switch active account"); + console.error(err); + return; + } + + ext.config.accounts.setActiveAccount(activeIndex); + + window.showInformationMessage( + `Switched to account ${activeAccount.fullName()}` + ); + + renderExtension(ext); + }); }; diff --git a/tools/vscode-extension/src/config.ts b/tools/vscode-extension/src/config.ts index b71575dd6e..98cc6f2764 100644 --- a/tools/vscode-extension/src/config.ts +++ b/tools/vscode-extension/src/config.ts @@ -1,7 +1,5 @@ -import {commands, window, workspace} from "vscode"; -import { addAddressPrefix } from "./address"; - -export const SERVICE_ADDR: string = "f8d6e0586b0a20c7"; +import { commands, window, workspace } from "vscode"; +import { AccountsService, createAccountsService } from "./accounts"; const CONFIG_FLOW_COMMAND = "flowCommand"; const CONFIG_SERVICE_PRIVATE_KEY = "servicePrivateKey"; @@ -11,147 +9,117 @@ const CONFIG_EMULATOR_ADDRESS = "emulatorAddress"; const CONFIG_NUM_ACCOUNTS = "numAccounts"; // An account that can be used to submit transactions. -export class Account { - index: number - address: string - - constructor(index: number, address: string) { - this.index = index; - this.address = address; - } - - name(): string { - return this.index === 0 ? "Service Account" : `Account ${this.index}`; - } - - fullName(): string { - return `${this.name()} (${addAddressPrefix(this.address)})`; - } -}; // The subset of extension configuration used by the language server. type ServerConfig = { - servicePrivateKey: string - serviceKeySignatureAlgorithm: string - serviceKeyHashAlgorithm: string - emulatorAddress: string + servicePrivateKey: string; + serviceKeySignatureAlgorithm: string; + serviceKeyHashAlgorithm: string; + emulatorAddress: string; }; // The configuration used by the extension. export class Config { - // The name of the flow CLI executable - flowCommand: string; - serverConfig: ServerConfig; - numAccounts: number; - // Set of created accounts for which we can submit transactions. - // Mapping from account address to account object. - accounts: Array; - // Index of the currently active account. - activeAccount: number; - - constructor(flowCommand: string, numAccounts: number, serverConfig: ServerConfig) { - this.flowCommand = flowCommand; - this.numAccounts = numAccounts; - this.serverConfig = serverConfig; - this.accounts = [new Account(0, SERVICE_ADDR)]; - this.activeAccount = 0; - } - - addAccount(address: string) { - const index = this.accounts.length; - this.accounts.push(new Account(index, address)); - } - - setActiveAccount(index: number) { - this.activeAccount = index; - } - - getActiveAccount(): Account { - return this.accounts[this.activeAccount]; - } - - getAccount(index: number): Account|null { - if (index < 0 || index >= this.accounts.length) { - return null; - } - - return this.accounts[index] - } - - // Resets account state - resetAccounts() { - this.accounts = [new Account(0, SERVICE_ADDR)]; - this.activeAccount = 0; - } + // The name of the flow CLI executable + flowCommand: string; + serverConfig: ServerConfig; + numAccounts: number; + accounts: AccountsService; + + constructor( + flowCommand: string, + numAccounts: number, + serverConfig: ServerConfig + ) { + this.flowCommand = flowCommand; + this.serverConfig = serverConfig; + this.numAccounts = numAccounts; + this.accounts = createAccountsService(numAccounts); + } } // Retrieves config from the workspace. export function getConfig(): Config { - const cadenceConfig = workspace - .getConfiguration("cadence"); - - const flowCommand: string | undefined = cadenceConfig.get(CONFIG_FLOW_COMMAND); - if (!flowCommand) { - throw new Error(`Missing ${CONFIG_FLOW_COMMAND} config`); - } - - const servicePrivateKey: string | undefined = cadenceConfig.get(CONFIG_SERVICE_PRIVATE_KEY); - if (!servicePrivateKey) { - throw new Error(`Missing ${CONFIG_SERVICE_PRIVATE_KEY} config`); - } - - const serviceKeySignatureAlgorithm: string | undefined = cadenceConfig.get(CONFIG_SERVICE_KEY_SIGNATURE_ALGORITHM); - if (!serviceKeySignatureAlgorithm) { - throw new Error(`Missing ${CONFIG_SERVICE_KEY_SIGNATURE_ALGORITHM} config`); - } - - const serviceKeyHashAlgorithm: string | undefined = cadenceConfig.get(CONFIG_SERVICE_KEY_HASH_ALGORITHM); - if (!serviceKeyHashAlgorithm) { - throw new Error(`Missing ${CONFIG_SERVICE_KEY_HASH_ALGORITHM} config`); - } - - const emulatorAddress: string | undefined = cadenceConfig.get(CONFIG_EMULATOR_ADDRESS); - if (!emulatorAddress) { - throw new Error(`Missing ${CONFIG_EMULATOR_ADDRESS} config`); - } - - const numAccounts: number | undefined = cadenceConfig.get(CONFIG_NUM_ACCOUNTS); - if (!numAccounts) { - throw new Error(`Missing ${CONFIG_NUM_ACCOUNTS} config`); - } - - const serverConfig: ServerConfig = { - servicePrivateKey, - serviceKeySignatureAlgorithm, - serviceKeyHashAlgorithm, - emulatorAddress - }; - - return new Config(flowCommand, numAccounts, serverConfig); + const cadenceConfig = workspace.getConfiguration("cadence"); + + const flowCommand: string | undefined = cadenceConfig.get( + CONFIG_FLOW_COMMAND + ); + if (!flowCommand) { + throw new Error(`Missing ${CONFIG_FLOW_COMMAND} config`); + } + + const servicePrivateKey: string | undefined = cadenceConfig.get( + CONFIG_SERVICE_PRIVATE_KEY + ); + if (!servicePrivateKey) { + throw new Error(`Missing ${CONFIG_SERVICE_PRIVATE_KEY} config`); + } + + const serviceKeySignatureAlgorithm: string | undefined = cadenceConfig.get( + CONFIG_SERVICE_KEY_SIGNATURE_ALGORITHM + ); + if (!serviceKeySignatureAlgorithm) { + throw new Error(`Missing ${CONFIG_SERVICE_KEY_SIGNATURE_ALGORITHM} config`); + } + + const serviceKeyHashAlgorithm: string | undefined = cadenceConfig.get( + CONFIG_SERVICE_KEY_HASH_ALGORITHM + ); + if (!serviceKeyHashAlgorithm) { + throw new Error(`Missing ${CONFIG_SERVICE_KEY_HASH_ALGORITHM} config`); + } + + const emulatorAddress: string | undefined = cadenceConfig.get( + CONFIG_EMULATOR_ADDRESS + ); + if (!emulatorAddress) { + throw new Error(`Missing ${CONFIG_EMULATOR_ADDRESS} config`); + } + + const numAccounts: number | undefined = cadenceConfig.get( + CONFIG_NUM_ACCOUNTS + ); + if (!numAccounts) { + throw new Error(`Missing ${CONFIG_NUM_ACCOUNTS} config`); + } + + const serverConfig: ServerConfig = { + servicePrivateKey, + serviceKeySignatureAlgorithm, + serviceKeyHashAlgorithm, + emulatorAddress, + }; + + return new Config(flowCommand, numAccounts, serverConfig); } // Adds an event handler that prompts the user to reload whenever the config // changes. export function handleConfigChanges() { - workspace.onDidChangeConfiguration(e => { - // TODO: do something smarter for account/emulator config (re-send to server) - const promptRestartKeys = ["languageServerPath", "accountKey", "accountAddress", "emulatorAddress"]; - const shouldPromptRestart = promptRestartKeys.some(key => - e.affectsConfiguration(`cadence.${key}`) - ); - if (shouldPromptRestart) { - window - .showInformationMessage( - "Server launch configuration change detected. Reload the window for changes to take effect", - "Reload Window", - "Not now" - ) - .then(choice => { - if (choice === "Reload Window") { - commands.executeCommand("workbench.action.reloadWindow"); - } - }); - } - }); + workspace.onDidChangeConfiguration((e) => { + // TODO: do something smarter for account/emulator config (re-send to server) + const promptRestartKeys = [ + "languageServerPath", + "accountKey", + "accountAddress", + "emulatorAddress", + ]; + const shouldPromptRestart = promptRestartKeys.some((key) => + e.affectsConfiguration(`cadence.${key}`) + ); + if (shouldPromptRestart) { + window + .showInformationMessage( + "Server launch configuration change detected. Reload the window for changes to take effect", + "Reload Window", + "Not now" + ) + .then((choice) => { + if (choice === "Reload Window") { + commands.executeCommand("workbench.action.reloadWindow"); + } + }); + } + }); } - diff --git a/tools/vscode-extension/src/explorer/accounts-data-provider.ts b/tools/vscode-extension/src/explorer/accounts-data-provider.ts new file mode 100644 index 0000000000..32da0c8925 --- /dev/null +++ b/tools/vscode-extension/src/explorer/accounts-data-provider.ts @@ -0,0 +1,92 @@ +import { + TreeItem, + TreeItemCollapsibleState, + TreeDataProvider, + ProviderResult, + ExtensionContext, + Event, + EventEmitter, +} from "vscode"; + +import { Config } from "../config"; + +export class FlowAccountTreeItem extends TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: TreeItemCollapsibleState, + public readonly accountAddress: string, + public readonly isActive: string + ) { + super(label, collapsibleState); + } + + get tooltip(): string { + return `${this.label}`; + } + + iconPath = { + light: "", + dark: "", + }; +} + +export class FlowAccountDetailTreeItem extends TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: TreeItemCollapsibleState, + public readonly isActive: boolean + ) { + super(label, collapsibleState); + } + + get tooltip(): string { + return `${this.label}`; + } + + get description(): string { + return `${this.label} ${this.isActive ? "(active)" : ""}`; + } + + iconPath = { + light: "", + dark: "", + }; +} + +export class FlowAccountDetailTreeDataProvider + implements TreeDataProvider { + config: Config; + constructor(ctx: ExtensionContext, config: Config) { + this.config = config; + } + + private getUpdatedAccountData(): FlowAccountTreeItem[] { + return this.config.accounts.list.map((acct) => { + const isServiceAccount = acct.index === 0; + const label = isServiceAccount + ? `${acct.address} (Service account)` + : acct.address; + return new FlowAccountTreeItem(label, 0, acct.address, "active"); + }); + } + + getTreeItem(element: TreeItem) { + return element; + } + + getChildren(element?: TreeItem): ProviderResult { + if (element === undefined) return this.getUpdatedAccountData(); + return undefined; + } + + private _onDidChangeTreeData: EventEmitter< + FlowAccountTreeItem | undefined + > = new EventEmitter(); + + readonly onDidChangeTreeData: Event = this + ._onDidChangeTreeData.event; + + refresh(): void { + this._onDidChangeTreeData.fire(); + } +} diff --git a/tools/vscode-extension/src/explorer/index.ts b/tools/vscode-extension/src/explorer/index.ts new file mode 100644 index 0000000000..c472d0c6f6 --- /dev/null +++ b/tools/vscode-extension/src/explorer/index.ts @@ -0,0 +1,23 @@ +import { ExtensionContext, window, TreeItem, TreeView } from "vscode"; +import { FlowAccountDetailTreeDataProvider } from "./accounts-data-provider"; +import { Config } from "../config"; + +export interface AccountsTreeView { + accountsTreeView: TreeView; + accountsTreeViewDataProvider: FlowAccountDetailTreeDataProvider; +} + +export function createAccountsTreeView( + ctx: ExtensionContext, + config: Config +): AccountsTreeView { + const accountsTreeViewDataProvider = new FlowAccountDetailTreeDataProvider( + ctx, + config + ); + const accountsTreeView = window.createTreeView("flowAccounts", { + treeDataProvider: accountsTreeViewDataProvider, + }); + + return { accountsTreeView, accountsTreeViewDataProvider }; +} diff --git a/tools/vscode-extension/src/extension.ts b/tools/vscode-extension/src/extension.ts index 767faec5bc..9333540ba7 100644 --- a/tools/vscode-extension/src/extension.ts +++ b/tools/vscode-extension/src/extension.ts @@ -1,57 +1,68 @@ +import { ExtensionContext, window, Terminal, StatusBarItem } from "vscode"; +import { getConfig, handleConfigChanges, Config } from "./config"; +import { LanguageServerAPI } from "./language-server"; +import { registerCommands } from "./commands"; +import { createTerminal } from "./terminal"; + +import { createAccountsTreeView, AccountsTreeView } from "./explorer"; + import { - ExtensionContext, - window, - Terminal, - StatusBarItem, -} from "vscode"; -import {getConfig, handleConfigChanges, Config} from "./config"; -import {LanguageServerAPI} from "./language-server"; -import {registerCommands} from "./commands"; -import {createTerminal} from "./terminal"; -import {createActiveAccountStatusBarItem, updateActiveAccountStatusBarItem} from "./status-bar"; + createActiveAccountStatusBarItem, + updateActiveAccountStatusBarItem, +} from "./status-bar"; +import { AccountsService } from "./accounts"; // The container for all data relevant to the extension. export type Extension = { - config: Config - ctx: ExtensionContext - api: LanguageServerAPI - terminal: Terminal - activeAccountStatusBarItem: StatusBarItem + config: Config; + ctx: ExtensionContext; + api: LanguageServerAPI; + terminal: Terminal; + activeAccountStatusBarItem: StatusBarItem; + accountsTreeView: AccountsTreeView; }; // Called when the extension starts up. Reads config, starts the language // server, and registers command handlers. export function activate(ctx: ExtensionContext) { - let config: Config; - let terminal: Terminal; - let activeAccountStatusBarItem: StatusBarItem; - let api: LanguageServerAPI; - - try { - config = getConfig(); - terminal = createTerminal(ctx); - api = new LanguageServerAPI(ctx, config); - activeAccountStatusBarItem = createActiveAccountStatusBarItem(); - } catch (err) { - window.showErrorMessage("Failed to activate extension: ", err); - return; - } - handleConfigChanges(); - - const ext: Extension = { - config: config, - ctx: ctx, - api: api, - terminal: terminal, - activeAccountStatusBarItem: activeAccountStatusBarItem, - }; - - registerCommands(ext); - renderExtension(ext); + let config: Config; + let terminal: Terminal; + let activeAccountStatusBarItem: StatusBarItem; + let api: LanguageServerAPI; + let accountsTreeView: AccountsTreeView; + + try { + config = getConfig(); + terminal = createTerminal(ctx); + api = new LanguageServerAPI(ctx, config); + + activeAccountStatusBarItem = createActiveAccountStatusBarItem(); + accountsTreeView = createAccountsTreeView(ctx, config); + } catch (err) { + window.showErrorMessage("Failed to activate extension: ", err); + return; + } + handleConfigChanges(); + + const ext: Extension = { + config: config, + ctx: ctx, + api: api, + terminal: terminal, + activeAccountStatusBarItem: activeAccountStatusBarItem, + accountsTreeView: accountsTreeView, + }; + + config.accounts.init(ext); + registerCommands(ext); + renderExtension(ext); } export function deactivate() {} export function renderExtension(ext: Extension) { - updateActiveAccountStatusBarItem(ext.activeAccountStatusBarItem, ext.config.getActiveAccount()); + updateActiveAccountStatusBarItem( + ext.activeAccountStatusBarItem, + ext.config.accounts.getActiveAccount() + ); } diff --git a/tools/vscode-extension/src/language-server.ts b/tools/vscode-extension/src/language-server.ts index f1240c15de..c7657477a6 100644 --- a/tools/vscode-extension/src/language-server.ts +++ b/tools/vscode-extension/src/language-server.ts @@ -1,73 +1,75 @@ -import {LanguageClient} from "vscode-languageclient"; -import {ExtensionContext, window} from "vscode"; -import {Config} from "./config"; -import {CREATE_ACCOUNT_SERVER, CREATE_DEFAULT_ACCOUNTS_SERVER, SWITCH_ACCOUNT_SERVER} from "./commands"; +import { LanguageClient } from "vscode-languageclient"; +import { ExtensionContext, window } from "vscode"; +import { Config } from "./config"; +import { + CREATE_ACCOUNT_SERVER, + CREATE_DEFAULT_ACCOUNTS_SERVER, + SWITCH_ACCOUNT_SERVER, +} from "./commands"; // The args to pass to the Flow CLI to start the language server. const START_LANGUAGE_SERVER_ARGS = ["cadence", "language-server"]; export class LanguageServerAPI { - client: LanguageClient; + client: LanguageClient; - constructor(ctx: ExtensionContext, config: Config) { - this.client = new LanguageClient( - "cadence", - "Cadence", - { - command: config.flowCommand, - args: START_LANGUAGE_SERVER_ARGS, - }, - { - documentSelector: [{ scheme: "file", language: "cadence" }], - synchronize: { - configurationSection: "cadence" - }, - initializationOptions: config.serverConfig, - } - ); + constructor(ctx: ExtensionContext, config: Config) { + this.client = new LanguageClient( + "cadence", + "Cadence", + { + command: config.flowCommand, + args: START_LANGUAGE_SERVER_ARGS, + }, + { + documentSelector: [{ scheme: "file", language: "cadence" }], + synchronize: { + configurationSection: "cadence", + }, + initializationOptions: config.serverConfig, + } + ); - this.client - .onReady() - .then(() => { - return window.showInformationMessage("Cadence language server started"); - }) - .catch(err => { - return window.showErrorMessage( - `Cadence language server failed to start: ${err}` - ); - }); + this.client + .onReady() + .then(() => { + return window.showInformationMessage("Cadence language server started"); + }) + .catch((err) => { + return window.showErrorMessage( + `Cadence language server failed to start: ${err}` + ); + }); - const clientDisposable = this.client.start(); - ctx.subscriptions.push(clientDisposable); - } + const clientDisposable = this.client.start(); + ctx.subscriptions.push(clientDisposable); + } - // Sends a request to switch the currently active account. - async switchActiveAccount(accountAddr: string) { - return this.client.sendRequest("workspace/executeCommand", { - command: SWITCH_ACCOUNT_SERVER, - arguments: [ - accountAddr, - ], - }); - } + // Sends a request to switch the currently active account. + async switchActiveAccount(accountAddr: string) { + return this.client.sendRequest("workspace/executeCommand", { + command: SWITCH_ACCOUNT_SERVER, + arguments: [accountAddr], + }); + } - // Sends a request to create a new account. Returns the address of the new - // account, if it was created successfully. - async createAccount(): Promise { - let res = await this.client.sendRequest("workspace/executeCommand", { - command: CREATE_ACCOUNT_SERVER, - arguments: [], - }); - return res as string; - } + // Sends a request to create a new account. Returns the address of the new + // account, if it was created successfully. + async createAccount(): Promise { + let res = await this.client.sendRequest("workspace/executeCommand", { + command: CREATE_ACCOUNT_SERVER, + arguments: [], + }); + return res as string; + } - // Sends a request to create a set of default accounts. Returns the addresses of the new - // accounts, if they were created successfully. - async createDefaultAccounts(count: number): Promise> { - let res = await this.client.sendRequest("workspace/executeCommand", { - command: CREATE_DEFAULT_ACCOUNTS_SERVER, - arguments: [count], - }); - return res as Array; - } + // Sends a request to create a set of default accounts. Returns the addresses of the new + // accounts, if they were created successfully. + async createDefaultAccounts(count: number): Promise> { + let res = await this.client.sendRequest("workspace/executeCommand", { + command: CREATE_DEFAULT_ACCOUNTS_SERVER, + arguments: [count], + }); + return res as Array; + } } diff --git a/tools/vscode-extension/src/status-bar.ts b/tools/vscode-extension/src/status-bar.ts index a2a2365bda..e70ac34650 100644 --- a/tools/vscode-extension/src/status-bar.ts +++ b/tools/vscode-extension/src/status-bar.ts @@ -1,18 +1,20 @@ -import { - window, - StatusBarItem, - StatusBarAlignment, -} from "vscode"; -import {Account} from "./config"; -import {SWITCH_ACCOUNT} from "./commands"; +import { window, StatusBarItem, StatusBarAlignment } from "vscode"; +import { Account } from "./accounts"; +import { SWITCH_ACCOUNT } from "./commands"; export function createActiveAccountStatusBarItem(): StatusBarItem { - const statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 100); - statusBarItem.command = SWITCH_ACCOUNT; - return statusBarItem + const statusBarItem = window.createStatusBarItem( + StatusBarAlignment.Left, + 100 + ); + statusBarItem.command = SWITCH_ACCOUNT; + return statusBarItem; } -export function updateActiveAccountStatusBarItem(statusBarItem: StatusBarItem, activeAccount: Account): void { - statusBarItem.text = `$(key) Active account: ${activeAccount.fullName()}` - statusBarItem.show() +export function updateActiveAccountStatusBarItem( + statusBarItem: StatusBarItem, + activeAccount: Account +): void { + statusBarItem.text = `$(key) Active account: ${activeAccount.fullName()}`; + statusBarItem.show(); }