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..cf48d280 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 './flow-cli/cli-selection-provider' // The container for all data relevant to the extension. export class Extension { @@ -30,6 +31,8 @@ export class Extension { #dependencyInstaller: DependencyInstaller #commands: CommandController #testProvider: TestProvider + #schemaProvider: JSONSchemaProvider + #cliSelectionProvider: CliSelectionProvider private constructor (settings: Settings, ctx: ExtensionContext) { this.ctx = ctx @@ -41,24 +44,27 @@ 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 + this.#cliSelectionProvider = new CliSelectionProvider(cliProvider) // Register JSON schema provider - if (ctx != null) JSONSchemaProvider.register(ctx, flowVersionProvider.state$) + this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider) // 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() @@ -79,6 +85,8 @@ export class Extension { // Called on exit async deactivate (): Promise { await this.languageServer.deactivate() - this.#testProvider?.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 new file mode 100644 index 00000000..3cfe03cc --- /dev/null +++ b/extension/src/flow-cli/cli-provider.ts @@ -0,0 +1,156 @@ +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 * as vscode from 'vscode' +import { Settings } from '../settings/settings' +import { isEqual } from 'lodash' + +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 interface CliBinary { + name: string + version: semver.SemVer +} + +interface 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.watch$(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 + }, {}) + + this.#availableBinaries$ = new StateCache(async () => { + return await this.getAvailableBinaries() + }) + + this.#currentBinary$ = new StateCache(async () => { + const name: string = this.#selectedBinaryName.getValue() + return await this.#availableBinaries[name].getValue() + }) + + // Display warning to user if binary doesn't exist (only if not using the default binary) + this.#currentBinary$.subscribe((binary) => { + if (binary === null && this.#selectedBinaryName.getValue() !== 'flow') { + void vscode.window.showErrorMessage(`The configured Flow CLI binary "${this.#selectedBinaryName.getValue()}" does not exist. Please check your settings.`) + } + }) + + this.#watchForBinaryChanges() + } + + #watchForBinaryChanges (): void { + // 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)) { + 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) + }) + }).pipe(distinctUntilChanged(isEqual)) + } + + 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$.pipe(distinctUntilChanged(isEqual)) + } + + async getCurrentBinary (): Promise { + return await this.#currentBinary$.getValue() + } + + 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) +} + +export function parseFlowCliVersion (buffer: Buffer | string): string { + return (buffer.toString().split('\n')[0]).split(' ')[1] +} 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..53637926 --- /dev/null +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -0,0 +1,155 @@ +import { zip } from 'rxjs' +import { CliBinary, CliProvider } from './cli-provider' +import { SemVer } from 'semver' +import * as vscode from 'vscode' + +const CHANGE_CADENCE_VERSION = 'cadence.changeCadenceVersion' +const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g +// label with icon +const GET_BINARY_LABEL = (version: SemVer): string => `Flow CLI v${version.format()}` + +export class CliSelectionProvider { + #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 + this.#disposables.push(vscode.commands.registerCommand(CHANGE_CADENCE_VERSION, async () => { + this.#cliProvider.refresh() + await this.#toggleSelector(true) + })) + + // Register UI components + zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => { + void this.#refreshSelector() + }) + this.#cliProvider.currentBinary$.subscribe((binary) => { + this.#statusBarItem?.dispose() + this.#statusBarItem = this.#createStatusBarItem(binary?.version ?? null) + this.#statusBarItem.show() + }) + } + + #createStatusBarItem (version: SemVer | null): vscode.StatusBarItem { + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) + statusBarItem.command = CHANGE_CADENCE_VERSION + statusBarItem.color = new vscode.ThemeColor('statusBar.foreground') + statusBarItem.tooltip = 'Click to change the Flow CLI version' + + if (version != null) { + statusBarItem.text = GET_BINARY_LABEL(version) + } else { + statusBarItem.text = '$(error) Flow CLI not found' + statusBarItem.color = new vscode.ThemeColor('errorForeground') + } + + 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.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + openLabel: 'Choose a Flow CLI binary' + }).then((uri) => { + if (uri != null) { + void this.#cliProvider.setCurrentBinary(uri[0].fsPath) + } + }) + } else if (selected instanceof AvailableBinaryItem) { + void 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) + 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()) + this.#statusBarItem?.dispose() + this.#versionSelector?.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/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..a423f7a6 100644 --- a/extension/src/json-schema-provider.ts +++ b/extension/src/json-schema-provider.ts @@ -2,68 +2,67 @@ 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` // 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 +75,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 +86,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/language-server.ts b/extension/src/server/language-server.ts index f643b07b..1973cd34 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -3,36 +3,50 @@ 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 } 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 } + // Activates the language server manager + // This will control the lifecycle of the language server + // & restart it when necessary async activate (): Promise { if (this.isActive) return await this.deactivate() this.#isActive = true - await this.startClient() + this.#subscribeToConfigChanges() this.#subscribeToSettingsChanges() + this.#subscribeToBinaryChanges() + + // Report error, but an error starting is non-terminal + // The server will be restarted if conditions change which make it possible + // (e.g. a new binary is selected, or the config file is created) + await this.startClient().catch((e) => { + console.error(e) + }) } async deactivate (): Promise { @@ -58,10 +72,15 @@ 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 } @@ -72,7 +91,7 @@ export class LanguageServerAPI { 'cadence', 'Cadence', { - command: this.settings.getSettings().flowCommand, + command: binaryPath, args: ['cadence', 'language-server', '--enable-flow-client=false'], options: { env @@ -99,20 +118,17 @@ export class LanguageServerAPI { void window.showErrorMessage(`Cadence language server failed to start: ${err.message}`) }) } catch (e) { - await this.client?.stop() - this.clientState$.next(State.Stopped) + await this.stopClient() throw e } } async stopClient (): Promise { - // Prevent stopping multiple times (important since LanguageClient state may be startFailed) - if (this.clientState$.getValue() === State.Stopped) return - // Set emulator state to disconnected this.clientState$.next(State.Stopped) await this.client?.stop() + await this.client?.dispose() this.client = null } @@ -122,34 +138,50 @@ export class LanguageServerAPI { } #subscribeToConfigChanges (): void { - const tryReloadConfig = (): void => { void this.#sendRequest(RELOAD_CONFIGURATION).catch(() => {}) } + const tryReloadConfig = (): void => { + void this.#sendRequest(RELOAD_CONFIGURATION).catch((e: any) => { + void window.showErrorMessage(`Failed to reload configuration: ${String(e)}`) + }) + } - this.#subscriptions.push(this.config.fileModified$.subscribe(() => { + this.#subscriptions.push(this.#config.fileModified$.subscribe(function notify (this: LanguageServerAPI): void { // Reload configuration if (this.clientState$.getValue() === State.Running) { tryReloadConfig() } else if (this.clientState$.getValue() === State.Starting) { // Wait for client to connect void firstValueFrom(this.clientState$.pipe(filter((state) => state === State.Running))).then(() => { - tryReloadConfig() + notify.call(this) }) + } else { + // Start client + void this.startClient() } - })) + }.bind(this))) - this.#subscriptions.push(this.config.pathChanged$.subscribe(() => { + this.#subscriptions.push(this.#config.pathChanged$.subscribe(() => { // Restart client void this.restart() })) } #subscribeToSettingsChanges (): void { - const onChange = (): void => { + // Subscribe to changes in the flowCommand setting to restart the client + // Skip the first value since we don't want to restart the client when it's first initialized + this.#settings.watch$((config) => config.flowCommand).pipe(skip(1)).subscribe(() => { // Restart client 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)) + #subscribeToBinaryChanges (): void { + // Subscribe to changes in the selected binary to restart the client + // Skip the first value since we don't want to restart the client when it's first initialized + const subscription = this.#cliProvider.currentBinary$.pipe(skip(1)).subscribe(() => { + // Restart client + void this.restart() + }) + 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..e759e068 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 { BehaviorSubject, Observable, distinctUntilChanged, map } from 'rxjs' +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()) } })) @@ -38,27 +40,41 @@ export class Settings implements Disposable { * settings.watch$(config => config.flowCommand) */ watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { - return this.#configuration$.asObservable().pipe( - skip(1), + 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 } + async updateSettings (config: Partial, target?: ConfigurationTarget): Promise { + // Recursively update all keys in the configuration + 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)) { + await update(newKey, value) + } else { + await workspace.getConfiguration().update(newKey, value, target) + } + })) + } + + await 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/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..3fc9f30b 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 } 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..7759781c 100644 --- a/extension/test/integration/3 - schema.test.ts +++ b/extension/test/integration/3 - schema.test.ts @@ -2,32 +2,40 @@ 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' 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[] + // Mock extension path + extensionPath = path.resolve(__dirname, '../../../..') + + // Mock cli provider + mockCliProvider = { + currentBinary$: new Subject(), + getCurrentBinary: sinon.stub().callsFake(async () => ((mockFlowVersionValue != null) + ? { + name: 'flow', + version: mockFlowVersionValue + } + : null)) } as any - // Mock flow version - mockFlowVersion = new StateCache(async () => mockFlowVersionValue) - // Mock fetch (assertion is for linter workaround) originalFetch = fetch.default ;(fetch as unknown as any).default = async (url: string) => { @@ -49,43 +57,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..9a6f7a5e 100644 --- a/extension/test/mock/mockSettings.ts +++ b/extension/test/mock/mockSettings.ts @@ -1,15 +1,15 @@ -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' -export function getMockSettings (settings$: BehaviorSubject> | Partial | null = null): Settings { +export function getMockSettings (_settings$: BehaviorSubject> | Partial | null = null): Settings { const mockSettings: Settings = { getSettings, watch$ } 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'), @@ -20,10 +20,9 @@ export function getMockSettings (settings$: BehaviorSubject (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { - if (!(settings$ instanceof BehaviorSubject)) return of() + 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..b9873970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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.13", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.13.tgz", + "integrity": "sha512-+LxQEbg4BDUf88utmhpUpTyYn1zHao443aGnXIAQak9ZMt9Rtsic0Oig0OS1xyIqdDXc5uMekoC6NaiUlkT/qA==", "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" @@ -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..7ad8ac2f 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": {