import { commands, ExtensionContext, Position, Range, window, env } from 'vscode' import { Extension, renderExtension, EmulatorState } from './extension' import { LanguageServerAPI } from './language-server' import { createTerminal } from './terminal' import { COPY_ADDRESS, CREATE_NEW_ACCOUNT, ACTIVE_PREFIX, INACTIVE_PREFIX, ADD_NEW_PREFIX } from './strings' // Command identifiers for locally handled commands export const RESTART_SERVER = 'cadence.restartServer' export const START_EMULATOR = 'cadence.runEmulator' export const STOP_EMULATOR = 'cadence.stopEmulator' export const CREATE_ACCOUNT = 'cadence.createAccount' export const SWITCH_ACCOUNT = 'cadence.switchActiveAccount' // Command identifiers for commands running in CLI export const DEPLOY_CONTRACT = 'cadence.deployContract' export const EXECUTE_SCRIPT = 'cadence.executeScript' export const SEND_TRANSACTION = 'cadence.sendTransaction' // Command identifies for commands handled by the Language server export const CREATE_ACCOUNT_SERVER = 'cadence.server.flow.createAccount' export const CREATE_DEFAULT_ACCOUNTS_SERVER = 'cadence.server.flow.createDefaultAccounts' export const SWITCH_ACCOUNT_SERVER = 'cadence.server.flow.switchActiveAccount' export const CHANGE_EMULATOR_STATE = 'cadence.server.flow.changeEmulatorState' export const INIT_ACCOUNT_MANAGER = 'cadence.server.flow.initAccountManager' // Registers a command with VS Code so it can be invoked by the user. function registerCommand ( ctx: ExtensionContext, command: string, callback: (...args: any[]) => any ): void { 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): void { 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 (): Promise<Extension> => { await ext.api.client.stop() const activeAccount = ext.config.getActiveAccount() ext.api = new LanguageServerAPI(ext.ctx, ext.config, ext.emulatorState, activeAccount) return ext } // Starts the emulator in a terminal window. const startEmulator = (ext: Extension) => async (): Promise<EmulatorState> => { // Start the emulator with the service key we gave to the language server. const { configPath } = ext.config ext.setEmulatorState(EmulatorState.Starting) renderExtension(ext) ext.terminal.sendText( [ ext.config.flowCommand, `emulator`, `--config-path="${configPath}"`, `--verbose`, ].join(' ') ); ext.terminal.show() try { await ext.api.initAccountManager() const accounts = await ext.api.createDefaultAccounts(ext.config.numAccounts) for (const account of accounts) { ext.config.addAccount(account) } await setActiveAccount(ext, 0) ext.setEmulatorState(EmulatorState.Started) renderExtension(ext) } catch (err) { ext.setEmulatorState(EmulatorState.Stopped) renderExtension(ext) } return ext.getEmulatorState() } // Stops emulator, exits the terminal, and removes all config/db files. const stopEmulator = (ext: Extension) => async (): Promise<EmulatorState> => { ext.terminal.dispose() ext.terminal = createTerminal(ext.ctx) ext.setEmulatorState(EmulatorState.Stopped) // 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.emulatorState, null) return ext.getEmulatorState() } // 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 () => { return await createNewAccount(ext) } // Switches the active account to the option selected by the user. The selection // is propagated to the Language Server. const switchActiveAccount = (ext: Extension) => async () => { // 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 prefix: string = account.index === ext.config.activeAccount ? ACTIVE_PREFIX : INACTIVE_PREFIX const label = `${prefix} ${account.fullName()}` return { label: label, target: account.index } }) accountOptions.push({ label: `${ADD_NEW_PREFIX} ${CREATE_NEW_ACCOUNT}`, target: accountOptions.length }) window.showQuickPick(accountOptions).then(async (selected) => { // `selected` is undefined if the QuickPick is dismissed, and the // string value of the selected option otherwise. if (selected === undefined) { return } if (selected.target === accountOptions.length - 1) { await createNewAccount(ext) return } await setActiveAccount(ext, selected.target) renderExtension(ext) }, () => {}) } const createNewAccount = async (ext: Extension): Promise<void> => { try { const account = await ext.api.createAccount() ext.config.addAccount(account) const lastIndex = ext.config.accounts.length - 1 await setActiveAccount(ext, lastIndex) renderExtension(ext) } catch (err) { // ref: is error handling necessary here? window.showErrorMessage(`Failed to create account: ${err.message as string}`) .then(() => {}, () => {}) } } const setActiveAccount = async (ext: Extension, activeIndex: number): Promise<void> => { const activeAccount = ext.config.getAccount(activeIndex) if (activeAccount == null) { window.showErrorMessage('Failed to switch account: account does not exist.') .then(() => {}, () => {}) return } try { await ext.api.switchActiveAccount(activeAccount) ext.config.setActiveAccount(activeIndex) window.showInformationMessage( `Switched to account ${activeAccount.fullName()}`, COPY_ADDRESS ).then((choice) => { if (choice === COPY_ADDRESS) { env.clipboard.writeText(`0x${activeAccount.address}`) .then(() => {}, () => {}) } }, () => {}) renderExtension(ext) } catch (err) { window.showErrorMessage(`Failed to switch account: ${err.message as string}`) .then(() => {}, () => {}) } } // This method will add and then remove a space on the last line to trick codelens to be updated export const refreshCodeLenses = (): void => { window.visibleTextEditors.forEach((editor) => { if (editor.document.lineCount !== 0) { 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') } }).then(() => {}, () => {}) }) }