From 2257363ae21238885279150bf700ff4c1be6a613 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 07:56:15 -0700 Subject: [PATCH] Add Flow CLI version selection functionality --- .../workflows/add-issues-to-devx-project.yml | 2 +- .../dependency-installer.ts | 11 +- .../src/dependency-installer/installer.ts | 4 +- .../installers/flow-cli-installer.ts | 10 +- extension/src/extension.ts | 20 +- extension/src/flow-cli/cli-provider.ts | 143 +++++++ .../src/flow-cli/flow-version-provider.ts | 57 --- extension/src/json-schema-provider.ts | 116 +++--- extension/src/server/flow-config.ts | 2 +- extension/src/server/language-server.ts | 41 +- extension/src/settings/settings.ts | 34 +- extension/src/test-provider/test-resolver.ts | 3 - extension/src/ui/cli-selection-provider.ts | 145 +++++++ .../test/integration/0 - dependencies.test.ts | 12 +- .../integration/1 - language-server.test.ts | 25 +- extension/test/integration/3 - schema.test.ts | 41 +- extension/test/mock/mockSettings.ts | 15 +- extension/test/unit/parser.test.ts | 2 +- package-lock.json | 380 +----------------- package.json | 9 +- 20 files changed, 495 insertions(+), 577 deletions(-) create mode 100644 extension/src/flow-cli/cli-provider.ts delete mode 100644 extension/src/flow-cli/flow-version-provider.ts create mode 100644 extension/src/ui/cli-selection-provider.ts diff --git a/.github/workflows/add-issues-to-devx-project.yml b/.github/workflows/add-issues-to-devx-project.yml index 4413cee8..ea2b8e0c 100644 --- a/.github/workflows/add-issues-to-devx-project.yml +++ b/.github/workflows/add-issues-to-devx-project.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.6.0 + - uses: actions/add-to-project@v0.5.0 with: project-url: https://github.com/orgs/onflow/projects/13 github-token: ${{ secrets.GH_ACTION_FOR_PROJECTS }} diff --git a/extension/src/dependency-installer/dependency-installer.ts b/extension/src/dependency-installer/dependency-installer.ts index 6c154061..525e640d 100644 --- a/extension/src/dependency-installer/dependency-installer.ts +++ b/extension/src/dependency-installer/dependency-installer.ts @@ -4,7 +4,7 @@ import { Installer, InstallerConstructor, InstallerContext, InstallError } from import { promptUserErrorMessage } from '../ui/prompts' import { StateCache } from '../utils/state-cache' import { LanguageServerAPI } from '../server/language-server' -import { FlowVersionProvider } from '../flow-cli/flow-version-provider' +import { CliProvider } from '../flow-cli/cli-provider' const INSTALLERS: InstallerConstructor[] = [ InstallFlowCLI @@ -15,12 +15,14 @@ export class DependencyInstaller { missingDependencies: StateCache #installerContext: InstallerContext - constructor (languageServerApi: LanguageServerAPI, flowVersionProvider: FlowVersionProvider) { + constructor (languageServerApi: LanguageServerAPI, cliProvider: CliProvider) { this.#installerContext = { refreshDependencies: this.checkDependencies.bind(this), languageServerApi, - flowVersionProvider + cliProvider } + + // Register installers this.#registerInstallers() // Create state cache for missing dependencies @@ -54,7 +56,8 @@ export class DependencyInstaller { async checkDependencies (): Promise { // Invalidate and wait for state to update // This will trigger the missingDependencies subscriptions - await this.missingDependencies.getValue(true) + this.missingDependencies.invalidate() + await this.missingDependencies.getValue() } async installMissing (): Promise { diff --git a/extension/src/dependency-installer/installer.ts b/extension/src/dependency-installer/installer.ts index 0a7d20f5..15444d26 100644 --- a/extension/src/dependency-installer/installer.ts +++ b/extension/src/dependency-installer/installer.ts @@ -2,7 +2,7 @@ import { window } from 'vscode' import { envVars } from '../utils/shell/env-vars' import { LanguageServerAPI } from '../server/language-server' -import { FlowVersionProvider } from '../flow-cli/flow-version-provider' +import { CliProvider } from '../flow-cli/cli-provider' // InstallError is thrown if install fails export class InstallError extends Error {} @@ -10,7 +10,7 @@ export class InstallError extends Error {} export interface InstallerContext { refreshDependencies: () => Promise languageServerApi: LanguageServerAPI - flowVersionProvider: FlowVersionProvider + cliProvider: CliProvider } export type InstallerConstructor = new (context: InstallerContext) => Installer diff --git a/extension/src/dependency-installer/installers/flow-cli-installer.ts b/extension/src/dependency-installer/installers/flow-cli-installer.ts index 79ae0045..32d4c951 100644 --- a/extension/src/dependency-installer/installers/flow-cli-installer.ts +++ b/extension/src/dependency-installer/installers/flow-cli-installer.ts @@ -99,9 +99,9 @@ export class InstallFlowCLI extends Installer { async checkVersion (vsn?: semver.SemVer): Promise { // Get user's version informaton - this.#context.flowVersionProvider.refresh() - const version = vsn ?? await this.#context.flowVersionProvider.getVersion() - if (version === null) return false + this.#context.cliProvider.refresh() + const version = vsn ?? await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow')?.version) + if (version == null) return false if (!semver.satisfies(version, COMPATIBLE_FLOW_CLI_VERSIONS, { includePrerelease: true @@ -127,8 +127,8 @@ export class InstallFlowCLI extends Installer { async verifyInstall (): Promise { // Check if flow version is valid to verify install - this.#context.flowVersionProvider.refresh() - const version = await this.#context.flowVersionProvider.getVersion() + this.#context.cliProvider.refresh() + const version = await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow')?.version) if (version == null) return false // Check flow-cli version number diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 527c494f..0fc0f9ee 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -4,7 +4,7 @@ import { CommandController } from './commands/command-controller' import { ExtensionContext } from 'vscode' import { DependencyInstaller } from './dependency-installer/dependency-installer' import { Settings } from './settings/settings' -import { FlowVersionProvider } from './flow-cli/flow-version-provider' +import { CliProvider } from './flow-cli/cli-provider' import { JSONSchemaProvider } from './json-schema-provider' import { LanguageServerAPI } from './server/language-server' import { FlowConfig } from './server/flow-config' @@ -12,6 +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' // The container for all data relevant to the extension. export class Extension { @@ -30,6 +31,7 @@ export class Extension { #dependencyInstaller: DependencyInstaller #commands: CommandController #testProvider: TestProvider + //#schemaProvider: JSONSchemaProvider private constructor (settings: Settings, ctx: ExtensionContext) { this.ctx = ctx @@ -41,24 +43,29 @@ export class Extension { const notificationProvider = new NotificationProvider(storageProvider) notificationProvider.activate() - // Register Flow version provider - const flowVersionProvider = new FlowVersionProvider(settings) + // Register CliProvider + const cliProvider = new CliProvider(settings) + + // Register CliSelectionProvider + const cliSelectionProvider = new CliSelectionProvider(cliProvider) // Register JSON schema provider - if (ctx != null) JSONSchemaProvider.register(ctx, flowVersionProvider.state$) + if (ctx != null) { + //this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider.currentBinary$) + } // Initialize Flow Config const flowConfig = new FlowConfig(settings) void flowConfig.activate() // Initialize Language Server - this.languageServer = new LanguageServerAPI(settings, flowConfig) + this.languageServer = new LanguageServerAPI(settings, cliProvider, flowConfig) // Check for any missing dependencies // The language server will start if all dependencies are installed // Otherwise, the language server will not start and will start after // the user installs the missing dependencies - this.#dependencyInstaller = new DependencyInstaller(this.languageServer, flowVersionProvider) + this.#dependencyInstaller = new DependencyInstaller(this.languageServer, cliProvider) this.#dependencyInstaller.missingDependencies.subscribe((missing) => { if (missing.length === 0) { void this.languageServer.activate() @@ -80,5 +87,6 @@ export class Extension { async deactivate (): Promise { await this.languageServer.deactivate() this.#testProvider?.dispose() + //this.#schemaProvider?.dispose() } } diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts new file mode 100644 index 00000000..93d179e6 --- /dev/null +++ b/extension/src/flow-cli/cli-provider.ts @@ -0,0 +1,143 @@ +import { BehaviorSubject, Observable, distinctUntilChanged, pairwise, startWith } from 'rxjs' +import { execDefault } from '../utils/shell/exec' +import { StateCache } from '../utils/state-cache' +import * as semver from 'semver' +import { Settings } from '../settings/settings' + +const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version` +const KNOWN_BINS = ['flow', 'flow-c1'] + +const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g + +export type CliBinary = { + name: string + version: semver.SemVer +} + +type AvailableBinariesCache = { + [key: string]: StateCache +} + +export class CliProvider { + #selectedBinaryName: BehaviorSubject + #currentBinary$: StateCache + #availableBinaries: AvailableBinariesCache = {} + #availableBinaries$: StateCache + #settings: Settings + + constructor (settings: Settings) { + this.#settings = settings + + this.#selectedBinaryName = new BehaviorSubject(settings.getSettings().flowCommand) + this.#settings.settings$(config => config.flowCommand).subscribe((flowCommand) => { + this.#selectedBinaryName.next(flowCommand) + }) + + 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() + }) + + this.#currentBinary$ = new StateCache(async () => { + const name: string = this.#selectedBinaryName.getValue() + return 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)) { + delete this.#availableBinaries[prev] + } + if (curr != null && !KNOWN_BINS.includes(curr)) { + this.#availableBinaries[curr] = new StateCache(async () => await this.#fetchBinaryInformation(curr)) + this.#availableBinaries[curr].subscribe(() => { + this.#availableBinaries$.invalidate() + }) + } + + // Invalidate the current binary cache + this.#currentBinary$.invalidate() + + // Invalidate the available binaries cache + this.#availableBinaries$.invalidate() + }) + } + + async #fetchBinaryInformation (bin: string): Promise { + try { + // Get user's version informaton + const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD( + bin + ))).stdout + + // Format version string from output + let versionStr: string | null = parseFlowCliVersion(buffer) + + versionStr = semver.clean(versionStr) + if (versionStr === null) return null + + // Ensure user has a compatible version number installed + const version: semver.SemVer | null = semver.parse(versionStr) + if (version === null) return null + + return { name: bin, version } + } catch { + return null + } + } + + refresh (): void { + for (const bin in this.#availableBinaries) { + this.#availableBinaries[bin].invalidate() + } + this.#currentBinary$.invalidate() + } + + get availableBinaries$ (): Observable { + return new Observable((subscriber) => { + this.#availableBinaries$.subscribe((binaries) => { + subscriber.next(binaries) + }) + }) + } + + + async getAvailableBinaries (): Promise { + const bins: CliBinary[] = [] + for (const name in this.#availableBinaries) { + const binary = await this.#availableBinaries[name].getValue().catch(() => null) + if (binary !== null) { + bins.push(binary) + } + } + return bins + } + + get currentBinary$ (): Observable { + return this.#currentBinary$ + } + + async getCurrentBinary (): Promise { + return this.#currentBinary$.getValue() + } + + setCurrentBinary (name: string): void { + this.#settings.updateSettings({ flowCommand: name }) + } +} + +export function isCadenceV1Cli (version: semver.SemVer): boolean { + return CADENCE_V1_CLI_REGEX.test(version.raw) +} + +export function parseFlowCliVersion (buffer: Buffer | string): string { + return (buffer.toString().split('\n')[0]).split(' ')[1] +} diff --git a/extension/src/flow-cli/flow-version-provider.ts b/extension/src/flow-cli/flow-version-provider.ts deleted file mode 100644 index d3bf1bc0..00000000 --- a/extension/src/flow-cli/flow-version-provider.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Settings } from '../settings/settings' -import { execDefault } from '../utils/shell/exec' -import { StateCache } from '../utils/state-cache' -import * as semver from 'semver' - -const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version` - -export class FlowVersionProvider { - #settings: Settings - #stateCache: StateCache - #parseCliVersion: (buffer: Buffer | string) => string - - constructor (settings: Settings, parseCliVersion: (buffer: Buffer | string) => string = parseFlowCliVersion) { - this.#stateCache = new StateCache(async () => await this.#fetchFlowVersion()) - this.#settings = settings - this.#parseCliVersion = parseCliVersion - } - - async #fetchFlowVersion (): Promise { - try { - // Get user's version informaton - const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD( - this.#settings.getSettings().flowCommand - ))).stdout - - // Format version string from output - let versionStr: string | null = this.#parseCliVersion(buffer) - - versionStr = semver.clean(versionStr) - if (versionStr === null) return null - - // Ensure user has a compatible version number installed - const version: semver.SemVer | null = semver.parse(versionStr) - if (version === null) return null - - return version - } catch { - return null - } - } - - refresh (): void { - this.#stateCache.invalidate() - } - - async getVersion (): Promise { - return await this.#stateCache.getValue() - } - - get state$ (): StateCache { - return this.#stateCache - } -} - -export function parseFlowCliVersion (buffer: Buffer | string): string { - return (buffer.toString().split('\n')[0]).split(' ')[1] -} diff --git a/extension/src/json-schema-provider.ts b/extension/src/json-schema-provider.ts index a8fdb0f5..42e96b38 100644 --- a/extension/src/json-schema-provider.ts +++ b/extension/src/json-schema-provider.ts @@ -2,68 +2,69 @@ import * as vscode from 'vscode' import { readFile } from 'fs' import { promisify } from 'util' import { resolve } from 'path' -import { SemVer } from 'semver' import fetch from 'node-fetch' -import { StateCache } from './utils/state-cache' -import { Subscription } from 'rxjs' +import { CliProvider } from './flow-cli/cli-provider' -const GET_FLOW_SCHEMA_URL = (version: SemVer): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version.format()}/flowkit/schema.json` +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 { - static CADENCE_SCHEMA_URI = 'cadence-schema' - static #instance: JSONSchemaProvider | null - #contentProviderDisposable: vscode.Disposable | undefined - #flowVersionSubscription: Subscription - #flowVersion: StateCache - #flowSchema: StateCache - #showLocalError: boolean = false - - static register (ctx: vscode.ExtensionContext, flowVersion: StateCache): void { - if (JSONSchemaProvider.#instance != null) { - JSONSchemaProvider.#instance.dispose() - } + #extensionPath: string + #cliProvider: CliProvider + #schemaCache: { [version: string]: Promise } = {} - // Create a provider for the cadence-schema URI scheme, this will be deactivated when the extension is deactivated - JSONSchemaProvider.#instance = new JSONSchemaProvider( - ctx, - flowVersion, - { dispose: () => contentProviderDisposable.dispose() } - ) - const contentProviderDisposable = vscode.workspace.registerFileSystemProvider( - JSONSchemaProvider.CADENCE_SCHEMA_URI, - JSONSchemaProvider.#instance - ) - ctx.subscriptions.push( - JSONSchemaProvider.#instance + constructor ( + extensionPath: string, + cliProvider: CliProvider + ) { + this.#extensionPath = extensionPath + this.#cliProvider = cliProvider + + // Register the schema provider + this.#contentProviderDisposable = vscode.workspace.registerFileSystemProvider( + CADENCE_SCHEMA_URI, + this ) } - private constructor ( - private readonly ctx: vscode.ExtensionContext, - flowVersion: StateCache, - contentProviderDisposable: vscode.Disposable - ) { - this.#flowVersion = flowVersion - this.#contentProviderDisposable = contentProviderDisposable - this.#flowSchema = new StateCache(async () => await this.#resolveFlowSchema()) + async #getFlowSchema (): Promise { + const cliBinary = await this.#cliProvider.getCurrentBinary() + if (cliBinary == null) { + void vscode.window.showWarningMessage('Cannot get flow-cli version, using local schema instead. Please install flow-cli to get the latest schema.') + return await this.getLocalSchema() + } - // Invalidate the schema when the flow-cli version changes - this.#flowVersionSubscription = this.#flowVersion.subscribe( - () => this.#flowSchema.invalidate() - ) + const version = cliBinary.version.format() + if (this.#schemaCache[version] == null) { + // Try to get schema from flow-cli repo based on the flow-cli version + this.#schemaCache[version] = fetch(GET_FLOW_SCHEMA_URL(version)).then(async (response: Response) => { + if (!response.ok) { + throw new Error(`Failed to fetch schema for flow-cli version ${version}`) + } + return await response.text() + }).catch(async () => { + void vscode.window.showWarningMessage('Failed to fetch flow.json schema from flow-cli repo, using local schema instead. Please update flow-cli to the latest version to get the latest schema.') + return await this.getLocalSchema() + }) + } + + return await this.#schemaCache[version] + } + + async getLocalSchema (): Promise { + const schemaUrl = resolve(this.#extensionPath, 'flow-schema.json') + return await promisify(readFile)(schemaUrl).then(x => x.toString()) } async readFile (uri: vscode.Uri): Promise { if (uri.path === '/flow.json') { - const schema = await this.#flowSchema.getValue() - if (this.#showLocalError) { - void vscode.window.showWarningMessage('Failed to fetch flow.json schema from flow-cli repo, using local schema instead. Please update flow-cli to the latest version to get the latest schema.') - this.#showLocalError = false - } - return Buffer.from(schema, 'utf-8') + const schema = await this.#getFlowSchema() + return Buffer.from(schema) } else { throw new Error('Unknown schema') } @@ -76,7 +77,7 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis type: vscode.FileType.File, ctime: 0, mtime: 0, - size: await this.#flowSchema.getValue().then(x => x.length) + size: await this.#getFlowSchema().then(x => x.length) } } else { throw new Error('Unknown schema') @@ -87,27 +88,6 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis if (this.#contentProviderDisposable != null) { this.#contentProviderDisposable.dispose() } - this.#flowVersionSubscription.unsubscribe() - } - - async #resolveFlowSchema (): Promise { - return await this.#flowVersion.getValue().then(async (cliVersion) => { - // Verify that version is valid (could be null if flow-cli is not installed, etc.) - if (cliVersion == null) throw new Error('Failed to get flow-cli version, please make sure flow-cli is installed and in your PATH') - - // Try to get schema from flow-cli repo based on the flow-cli version - return fetch(GET_FLOW_SCHEMA_URL(cliVersion)) - }).then(async (response: Response) => { - if (!response.ok) { - throw new Error(`Failed to fetch schema from flow-cli repo: ${response.statusText}`) - } - return await response.text() - }).catch(async () => { - // Fallback to local schema - this.#showLocalError = true - const schemaUrl = resolve(this.ctx.extensionPath, 'flow-schema.json') - return await promisify(readFile)(schemaUrl).then(x => x.toString()) - }) } // Unsupported file system provider methods diff --git a/extension/src/server/flow-config.ts b/extension/src/server/flow-config.ts index bb27ec43..c6b9de44 100644 --- a/extension/src/server/flow-config.ts +++ b/extension/src/server/flow-config.ts @@ -172,7 +172,7 @@ export class FlowConfig implements Disposable { // Watch and reload flow configuration when changed. #watchWorkspaceConfiguration (): Subscription { - return this.#settings.watch$(config => config.customConfigPath).subscribe(() => { + return this.#settings.settings$(config => config.customConfigPath).subscribe(() => { void this.reloadConfigPath() }) } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index f643b07b..fa103a09 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -3,26 +3,29 @@ import { window } from 'vscode' import { Settings } from '../settings/settings' import { exec } from 'child_process' import { ExecuteCommandRequest } from 'vscode-languageclient' -import { BehaviorSubject, Subscription, filter, firstValueFrom } from 'rxjs' +import { BehaviorSubject, Subscription, filter, firstValueFrom, skip, zip } from 'rxjs' import { envVars } from '../utils/shell/env-vars' import { FlowConfig } from './flow-config' +import { CliProvider } from '../flow-cli/cli-provider' // Identities for commands handled by the Language server const RELOAD_CONFIGURATION = 'cadence.server.flow.reloadConfiguration' export class LanguageServerAPI { - config: FlowConfig + #settings: Settings + #config: FlowConfig + #cliProvider: CliProvider client: LanguageClient | null = null - settings: Settings clientState$ = new BehaviorSubject(State.Stopped) #subscriptions: Subscription[] = [] #isActive = false - constructor (settings: Settings, config: FlowConfig) { - this.settings = settings - this.config = config + constructor (settings: Settings, cliProvider: CliProvider, config: FlowConfig) { + this.#settings = settings + this.#cliProvider = cliProvider + this.#config = config } async activate (): Promise { @@ -58,21 +61,26 @@ export class LanguageServerAPI { // Set client state to starting this.clientState$.next(State.Starting) - const accessCheckMode: string = this.settings.getSettings().accessCheckMode - const configPath: string | null = this.config.configPath + const accessCheckMode: string = this.#settings.getSettings().accessCheckMode + const configPath: string | null = this.#config.configPath - if (this.settings.getSettings().flowCommand !== 'flow') { + const binaryPath = (await this.#cliProvider.getCurrentBinary())?.name + if (binaryPath == null) { + throw new Error('No flow binary found') + } + + if (binaryPath !== 'flow') { try { 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', 'Cadence', { - command: this.settings.getSettings().flowCommand, + command: binaryPath, args: ['cadence', 'language-server', '--enable-flow-client=false'], options: { env @@ -124,7 +132,7 @@ export class LanguageServerAPI { #subscribeToConfigChanges (): void { const tryReloadConfig = (): void => { void this.#sendRequest(RELOAD_CONFIGURATION).catch(() => {}) } - this.#subscriptions.push(this.config.fileModified$.subscribe(() => { + this.#subscriptions.push(this.#config.fileModified$.subscribe(() => { // Reload configuration if (this.clientState$.getValue() === State.Running) { tryReloadConfig() @@ -136,7 +144,7 @@ export class LanguageServerAPI { } })) - this.#subscriptions.push(this.config.pathChanged$.subscribe(() => { + this.#subscriptions.push(this.#config.pathChanged$.subscribe(() => { // Restart client void this.restart() })) @@ -148,8 +156,11 @@ export class LanguageServerAPI { void this.restart() } - this.#subscriptions.push(this.settings.watch$((config) => config.flowCommand).subscribe(onChange)) - this.#subscriptions.push(this.settings.watch$((config) => config.accessCheckMode).subscribe(onChange)) + const subscription = zip( + this.#cliProvider.currentBinary$.pipe(skip(1)), + this.#settings.settings$((config) => config.flowCommand).pipe(skip(1)) + ).subscribe(onChange) + this.#subscriptions.push(subscription) } async #sendRequest (cmd: string, args: any[] = []): Promise { diff --git a/extension/src/settings/settings.ts b/extension/src/settings/settings.ts index 9a7bc773..aa26740a 100644 --- a/extension/src/settings/settings.ts +++ b/extension/src/settings/settings.ts @@ -1,8 +1,10 @@ /* Workspace Settings */ import { BehaviorSubject, Observable, distinctUntilChanged, map, skip } from 'rxjs' -import { workspace, Disposable } from 'vscode' +import { workspace, Disposable, ConfigurationTarget } from 'vscode' import { isEqual } from 'lodash' +const CONFIGURATION_KEY = 'cadence' + // Schema for the cadence configuration export interface CadenceConfiguration { flowCommand: string @@ -20,7 +22,7 @@ export class Settings implements Disposable { constructor () { // Watch for configuration changes this.#disposables.push(workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('cadence')) { + if (e.affectsConfiguration(CONFIGURATION_KEY)) { this.#configuration$.next(this.#getConfiguration()) } })) @@ -35,30 +37,44 @@ export class Settings implements Disposable { * @template T The type of the selected value * @example * // Emit whenever the flow command changes - * settings.watch$(config => config.flowCommand) + * settings.settings$(config => config.flowCommand) */ - watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { - return this.#configuration$.asObservable().pipe( - skip(1), + settings$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { + return this.#configuration$.pipe( map(selector), distinctUntilChanged(isEqual) ) } /** - * Returns the current configuration - * + * Get the current configuration * @returns The current configuration */ getSettings (): CadenceConfiguration { return this.#configuration$.value } + updateSettings (config: Partial, target?: ConfigurationTarget): void { + // 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 + if (typeof value === 'object' && !Array.isArray(value)) { + update(newKey, value) + } else { + workspace.getConfiguration().update(newKey, value, target) + } + }) + } + + update(CONFIGURATION_KEY, config) + } + dispose (): void { this.#configuration$.complete() } #getConfiguration (): CadenceConfiguration { - return workspace.getConfiguration('cadence') as unknown as CadenceConfiguration + return workspace.getConfiguration(CONFIGURATION_KEY) as unknown as CadenceConfiguration } } diff --git a/extension/src/test-provider/test-resolver.ts b/extension/src/test-provider/test-resolver.ts index 422887fc..4dd149fd 100644 --- a/extension/src/test-provider/test-resolver.ts +++ b/extension/src/test-provider/test-resolver.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode' import * as CadenceParser from '@onflow/cadence-parser' -import { StateCache } from '../utils/state-cache' import { QueuedMutator, TestFunction, TestTrie } from './test-trie' import { isDirectory } from '../utils/utils' @@ -26,7 +25,6 @@ interface Declaration { } export class TestResolver implements vscode.Disposable { - testTree: StateCache #controller: vscode.TestController #parser: Thenable #testTrie: QueuedMutator @@ -35,7 +33,6 @@ export class TestResolver implements vscode.Disposable { constructor (parserLocation: string, controller: vscode.TestController, testTrie: QueuedMutator) { this.#controller = controller this.#parser = vscode.workspace.fs.readFile(vscode.Uri.file(parserLocation)).then(async buffer => await CadenceParser.CadenceParser.create(buffer)) - this.testTree = new StateCache(async () => await Promise.resolve()) this.#testTrie = testTrie void this.watchFiles() diff --git a/extension/src/ui/cli-selection-provider.ts b/extension/src/ui/cli-selection-provider.ts new file mode 100644 index 00000000..cae13a6d --- /dev/null +++ b/extension/src/ui/cli-selection-provider.ts @@ -0,0 +1,145 @@ +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/0 - dependencies.test.ts b/extension/test/integration/0 - dependencies.test.ts index 674daf97..2114475d 100644 --- a/extension/test/integration/0 - dependencies.test.ts +++ b/extension/test/integration/0 - dependencies.test.ts @@ -4,15 +4,15 @@ import { MaxTimeout } from '../globals' import { InstallFlowCLI } from '../../src/dependency-installer/installers/flow-cli-installer' import { stub } from 'sinon' import { before } from 'mocha' -import { FlowVersionProvider } from '../../src/flow-cli/flow-version-provider' +import { CliProvider } from '../../src/flow-cli/cli-provider' import { getMockSettings } from '../mock/mockSettings' // Note: Dependency installation must run before other integration tests suite('Dependency Installer', () => { - let flowVersionProvider: any + let cliProvider: any before(async function () { - flowVersionProvider = new FlowVersionProvider(getMockSettings()) + cliProvider = new CliProvider(getMockSettings()) }) test('Install Missing Dependencies', async () => { @@ -21,7 +21,7 @@ suite('Dependency Installer', () => { deactivate: stub(), isActive: true } - const dependencyManager = new DependencyInstaller(mockLanguageServerApi as any, flowVersionProvider) + const dependencyManager = new DependencyInstaller(mockLanguageServerApi as any, cliProvider) await assert.doesNotReject(async () => { await dependencyManager.installMissing() }) // Check that all dependencies are installed @@ -42,7 +42,7 @@ suite('Dependency Installer', () => { const mockInstallerContext = { refreshDependencies: async () => {}, languageServerApi: mockLanguageServerApi as any, - flowVersionProvider + cliProvider } const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) @@ -65,7 +65,7 @@ suite('Dependency Installer', () => { const mockInstallerContext = { refreshDependencies: async () => {}, languageServerApi: mockLanguageServerApi as any, - flowVersionProvider + cliProvider } const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) diff --git a/extension/test/integration/1 - language-server.test.ts b/extension/test/integration/1 - language-server.test.ts index f5673b58..8eab23ba 100644 --- a/extension/test/integration/1 - language-server.test.ts +++ b/extension/test/integration/1 - language-server.test.ts @@ -5,8 +5,11 @@ import { LanguageServerAPI } from '../../src/server/language-server' import { FlowConfig } from '../../src/server/flow-config' import { Settings } from '../../src/settings/settings' import { MaxTimeout } from '../globals' -import { Subject } from 'rxjs' +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 { SemVer } from 'semver' suite('Language Server & Emulator Integration', () => { let LS: LanguageServerAPI @@ -14,6 +17,7 @@ suite('Language Server & Emulator Integration', () => { let mockConfig: FlowConfig let fileModified$: Subject let pathChanged$: Subject + let cliBinary$: BehaviorSubject before(async function () { this.timeout(MaxTimeout) @@ -27,7 +31,17 @@ suite('Language Server & Emulator Integration', () => { configPath: null } as any - LS = new LanguageServerAPI(settings, mockConfig) + // create a mock cli provider without invokign the constructor + cliBinary$ = new BehaviorSubject({ + name: 'flow', + version: new SemVer('1.0.0') + }) + const mockCliProvider = { + currentBinary$: cliBinary$, + getCurrentBinary: sinon.stub().callsFake(async () => cliBinary$.getValue()) + } as any + + LS = new LanguageServerAPI(settings, mockCliProvider, mockConfig) await LS.activate() }) @@ -37,7 +51,6 @@ suite('Language Server & Emulator Integration', () => { }) test('Language Server Client', async () => { - await LS.startClient() assert.notStrictEqual(LS.client, undefined) assert.equal(LS.client?.state, State.Running) }) @@ -46,9 +59,13 @@ suite('Language Server & Emulator Integration', () => { const client = LS.client await LS.deactivate() - // Check that client remains stopped even if config changes + // Check that client remains stopped even if config changes or CLI binary changes fileModified$.next() pathChanged$.next('foo') + cliBinary$.next({ + name: 'flow', + version: new SemVer('1.0.1') + }) assert.equal(client?.state, State.Stopped) assert.equal(LS.client, null) diff --git a/extension/test/integration/3 - schema.test.ts b/extension/test/integration/3 - schema.test.ts index 1083ccf6..26540431 100644 --- a/extension/test/integration/3 - schema.test.ts +++ b/extension/test/integration/3 - schema.test.ts @@ -8,25 +8,32 @@ import { JSONSchemaProvider } from '../../src/json-schema-provider' import * as fetch from 'node-fetch' import { readFileSync } from 'fs' import * as path from 'path' +import * as sinon from 'sinon' +import { CliProvider } from '../../src/flow-cli/cli-provider' +import { Subject } from 'rxjs' suite('JSON schema tests', () => { let mockFlowVersionValue: SemVer | null = null - let mockFlowVersion: StateCache - let mockContext: vscode.ExtensionContext + let mockCliProvider: CliProvider + let extensionPath: string + let schemaProvider: JSONSchemaProvider let originalFetch: typeof fetch before(async function () { this.timeout(MaxTimeout) - // Mock extension context - mockContext = { - extensionPath: path.resolve(__dirname, '../../../..'), - subscriptions: [] as vscode.Disposable[] - } as any + // Mock extension path + extensionPath = path.resolve(__dirname, '../../../..') - // Mock flow version - mockFlowVersion = new StateCache(async () => mockFlowVersionValue) + // Mock cli provider + mockCliProvider = { + currentBinary$: new Subject(), + getCurrentBinary: sinon.stub().callsFake(async () => (mockFlowVersionValue ? { + name: 'flow', + version: mockFlowVersionValue + } : null)) + } as any // Mock fetch (assertion is for linter workaround) originalFetch = fetch.default @@ -49,43 +56,39 @@ suite('JSON schema tests', () => { } // Initialize the schema provider - JSONSchemaProvider.register(mockContext, mockFlowVersion) + schemaProvider = new JSONSchemaProvider(extensionPath, mockCliProvider) }) after(async function () { this.timeout(MaxTimeout) - // Restore fetch (assertion is for linter workaround) + // Restore fetch ;(fetch as unknown as any).default = originalFetch - // Clear subscriptions - mockContext.subscriptions.forEach((sub) => sub.dispose()) - ;(mockContext as any).subscriptions = [] + // Dispose the schema provider + schemaProvider.dispose() }) test('Defaults to local schema when version not found', async () => { mockFlowVersionValue = new SemVer('0.0.0') - mockFlowVersion.invalidate() // Assert that the schema is the same as the local schema await vscode.workspace.fs.readFile(vscode.Uri.parse('cadence-schema:///flow.json')).then((data) => { - assert.strictEqual(data.toString(), readFileSync(path.resolve(mockContext.extensionPath, './flow-schema.json'), 'utf-8')) + assert.strictEqual(data.toString(), readFileSync(path.resolve(extensionPath, './flow-schema.json'), 'utf-8')) }) }).timeout(MaxTimeout) test('Defaults to local schema when version is invalid', async () => { mockFlowVersionValue = null - mockFlowVersion.invalidate() // Assert that the schema is the same as the local schema await vscode.workspace.fs.readFile(vscode.Uri.parse('cadence-schema:///flow.json')).then((data) => { - assert.strictEqual(data.toString(), readFileSync(path.resolve(mockContext.extensionPath, './flow-schema.json'), 'utf-8')) + assert.strictEqual(data.toString(), readFileSync(path.resolve(extensionPath, './flow-schema.json'), 'utf-8')) }) }).timeout(MaxTimeout) test('Fetches remote schema for current CLI version', async () => { mockFlowVersionValue = new SemVer('1.0.0') - mockFlowVersion.invalidate() // Assert that the schema is the same as the remote schema await vscode.workspace.fs.readFile(vscode.Uri.parse('cadence-schema:///flow.json')).then((data) => { diff --git a/extension/test/mock/mockSettings.ts b/extension/test/mock/mockSettings.ts index e341fa31..31bee920 100644 --- a/extension/test/mock/mockSettings.ts +++ b/extension/test/mock/mockSettings.ts @@ -3,13 +3,13 @@ import { CadenceConfiguration, Settings } from '../../src/settings/settings' import * as path from 'path' import { isEqual } from 'lodash' -export function getMockSettings (settings$: BehaviorSubject> | Partial | null = null): Settings { - const mockSettings: Settings = { getSettings, watch$ } as any +export function getMockSettings (_settings$: BehaviorSubject> | Partial | null = null): Settings { + const mockSettings: Settings = { getSettings, settings$ } as any function getSettings (): Partial { - if (!(settings$ instanceof BehaviorSubject) && settings$ != null) return settings$ + if (!(_settings$ instanceof BehaviorSubject) && _settings$ != null) return _settings$ - return settings$?.getValue() ?? { + return _settings$?.getValue() ?? { flowCommand: 'flow', accessCheckMode: 'strict', customConfigPath: path.join(__dirname, '../integration/fixtures/workspace/flow.json'), @@ -19,11 +19,10 @@ export function getMockSettings (settings$: BehaviorSubject (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { - if (!(settings$ instanceof BehaviorSubject)) return of() + function settings$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { + if (!(_settings$ instanceof BehaviorSubject)) return of() - return settings$.asObservable().pipe( - skip(1), + return _settings$.asObservable().pipe( map(selector as any), distinctUntilChanged(isEqual) ) diff --git a/extension/test/unit/parser.test.ts b/extension/test/unit/parser.test.ts index fe3ae107..6aba70a6 100644 --- a/extension/test/unit/parser.test.ts +++ b/extension/test/unit/parser.test.ts @@ -1,5 +1,5 @@ import * as assert from 'assert' -import { parseFlowCliVersion } from '../../src/flow-cli/flow-version-provider' +import { parseFlowCliVersion } from '../../src/flow-cli/cli-provider' import { execDefault } from '../../src/utils/shell/exec' import { ASSERT_EQUAL } from '../globals' import * as semver from 'semver' diff --git a/package-lock.json b/package-lock.json index fb963144..b194a226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "vscode-languageclient": "^9.0.1" }, "devDependencies": { - "@types/chai": "^4.3.12", + "@types/chai": "^4.3.11", "@types/expect": "^24.3.0", "@types/glob": "^8.0.1", "@types/lodash": "^4.17.0", @@ -36,7 +36,7 @@ "@types/mocha": "^10.0.6", "@types/node": "^20.11.28", "@types/object-hash": "^3.0.6", - "@types/semver": "^7.5.8", + "@types/semver": "^7.5.7", "@types/sinon": "^17.0.3", "@types/uuid": "^9.0.8", "@types/vscode": "^1.82.0", @@ -398,70 +398,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", - "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", - "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", - "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", - "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", @@ -478,294 +414,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", - "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", - "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", - "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", - "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", - "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", - "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", - "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", - "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", - "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", - "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", - "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", - "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", - "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", - "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", - "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", - "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", - "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", - "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -1343,9 +991,9 @@ "dev": true }, "node_modules/@types/chai": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.12.tgz", - "integrity": "sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, "node_modules/@types/expect": { @@ -1429,9 +1077,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz", - "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1444,9 +1092,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, "node_modules/@types/sinon": { @@ -3998,9 +3646,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 0b62f4ac..d1de7963 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,11 @@ "command": "cadence.checkDepencencies", "category": "Cadence", "title": "Check Dependencies" + }, + { + "command": "cadence.changeCadenceVersion", + "category": "Cadence", + "title": "Change Cadence Version" } ], "configuration": { @@ -179,7 +184,7 @@ ] }, "devDependencies": { - "@types/chai": "^4.3.12", + "@types/chai": "^4.3.11", "@types/expect": "^24.3.0", "@types/glob": "^8.0.1", "@types/lodash": "^4.17.0", @@ -187,7 +192,7 @@ "@types/mocha": "^10.0.6", "@types/node": "^20.11.28", "@types/object-hash": "^3.0.6", - "@types/semver": "^7.5.8", + "@types/semver": "^7.5.7", "@types/sinon": "^17.0.3", "@types/uuid": "^9.0.8", "@types/vscode": "^1.82.0",