diff --git a/extension/src/extension.ts b/extension/src/extension.ts index d0c3b46f..cf48d280 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -12,7 +12,7 @@ import { TestProvider } from './test-provider/test-provider' import { StorageProvider } from './storage/storage-provider' import * as path from 'path' import { NotificationProvider } from './ui/notification-provider' -import { CliSelectionProvider } from './ui/cli-selection-provider' +import { CliSelectionProvider } from './flow-cli/cli-selection-provider' // The container for all data relevant to the extension. export class Extension { @@ -32,6 +32,7 @@ export class Extension { #commands: CommandController #testProvider: TestProvider #schemaProvider: JSONSchemaProvider + #cliSelectionProvider: CliSelectionProvider private constructor (settings: Settings, ctx: ExtensionContext) { this.ctx = ctx @@ -47,7 +48,7 @@ export class Extension { const cliProvider = new CliProvider(settings) // Register CliSelectionProvider - const cliSelectionProvider = new CliSelectionProvider(cliProvider) + this.#cliSelectionProvider = new CliSelectionProvider(cliProvider) // Register JSON schema provider this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider) @@ -84,7 +85,8 @@ export class Extension { // Called on exit async deactivate (): Promise { await this.languageServer.deactivate() - this.#testProvider?.dispose() - this.#schemaProvider?.dispose() + this.#testProvider.dispose() + this.#schemaProvider.dispose() + this.#cliSelectionProvider.dispose() } } diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 93d179e6..66aa9424 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -9,12 +9,12 @@ const KNOWN_BINS = ['flow', 'flow-c1'] const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g -export type CliBinary = { +export interface CliBinary { name: string version: semver.SemVer } -type AvailableBinariesCache = { +interface AvailableBinariesCache { [key: string]: StateCache } @@ -33,27 +33,28 @@ export class CliProvider { this.#selectedBinaryName.next(flowCommand) }) - this.#availableBinaries = KNOWN_BINS.reduce((acc, bin) => { + this.#availableBinaries = KNOWN_BINS.reduce((acc, bin) => { acc[bin] = new StateCache(async () => await this.#fetchBinaryInformation(bin)) acc[bin].subscribe(() => { this.#availableBinaries$.invalidate() }) return acc - }, {} as AvailableBinariesCache) + }, {}) this.#availableBinaries$ = new StateCache(async () => { - return this.getAvailableBinaries() + return await this.getAvailableBinaries() }) this.#currentBinary$ = new StateCache(async () => { const name: string = this.#selectedBinaryName.getValue() - return this.#availableBinaries[name].getValue() + return await this.#availableBinaries[name].getValue() }) // Subscribe to changes in the selected binary to update the caches this.#selectedBinaryName.pipe(distinctUntilChanged(), startWith(null), pairwise()).subscribe(([prev, curr]) => { // Swap out the cache for the selected binary if (prev != null && !KNOWN_BINS.includes(prev)) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.#availableBinaries[prev] } if (curr != null && !KNOWN_BINS.includes(curr)) { @@ -109,7 +110,6 @@ export class CliProvider { }) } - async getAvailableBinaries (): Promise { const bins: CliBinary[] = [] for (const name in this.#availableBinaries) { @@ -126,16 +126,16 @@ export class CliProvider { } async getCurrentBinary (): Promise { - return this.#currentBinary$.getValue() + return await this.#currentBinary$.getValue() } - setCurrentBinary (name: string): void { - this.#settings.updateSettings({ flowCommand: name }) + async setCurrentBinary (name: string): Promise { + await this.#settings.updateSettings({ flowCommand: name }) } } export function isCadenceV1Cli (version: semver.SemVer): boolean { - return CADENCE_V1_CLI_REGEX.test(version.raw) + return CADENCE_V1_CLI_REGEX.test(version.raw) } export function parseFlowCliVersion (buffer: Buffer | string): string { diff --git a/extension/src/flow-cli/cli-selection-provider.ts b/extension/src/flow-cli/cli-selection-provider.ts new file mode 100644 index 00000000..beff9022 --- /dev/null +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -0,0 +1,145 @@ +import { zip } from 'rxjs' +import { CliBinary, CliProvider } from './cli-provider' +import { SemVer } from 'semver' +import * as vscode from 'vscode' + +const TOGGLE_CADENCE_VERSION_COMMAND = 'cadence.changeCadenceVersion' +const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g +const GET_BINARY_LABEL = (version: SemVer): string => `Flow CLI v${version.format()}` + +export class CliSelectionProvider implements vscode.Disposable { + #statusBarItem: vscode.StatusBarItem | undefined + #cliProvider: CliProvider + #showSelector: boolean = false + #versionSelector: vscode.QuickPick | undefined + #disposables: vscode.Disposable[] = [] + + constructor (cliProvider: CliProvider) { + this.#cliProvider = cliProvider + + // Register the command to toggle the version + vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, async () => await this.#toggleSelector(true)) + + // Register UI components + zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => { + void this.#refreshSelector() + }) + this.#cliProvider.currentBinary$.subscribe((binary) => { + if (binary === null) return + this.#statusBarItem?.dispose() + this.#statusBarItem = this.#createStatusBarItem(binary?.version) + this.#statusBarItem.show() + }) + } + + #createStatusBarItem (version: SemVer): vscode.StatusBarItem { + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) + statusBarItem.command = TOGGLE_CADENCE_VERSION_COMMAND + statusBarItem.color = new vscode.ThemeColor('statusBar.foreground') + statusBarItem.tooltip = 'Click to change the Flow CLI version' + + // Update the status bar text when the version changes + statusBarItem.text = GET_BINARY_LABEL(version) + + return statusBarItem + } + + #createVersionSelector (currentBinary: CliBinary | null, availableBinaries: CliBinary[]): vscode.QuickPick { + const versionSelector = vscode.window.createQuickPick() + versionSelector.title = 'Select a Flow CLI version' + + // Update selected binary when the user selects a version + this.#disposables.push(versionSelector.onDidAccept(async () => { + if (versionSelector.selectedItems.length === 0) return + await this.#toggleSelector(false) + + const selected = versionSelector.selectedItems[0] + + if (selected instanceof CustomBinaryItem) { + void vscode.window.showInputBox({ + placeHolder: 'Enter the path to the Flow CLI binary', + prompt: 'Enter the path to the Flow CLI binary' + }).then((path) => { + if (path != null) { + this.#cliProvider.setCurrentBinary(path) + } + }) + } else if (selected instanceof AvailableBinaryItem) { + this.#cliProvider.setCurrentBinary(selected.path) + } + })) + + // Update available versions + const items: Array = availableBinaries.map(binary => new AvailableBinaryItem(binary)) + items.push(new CustomBinaryItem()) + versionSelector.items = items + + // Select the current binary + if (currentBinary !== null) { + const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name) + console.log(currentBinaryItem) + if (currentBinaryItem != null) { + versionSelector.selectedItems = [currentBinaryItem] + } + } + + return versionSelector + } + + async #toggleSelector (show: boolean): Promise { + this.#showSelector = show + await this.#refreshSelector() + } + + async #refreshSelector (): Promise { + if (this.#showSelector) { + this.#versionSelector?.dispose() + const currentBinary = await this.#cliProvider.getCurrentBinary() + const availableBinaries = await this.#cliProvider.getAvailableBinaries() + this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries) + this.#disposables.push(this.#versionSelector) + this.#versionSelector.show() + } else { + this.#versionSelector?.dispose() + } + } + + dispose (): void { + this.#disposables.forEach(disposable => disposable.dispose()) + } +} + +class AvailableBinaryItem implements vscode.QuickPickItem { + detail?: string + picked?: boolean + alwaysShow?: boolean + #binary: CliBinary + + constructor (binary: CliBinary) { + this.#binary = binary + } + + get label (): string { + return GET_BINARY_LABEL(this.#binary.version) + } + + get description (): string { + return `(${this.#binary.name})` + } + + get path (): string { + return this.#binary.name + } +} + +class CustomBinaryItem implements vscode.QuickPickItem { + label: string + + constructor () { + this.label = 'Choose a custom version...' + } +} + +export function isCliCadenceV1 (version: SemVer): boolean { + return CADENCE_V1_CLI_REGEX.test(version.raw) +} diff --git a/extension/src/json-schema-provider.ts b/extension/src/json-schema-provider.ts index 42e96b38..a423f7a6 100644 --- a/extension/src/json-schema-provider.ts +++ b/extension/src/json-schema-provider.ts @@ -8,8 +8,6 @@ import { CliProvider } from './flow-cli/cli-provider' const CADENCE_SCHEMA_URI = 'cadence-schema' const GET_FLOW_SCHEMA_URL = (version: string): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version}/flowkit/schema.json` -const LOCAL_SCHEMA_KEY = 'local' - // This class provides the JSON schema for the flow.json file // It is accessible via the URI scheme "cadence-schema:///flow.json" export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Disposable { @@ -52,7 +50,7 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis return await this.getLocalSchema() }) } - + return await this.#schemaCache[version] } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index fa103a09..11af7b68 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -74,7 +74,7 @@ export class LanguageServerAPI { exec('killall dlv') // Required when running language server locally on mac } catch (err) { void err } } - + const env = await envVars.getValue() this.client = new LanguageClient( 'cadence', diff --git a/extension/src/settings/settings.ts b/extension/src/settings/settings.ts index aa26740a..771bafb3 100644 --- a/extension/src/settings/settings.ts +++ b/extension/src/settings/settings.ts @@ -1,5 +1,5 @@ /* Workspace Settings */ -import { BehaviorSubject, Observable, distinctUntilChanged, map, skip } from 'rxjs' +import { BehaviorSubject, Observable, distinctUntilChanged, map } from 'rxjs' import { workspace, Disposable, ConfigurationTarget } from 'vscode' import { isEqual } from 'lodash' @@ -54,20 +54,20 @@ export class Settings implements Disposable { return this.#configuration$.value } - updateSettings (config: Partial, target?: ConfigurationTarget): void { + async updateSettings (config: Partial, target?: ConfigurationTarget): Promise { // Recursively update all keys in the configuration - function update(section: string, obj: any) { - Object.entries(obj).forEach(([key, value]) => { - const newKey = section ? `${section}.${key}` : key + async function update (section: string, obj: any): Promise { + await Promise.all(Object.entries(obj).map(async ([key, value]) => { + const newKey = `${section}.${key}` if (typeof value === 'object' && !Array.isArray(value)) { - update(newKey, value) + await update(newKey, value) } else { - workspace.getConfiguration().update(newKey, value, target) + await workspace.getConfiguration().update(newKey, value, target) } - }) + })) } - update(CONFIGURATION_KEY, config) + await update(CONFIGURATION_KEY, config) } dispose (): void { diff --git a/extension/src/ui/cli-selection-provider.ts b/extension/src/ui/cli-selection-provider.ts deleted file mode 100644 index cae13a6d..00000000 --- a/extension/src/ui/cli-selection-provider.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { BehaviorSubject, first, zip } from "rxjs"; -import * as vscode from 'vscode'; -import { CliBinary, CliProvider } from "../flow-cli/cli-provider"; -import { SemVer } from "semver"; - -const TOGGLE_CADENCE_VERSION_COMMAND = "cadence.changeCadenceVersion"; -const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g; -const GET_BINARY_LABEL = (binary: CliBinary): string => `Flow CLI ${binary.version}`; - -export class CliSelectionProvider implements vscode.Disposable { - #statusBarItem: vscode.StatusBarItem | undefined; - #cliProvider: CliProvider; - #showSelector: boolean = false; - #versionSelector: vscode.QuickPick | undefined; - #disposables: vscode.Disposable[] = []; - - constructor(cliProvider: CliProvider) { - this.#cliProvider = cliProvider; - - // Register the command to toggle the version - vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, () => this.#toggleSelector(true)); - - // Register UI components - zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => { - void this.#refreshSelector(); - }) - this.#cliProvider.currentBinary$.subscribe((binary) => { - if (binary === null) return; - this.#statusBarItem?.dispose(); - this.#statusBarItem = this.#createStatusBarItem(binary?.version); - this.#statusBarItem.show(); - }) - } - - #createStatusBarItem(version: SemVer): vscode.StatusBarItem { - const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); - statusBarItem.command = TOGGLE_CADENCE_VERSION_COMMAND; - statusBarItem.color = new vscode.ThemeColor("statusBar.foreground"); - statusBarItem.tooltip = "Click to change the Flow CLI version"; - - // Update the status bar text when the version changes - statusBarItem.text = `Flow CLI v${version}`; - - return statusBarItem; - } - - #createVersionSelector(currentBinary: CliBinary | null, availableBinaries: CliBinary[]): vscode.QuickPick { - const versionSelector = vscode.window.createQuickPick(); - versionSelector.title = "Select a Flow CLI version"; - - // Update selected binary when the user selects a version - this.#disposables.push(versionSelector.onDidAccept(() => { - if (versionSelector.selectedItems.length === 0) return; - this.#toggleSelector(false); - - const selected = versionSelector.selectedItems[0]; - - if (selected instanceof CustomBinaryItem) { - void vscode.window.showInputBox({ - placeHolder: "Enter the path to the Flow CLI binary", - prompt: "Enter the path to the Flow CLI binary" - }).then((path) => { - if (path) { - this.#cliProvider.setCurrentBinary(path); - } - }); - } else if (selected instanceof AvailableBinaryItem) { - this.#cliProvider.setCurrentBinary(selected.path); - } - })); - - // Update available versions - const items: (AvailableBinaryItem | CustomBinaryItem)[] = availableBinaries.map(binary => new AvailableBinaryItem(binary)); - items.push(new CustomBinaryItem()); - versionSelector.items = items; - - // Select the current binary - if (currentBinary !== null) { - const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name); - console.log(currentBinaryItem) - if (currentBinaryItem) { - versionSelector.selectedItems = [currentBinaryItem]; - } - } - - return versionSelector; - } - - async #toggleSelector(show: boolean) { - this.#showSelector = show; - await this.#refreshSelector(); - } - - async #refreshSelector() { - if (this.#showSelector) { - this.#versionSelector?.dispose(); - const currentBinary = await this.#cliProvider.getCurrentBinary(); - const availableBinaries = await this.#cliProvider.getAvailableBinaries(); - this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries); - this.#disposables.push(this.#versionSelector); - this.#versionSelector.show(); - } else { - this.#versionSelector?.dispose(); - } - } - - dispose() { - this.#disposables.forEach(disposable => disposable.dispose()); - } -} - -class AvailableBinaryItem implements vscode.QuickPickItem { - detail?: string; - picked?: boolean; - alwaysShow?: boolean; - #binary: CliBinary; - - constructor(binary: CliBinary) { - this.#binary = binary; - } - - get label(): string { - return GET_BINARY_LABEL(this.#binary); - } - - get description(): string { - return `(${this.#binary.name})`; - } - - get path(): string { - return this.#binary.name; - } -} - -class CustomBinaryItem implements vscode.QuickPickItem { - label: string; - - constructor() { - this.label = "Choose a custom version..."; - } -} - -export function isCliCadenceV1(version: SemVer): boolean { - return CADENCE_V1_CLI_REGEX.test(version.raw); -} \ No newline at end of file diff --git a/extension/test/integration/1 - language-server.test.ts b/extension/test/integration/1 - language-server.test.ts index 8eab23ba..3fc9f30b 100644 --- a/extension/test/integration/1 - language-server.test.ts +++ b/extension/test/integration/1 - language-server.test.ts @@ -8,7 +8,7 @@ import { MaxTimeout } from '../globals' import { BehaviorSubject, Subject } from 'rxjs' import { State } from 'vscode-languageclient' import * as sinon from 'sinon' -import { CliBinary, CliProvider } from '../../src/flow-cli/cli-provider' +import { CliBinary } from '../../src/flow-cli/cli-provider' import { SemVer } from 'semver' suite('Language Server & Emulator Integration', () => { @@ -40,7 +40,7 @@ suite('Language Server & Emulator Integration', () => { currentBinary$: cliBinary$, getCurrentBinary: sinon.stub().callsFake(async () => cliBinary$.getValue()) } as any - + LS = new LanguageServerAPI(settings, mockCliProvider, mockConfig) await LS.activate() }) diff --git a/extension/test/integration/3 - schema.test.ts b/extension/test/integration/3 - schema.test.ts index 26540431..7759781c 100644 --- a/extension/test/integration/3 - schema.test.ts +++ b/extension/test/integration/3 - schema.test.ts @@ -2,7 +2,6 @@ import { MaxTimeout } from '../globals' import { before, after } from 'mocha' import * as assert from 'assert' import * as vscode from 'vscode' -import { StateCache } from '../../src/utils/state-cache' import { SemVer } from 'semver' import { JSONSchemaProvider } from '../../src/json-schema-provider' import * as fetch from 'node-fetch' @@ -29,10 +28,12 @@ suite('JSON schema tests', () => { // Mock cli provider mockCliProvider = { currentBinary$: new Subject(), - getCurrentBinary: sinon.stub().callsFake(async () => (mockFlowVersionValue ? { - name: 'flow', - version: mockFlowVersionValue - } : null)) + getCurrentBinary: sinon.stub().callsFake(async () => ((mockFlowVersionValue != null) + ? { + name: 'flow', + version: mockFlowVersionValue + } + : null)) } as any // Mock fetch (assertion is for linter workaround) diff --git a/extension/test/mock/mockSettings.ts b/extension/test/mock/mockSettings.ts index 31bee920..9c322ec8 100644 --- a/extension/test/mock/mockSettings.ts +++ b/extension/test/mock/mockSettings.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject, Observable, of, map, distinctUntilChanged, skip } from 'rxjs' +import { BehaviorSubject, Observable, of, map, distinctUntilChanged } from 'rxjs' import { CadenceConfiguration, Settings } from '../../src/settings/settings' import * as path from 'path' import { isEqual } from 'lodash'