From 2257363ae21238885279150bf700ff4c1be6a613 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 07:56:15 -0700 Subject: [PATCH 01/17] 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", From c0268f9bd55430fdc924bdcbf5e36435b8e9b1de Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 07:57:54 -0700 Subject: [PATCH 02/17] restore --- .github/workflows/add-issues-to-devx-project.yml | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/add-issues-to-devx-project.yml b/.github/workflows/add-issues-to-devx-project.yml index ea2b8e0c..4413cee8 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.5.0 + - uses: actions/add-to-project@v0.6.0 with: project-url: https://github.com/orgs/onflow/projects/13 github-token: ${{ secrets.GH_ACTION_FOR_PROJECTS }} diff --git a/package.json b/package.json index d1de7963..7ad8ac2f 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ ] }, "devDependencies": { - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.12", "@types/expect": "^24.3.0", "@types/glob": "^8.0.1", "@types/lodash": "^4.17.0", @@ -192,7 +192,7 @@ "@types/mocha": "^10.0.6", "@types/node": "^20.11.28", "@types/object-hash": "^3.0.6", - "@types/semver": "^7.5.7", + "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/uuid": "^9.0.8", "@types/vscode": "^1.82.0", From 55c4da9aae4839a143364c7a81916056c9ca9290 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 07:58:12 -0700 Subject: [PATCH 03/17] package lock --- package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b194a226..b9873970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "vscode-languageclient": "^9.0.1" }, "devDependencies": { - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.12", "@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.7", + "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/uuid": "^9.0.8", "@types/vscode": "^1.82.0", @@ -991,9 +991,9 @@ "dev": true }, "node_modules/@types/chai": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", - "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", + "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": { @@ -1092,9 +1092,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/sinon": { From 4a32990f0974b9bea287d8c78be0d4fb96ecd51c Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 08:00:31 -0700 Subject: [PATCH 04/17] restore schema provider --- extension/src/extension.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 0fc0f9ee..d0c3b46f 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -31,7 +31,7 @@ export class Extension { #dependencyInstaller: DependencyInstaller #commands: CommandController #testProvider: TestProvider - //#schemaProvider: JSONSchemaProvider + #schemaProvider: JSONSchemaProvider private constructor (settings: Settings, ctx: ExtensionContext) { this.ctx = ctx @@ -50,9 +50,7 @@ export class Extension { const cliSelectionProvider = new CliSelectionProvider(cliProvider) // Register JSON schema provider - if (ctx != null) { - //this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider.currentBinary$) - } + this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider) // Initialize Flow Config const flowConfig = new FlowConfig(settings) @@ -87,6 +85,6 @@ export class Extension { async deactivate (): Promise { await this.languageServer.deactivate() this.#testProvider?.dispose() - //this.#schemaProvider?.dispose() + this.#schemaProvider?.dispose() } } From 41f05f2f6783756ec1c3825d9113897836f5f32f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 08:59:38 -0700 Subject: [PATCH 05/17] format --- extension/src/extension.ts | 10 +- extension/src/flow-cli/cli-provider.ts | 22 +-- .../src/flow-cli/cli-selection-provider.ts | 145 ++++++++++++++++++ extension/src/json-schema-provider.ts | 4 +- extension/src/server/language-server.ts | 2 +- extension/src/settings/settings.ts | 18 +-- extension/src/ui/cli-selection-provider.ts | 145 ------------------ .../integration/1 - language-server.test.ts | 4 +- extension/test/integration/3 - schema.test.ts | 11 +- extension/test/mock/mockSettings.ts | 2 +- 10 files changed, 182 insertions(+), 181 deletions(-) create mode 100644 extension/src/flow-cli/cli-selection-provider.ts delete mode 100644 extension/src/ui/cli-selection-provider.ts diff --git a/extension/src/extension.ts b/extension/src/extension.ts index d0c3b46f..cf48d280 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -12,7 +12,7 @@ import { TestProvider } from './test-provider/test-provider' import { StorageProvider } from './storage/storage-provider' import * as path from 'path' import { NotificationProvider } from './ui/notification-provider' -import { CliSelectionProvider } from './ui/cli-selection-provider' +import { CliSelectionProvider } from './flow-cli/cli-selection-provider' // The container for all data relevant to the extension. export class Extension { @@ -32,6 +32,7 @@ export class Extension { #commands: CommandController #testProvider: TestProvider #schemaProvider: JSONSchemaProvider + #cliSelectionProvider: CliSelectionProvider private constructor (settings: Settings, ctx: ExtensionContext) { this.ctx = ctx @@ -47,7 +48,7 @@ export class Extension { const cliProvider = new CliProvider(settings) // Register CliSelectionProvider - const cliSelectionProvider = new CliSelectionProvider(cliProvider) + this.#cliSelectionProvider = new CliSelectionProvider(cliProvider) // Register JSON schema provider this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider) @@ -84,7 +85,8 @@ export class Extension { // Called on exit async deactivate (): Promise { await this.languageServer.deactivate() - this.#testProvider?.dispose() - this.#schemaProvider?.dispose() + this.#testProvider.dispose() + this.#schemaProvider.dispose() + this.#cliSelectionProvider.dispose() } } diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 93d179e6..66aa9424 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -9,12 +9,12 @@ const KNOWN_BINS = ['flow', 'flow-c1'] const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g -export type CliBinary = { +export interface CliBinary { name: string version: semver.SemVer } -type AvailableBinariesCache = { +interface AvailableBinariesCache { [key: string]: StateCache } @@ -33,27 +33,28 @@ export class CliProvider { this.#selectedBinaryName.next(flowCommand) }) - this.#availableBinaries = KNOWN_BINS.reduce((acc, bin) => { + this.#availableBinaries = KNOWN_BINS.reduce((acc, bin) => { acc[bin] = new StateCache(async () => await this.#fetchBinaryInformation(bin)) acc[bin].subscribe(() => { this.#availableBinaries$.invalidate() }) return acc - }, {} as AvailableBinariesCache) + }, {}) this.#availableBinaries$ = new StateCache(async () => { - return this.getAvailableBinaries() + return await this.getAvailableBinaries() }) this.#currentBinary$ = new StateCache(async () => { const name: string = this.#selectedBinaryName.getValue() - return this.#availableBinaries[name].getValue() + return await this.#availableBinaries[name].getValue() }) // Subscribe to changes in the selected binary to update the caches this.#selectedBinaryName.pipe(distinctUntilChanged(), startWith(null), pairwise()).subscribe(([prev, curr]) => { // Swap out the cache for the selected binary if (prev != null && !KNOWN_BINS.includes(prev)) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.#availableBinaries[prev] } if (curr != null && !KNOWN_BINS.includes(curr)) { @@ -109,7 +110,6 @@ export class CliProvider { }) } - async getAvailableBinaries (): Promise { const bins: CliBinary[] = [] for (const name in this.#availableBinaries) { @@ -126,16 +126,16 @@ export class CliProvider { } async getCurrentBinary (): Promise { - return this.#currentBinary$.getValue() + return await this.#currentBinary$.getValue() } - setCurrentBinary (name: string): void { - this.#settings.updateSettings({ flowCommand: name }) + async setCurrentBinary (name: string): Promise { + await this.#settings.updateSettings({ flowCommand: name }) } } export function isCadenceV1Cli (version: semver.SemVer): boolean { - return CADENCE_V1_CLI_REGEX.test(version.raw) + return CADENCE_V1_CLI_REGEX.test(version.raw) } export function parseFlowCliVersion (buffer: Buffer | string): string { diff --git a/extension/src/flow-cli/cli-selection-provider.ts b/extension/src/flow-cli/cli-selection-provider.ts new file mode 100644 index 00000000..beff9022 --- /dev/null +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -0,0 +1,145 @@ +import { zip } from 'rxjs' +import { CliBinary, CliProvider } from './cli-provider' +import { SemVer } from 'semver' +import * as vscode from 'vscode' + +const TOGGLE_CADENCE_VERSION_COMMAND = 'cadence.changeCadenceVersion' +const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g +const GET_BINARY_LABEL = (version: SemVer): string => `Flow CLI v${version.format()}` + +export class CliSelectionProvider implements vscode.Disposable { + #statusBarItem: vscode.StatusBarItem | undefined + #cliProvider: CliProvider + #showSelector: boolean = false + #versionSelector: vscode.QuickPick | undefined + #disposables: vscode.Disposable[] = [] + + constructor (cliProvider: CliProvider) { + this.#cliProvider = cliProvider + + // Register the command to toggle the version + vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, async () => await this.#toggleSelector(true)) + + // Register UI components + zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => { + void this.#refreshSelector() + }) + this.#cliProvider.currentBinary$.subscribe((binary) => { + if (binary === null) return + this.#statusBarItem?.dispose() + this.#statusBarItem = this.#createStatusBarItem(binary?.version) + this.#statusBarItem.show() + }) + } + + #createStatusBarItem (version: SemVer): vscode.StatusBarItem { + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) + statusBarItem.command = TOGGLE_CADENCE_VERSION_COMMAND + statusBarItem.color = new vscode.ThemeColor('statusBar.foreground') + statusBarItem.tooltip = 'Click to change the Flow CLI version' + + // Update the status bar text when the version changes + statusBarItem.text = GET_BINARY_LABEL(version) + + return statusBarItem + } + + #createVersionSelector (currentBinary: CliBinary | null, availableBinaries: CliBinary[]): vscode.QuickPick { + const versionSelector = vscode.window.createQuickPick() + versionSelector.title = 'Select a Flow CLI version' + + // Update selected binary when the user selects a version + this.#disposables.push(versionSelector.onDidAccept(async () => { + if (versionSelector.selectedItems.length === 0) return + await this.#toggleSelector(false) + + const selected = versionSelector.selectedItems[0] + + if (selected instanceof CustomBinaryItem) { + void vscode.window.showInputBox({ + placeHolder: 'Enter the path to the Flow CLI binary', + prompt: 'Enter the path to the Flow CLI binary' + }).then((path) => { + if (path != null) { + this.#cliProvider.setCurrentBinary(path) + } + }) + } else if (selected instanceof AvailableBinaryItem) { + this.#cliProvider.setCurrentBinary(selected.path) + } + })) + + // Update available versions + const items: Array = availableBinaries.map(binary => new AvailableBinaryItem(binary)) + items.push(new CustomBinaryItem()) + versionSelector.items = items + + // Select the current binary + if (currentBinary !== null) { + const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name) + console.log(currentBinaryItem) + if (currentBinaryItem != null) { + versionSelector.selectedItems = [currentBinaryItem] + } + } + + return versionSelector + } + + async #toggleSelector (show: boolean): Promise { + this.#showSelector = show + await this.#refreshSelector() + } + + async #refreshSelector (): Promise { + if (this.#showSelector) { + this.#versionSelector?.dispose() + const currentBinary = await this.#cliProvider.getCurrentBinary() + const availableBinaries = await this.#cliProvider.getAvailableBinaries() + this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries) + this.#disposables.push(this.#versionSelector) + this.#versionSelector.show() + } else { + this.#versionSelector?.dispose() + } + } + + dispose (): void { + this.#disposables.forEach(disposable => disposable.dispose()) + } +} + +class AvailableBinaryItem implements vscode.QuickPickItem { + detail?: string + picked?: boolean + alwaysShow?: boolean + #binary: CliBinary + + constructor (binary: CliBinary) { + this.#binary = binary + } + + get label (): string { + return GET_BINARY_LABEL(this.#binary.version) + } + + get description (): string { + return `(${this.#binary.name})` + } + + get path (): string { + return this.#binary.name + } +} + +class CustomBinaryItem implements vscode.QuickPickItem { + label: string + + constructor () { + this.label = 'Choose a custom version...' + } +} + +export function isCliCadenceV1 (version: SemVer): boolean { + return CADENCE_V1_CLI_REGEX.test(version.raw) +} diff --git a/extension/src/json-schema-provider.ts b/extension/src/json-schema-provider.ts index 42e96b38..a423f7a6 100644 --- a/extension/src/json-schema-provider.ts +++ b/extension/src/json-schema-provider.ts @@ -8,8 +8,6 @@ import { CliProvider } from './flow-cli/cli-provider' const CADENCE_SCHEMA_URI = 'cadence-schema' const GET_FLOW_SCHEMA_URL = (version: string): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version}/flowkit/schema.json` -const LOCAL_SCHEMA_KEY = 'local' - // This class provides the JSON schema for the flow.json file // It is accessible via the URI scheme "cadence-schema:///flow.json" export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Disposable { @@ -52,7 +50,7 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis return await this.getLocalSchema() }) } - + return await this.#schemaCache[version] } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index fa103a09..11af7b68 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -74,7 +74,7 @@ export class LanguageServerAPI { exec('killall dlv') // Required when running language server locally on mac } catch (err) { void err } } - + const env = await envVars.getValue() this.client = new LanguageClient( 'cadence', diff --git a/extension/src/settings/settings.ts b/extension/src/settings/settings.ts index aa26740a..771bafb3 100644 --- a/extension/src/settings/settings.ts +++ b/extension/src/settings/settings.ts @@ -1,5 +1,5 @@ /* Workspace Settings */ -import { BehaviorSubject, Observable, distinctUntilChanged, map, skip } from 'rxjs' +import { BehaviorSubject, Observable, distinctUntilChanged, map } from 'rxjs' import { workspace, Disposable, ConfigurationTarget } from 'vscode' import { isEqual } from 'lodash' @@ -54,20 +54,20 @@ export class Settings implements Disposable { return this.#configuration$.value } - updateSettings (config: Partial, target?: ConfigurationTarget): void { + async updateSettings (config: Partial, target?: ConfigurationTarget): Promise { // Recursively update all keys in the configuration - function update(section: string, obj: any) { - Object.entries(obj).forEach(([key, value]) => { - const newKey = section ? `${section}.${key}` : key + async function update (section: string, obj: any): Promise { + await Promise.all(Object.entries(obj).map(async ([key, value]) => { + const newKey = `${section}.${key}` if (typeof value === 'object' && !Array.isArray(value)) { - update(newKey, value) + await update(newKey, value) } else { - workspace.getConfiguration().update(newKey, value, target) + await workspace.getConfiguration().update(newKey, value, target) } - }) + })) } - update(CONFIGURATION_KEY, config) + await update(CONFIGURATION_KEY, config) } dispose (): void { diff --git a/extension/src/ui/cli-selection-provider.ts b/extension/src/ui/cli-selection-provider.ts deleted file mode 100644 index cae13a6d..00000000 --- a/extension/src/ui/cli-selection-provider.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { BehaviorSubject, first, zip } from "rxjs"; -import * as vscode from 'vscode'; -import { CliBinary, CliProvider } from "../flow-cli/cli-provider"; -import { SemVer } from "semver"; - -const TOGGLE_CADENCE_VERSION_COMMAND = "cadence.changeCadenceVersion"; -const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g; -const GET_BINARY_LABEL = (binary: CliBinary): string => `Flow CLI ${binary.version}`; - -export class CliSelectionProvider implements vscode.Disposable { - #statusBarItem: vscode.StatusBarItem | undefined; - #cliProvider: CliProvider; - #showSelector: boolean = false; - #versionSelector: vscode.QuickPick | undefined; - #disposables: vscode.Disposable[] = []; - - constructor(cliProvider: CliProvider) { - this.#cliProvider = cliProvider; - - // Register the command to toggle the version - vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, () => this.#toggleSelector(true)); - - // Register UI components - zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => { - void this.#refreshSelector(); - }) - this.#cliProvider.currentBinary$.subscribe((binary) => { - if (binary === null) return; - this.#statusBarItem?.dispose(); - this.#statusBarItem = this.#createStatusBarItem(binary?.version); - this.#statusBarItem.show(); - }) - } - - #createStatusBarItem(version: SemVer): vscode.StatusBarItem { - const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); - statusBarItem.command = TOGGLE_CADENCE_VERSION_COMMAND; - statusBarItem.color = new vscode.ThemeColor("statusBar.foreground"); - statusBarItem.tooltip = "Click to change the Flow CLI version"; - - // Update the status bar text when the version changes - statusBarItem.text = `Flow CLI v${version}`; - - return statusBarItem; - } - - #createVersionSelector(currentBinary: CliBinary | null, availableBinaries: CliBinary[]): vscode.QuickPick { - const versionSelector = vscode.window.createQuickPick(); - versionSelector.title = "Select a Flow CLI version"; - - // Update selected binary when the user selects a version - this.#disposables.push(versionSelector.onDidAccept(() => { - if (versionSelector.selectedItems.length === 0) return; - this.#toggleSelector(false); - - const selected = versionSelector.selectedItems[0]; - - if (selected instanceof CustomBinaryItem) { - void vscode.window.showInputBox({ - placeHolder: "Enter the path to the Flow CLI binary", - prompt: "Enter the path to the Flow CLI binary" - }).then((path) => { - if (path) { - this.#cliProvider.setCurrentBinary(path); - } - }); - } else if (selected instanceof AvailableBinaryItem) { - this.#cliProvider.setCurrentBinary(selected.path); - } - })); - - // Update available versions - const items: (AvailableBinaryItem | CustomBinaryItem)[] = availableBinaries.map(binary => new AvailableBinaryItem(binary)); - items.push(new CustomBinaryItem()); - versionSelector.items = items; - - // Select the current binary - if (currentBinary !== null) { - const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name); - console.log(currentBinaryItem) - if (currentBinaryItem) { - versionSelector.selectedItems = [currentBinaryItem]; - } - } - - return versionSelector; - } - - async #toggleSelector(show: boolean) { - this.#showSelector = show; - await this.#refreshSelector(); - } - - async #refreshSelector() { - if (this.#showSelector) { - this.#versionSelector?.dispose(); - const currentBinary = await this.#cliProvider.getCurrentBinary(); - const availableBinaries = await this.#cliProvider.getAvailableBinaries(); - this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries); - this.#disposables.push(this.#versionSelector); - this.#versionSelector.show(); - } else { - this.#versionSelector?.dispose(); - } - } - - dispose() { - this.#disposables.forEach(disposable => disposable.dispose()); - } -} - -class AvailableBinaryItem implements vscode.QuickPickItem { - detail?: string; - picked?: boolean; - alwaysShow?: boolean; - #binary: CliBinary; - - constructor(binary: CliBinary) { - this.#binary = binary; - } - - get label(): string { - return GET_BINARY_LABEL(this.#binary); - } - - get description(): string { - return `(${this.#binary.name})`; - } - - get path(): string { - return this.#binary.name; - } -} - -class CustomBinaryItem implements vscode.QuickPickItem { - label: string; - - constructor() { - this.label = "Choose a custom version..."; - } -} - -export function isCliCadenceV1(version: SemVer): boolean { - return CADENCE_V1_CLI_REGEX.test(version.raw); -} \ No newline at end of file diff --git a/extension/test/integration/1 - language-server.test.ts b/extension/test/integration/1 - language-server.test.ts index 8eab23ba..3fc9f30b 100644 --- a/extension/test/integration/1 - language-server.test.ts +++ b/extension/test/integration/1 - language-server.test.ts @@ -8,7 +8,7 @@ import { MaxTimeout } from '../globals' import { BehaviorSubject, Subject } from 'rxjs' import { State } from 'vscode-languageclient' import * as sinon from 'sinon' -import { CliBinary, CliProvider } from '../../src/flow-cli/cli-provider' +import { CliBinary } from '../../src/flow-cli/cli-provider' import { SemVer } from 'semver' suite('Language Server & Emulator Integration', () => { @@ -40,7 +40,7 @@ suite('Language Server & Emulator Integration', () => { currentBinary$: cliBinary$, getCurrentBinary: sinon.stub().callsFake(async () => cliBinary$.getValue()) } as any - + LS = new LanguageServerAPI(settings, mockCliProvider, mockConfig) await LS.activate() }) diff --git a/extension/test/integration/3 - schema.test.ts b/extension/test/integration/3 - schema.test.ts index 26540431..7759781c 100644 --- a/extension/test/integration/3 - schema.test.ts +++ b/extension/test/integration/3 - schema.test.ts @@ -2,7 +2,6 @@ import { MaxTimeout } from '../globals' import { before, after } from 'mocha' import * as assert from 'assert' import * as vscode from 'vscode' -import { StateCache } from '../../src/utils/state-cache' import { SemVer } from 'semver' import { JSONSchemaProvider } from '../../src/json-schema-provider' import * as fetch from 'node-fetch' @@ -29,10 +28,12 @@ suite('JSON schema tests', () => { // Mock cli provider mockCliProvider = { currentBinary$: new Subject(), - getCurrentBinary: sinon.stub().callsFake(async () => (mockFlowVersionValue ? { - name: 'flow', - version: mockFlowVersionValue - } : null)) + getCurrentBinary: sinon.stub().callsFake(async () => ((mockFlowVersionValue != null) + ? { + name: 'flow', + version: mockFlowVersionValue + } + : null)) } as any // Mock fetch (assertion is for linter workaround) diff --git a/extension/test/mock/mockSettings.ts b/extension/test/mock/mockSettings.ts index 31bee920..9c322ec8 100644 --- a/extension/test/mock/mockSettings.ts +++ b/extension/test/mock/mockSettings.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject, Observable, of, map, distinctUntilChanged, skip } from 'rxjs' +import { BehaviorSubject, Observable, of, map, distinctUntilChanged } from 'rxjs' import { CadenceConfiguration, Settings } from '../../src/settings/settings' import * as path from 'path' import { isEqual } from 'lodash' From 31adac58c70452f888bf20417cbc7e369f7b0d46 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 19 Mar 2024 09:13:04 -0700 Subject: [PATCH 06/17] add custom picker --- extension/src/flow-cli/cli-selection-provider.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/extension/src/flow-cli/cli-selection-provider.ts b/extension/src/flow-cli/cli-selection-provider.ts index beff9022..02b3de2d 100644 --- a/extension/src/flow-cli/cli-selection-provider.ts +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -56,16 +56,18 @@ export class CliSelectionProvider implements vscode.Disposable { const selected = versionSelector.selectedItems[0] if (selected instanceof CustomBinaryItem) { - void vscode.window.showInputBox({ - placeHolder: 'Enter the path to the Flow CLI binary', - prompt: 'Enter the path to the Flow CLI binary' - }).then((path) => { - if (path != null) { - this.#cliProvider.setCurrentBinary(path) + 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) { - this.#cliProvider.setCurrentBinary(selected.path) + void this.#cliProvider.setCurrentBinary(selected.path) } })) From 8eed6d8b7cf5d73ec99704df4f715332153071c8 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 11:13:08 -0700 Subject: [PATCH 07/17] fixup --- extension/src/flow-cli/cli-provider.ts | 8 +++++++ .../src/flow-cli/cli-selection-provider.ts | 24 ++++++++++++------- extension/src/server/language-server.ts | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 66aa9424..86c04338 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -2,6 +2,7 @@ import { BehaviorSubject, Observable, distinctUntilChanged, pairwise, startWith 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' const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version` @@ -50,6 +51,13 @@ export class CliProvider { return await this.#availableBinaries[name].getValue() }) + // Display warning to user if binary doesn't exist + this.#currentBinary$.subscribe((binary) => { + if (binary === null) { + void vscode.window.showErrorMessage(`The configured Flow CLI binary "${this.#selectedBinaryName.getValue()}" does not exist. Please check your settings.`) + } + }) + // 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 diff --git a/extension/src/flow-cli/cli-selection-provider.ts b/extension/src/flow-cli/cli-selection-provider.ts index 02b3de2d..ce6cfe35 100644 --- a/extension/src/flow-cli/cli-selection-provider.ts +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -3,8 +3,9 @@ import { CliBinary, CliProvider } from './cli-provider' import { SemVer } from 'semver' import * as vscode from 'vscode' -const TOGGLE_CADENCE_VERSION_COMMAND = 'cadence.changeCadenceVersion' +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 implements vscode.Disposable { @@ -18,28 +19,34 @@ export class CliSelectionProvider implements vscode.Disposable { this.#cliProvider = cliProvider // Register the command to toggle the version - vscode.commands.registerCommand(TOGGLE_CADENCE_VERSION_COMMAND, async () => await this.#toggleSelector(true)) + 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) => { - if (binary === null) return this.#statusBarItem?.dispose() - this.#statusBarItem = this.#createStatusBarItem(binary?.version) + this.#statusBarItem = this.#createStatusBarItem(binary?.version ?? null) this.#statusBarItem.show() }) } - #createStatusBarItem (version: SemVer): vscode.StatusBarItem { + #createStatusBarItem (version: SemVer | null): vscode.StatusBarItem { const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) - statusBarItem.command = TOGGLE_CADENCE_VERSION_COMMAND + statusBarItem.command = CHANGE_CADENCE_VERSION statusBarItem.color = new vscode.ThemeColor('statusBar.foreground') statusBarItem.tooltip = 'Click to change the Flow CLI version' - // Update the status bar text when the version changes - statusBarItem.text = GET_BINARY_LABEL(version) + if (version) { + statusBarItem.text = GET_BINARY_LABEL(version) + } else { + statusBarItem.text = '$(error) Flow CLI not found' + statusBarItem.color = new vscode.ThemeColor('errorForeground') + } return statusBarItem } @@ -79,7 +86,6 @@ export class CliSelectionProvider implements vscode.Disposable { // Select the current binary if (currentBinary !== null) { const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name) - console.log(currentBinaryItem) if (currentBinaryItem != null) { versionSelector.selectedItems = [currentBinaryItem] } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 11af7b68..73cf266c 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -33,7 +33,7 @@ export class LanguageServerAPI { await this.deactivate() this.#isActive = true - await this.startClient() + await this.startClient().catch(() => {}) this.#subscribeToConfigChanges() this.#subscribeToSettingsChanges() } From 0d3ea35c96672a8d80d6da63ac251a0cdf5131b6 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 11:14:40 -0700 Subject: [PATCH 08/17] format --- extension/src/flow-cli/cli-selection-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/flow-cli/cli-selection-provider.ts b/extension/src/flow-cli/cli-selection-provider.ts index ce6cfe35..b8ea993a 100644 --- a/extension/src/flow-cli/cli-selection-provider.ts +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -41,7 +41,7 @@ export class CliSelectionProvider implements vscode.Disposable { statusBarItem.color = new vscode.ThemeColor('statusBar.foreground') statusBarItem.tooltip = 'Click to change the Flow CLI version' - if (version) { + if (version != null) { statusBarItem.text = GET_BINARY_LABEL(version) } else { statusBarItem.text = '$(error) Flow CLI not found' From 4822e2fedf3882635332eafbca5a0a84d3799f0b Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 11:28:51 -0700 Subject: [PATCH 09/17] fixup --- extension/src/server/language-server.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 73cf266c..322a45eb 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -130,7 +130,11 @@ 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: ${e}`) + }) + } this.#subscriptions.push(this.#config.fileModified$.subscribe(() => { // Reload configuration @@ -141,6 +145,11 @@ export class LanguageServerAPI { void firstValueFrom(this.clientState$.pipe(filter((state) => state === State.Running))).then(() => { tryReloadConfig() }) + } else { + // Start client + void this.startClient().then(() => { + tryReloadConfig() + }) } })) From 8713419d29c7da52b084bb943eefe88af93d2fd7 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 11:46:52 -0700 Subject: [PATCH 10/17] fixup --- extension/src/flow-cli/cli-provider.ts | 4 ++++ extension/src/flow-cli/cli-selection-provider.ts | 8 +++++--- extension/src/server/language-server.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 86c04338..8b6712e8 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -58,6 +58,10 @@ export class CliProvider { } }) + 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 diff --git a/extension/src/flow-cli/cli-selection-provider.ts b/extension/src/flow-cli/cli-selection-provider.ts index b8ea993a..53637926 100644 --- a/extension/src/flow-cli/cli-selection-provider.ts +++ b/extension/src/flow-cli/cli-selection-provider.ts @@ -8,7 +8,7 @@ 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 implements vscode.Disposable { +export class CliSelectionProvider { #statusBarItem: vscode.StatusBarItem | undefined #cliProvider: CliProvider #showSelector: boolean = false @@ -19,10 +19,10 @@ export class CliSelectionProvider implements vscode.Disposable { this.#cliProvider = cliProvider // Register the command to toggle the version - vscode.commands.registerCommand(CHANGE_CADENCE_VERSION, async () => { + 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(() => { @@ -114,6 +114,8 @@ export class CliSelectionProvider implements vscode.Disposable { dispose (): void { this.#disposables.forEach(disposable => disposable.dispose()) + this.#statusBarItem?.dispose() + this.#versionSelector?.dispose() } } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 322a45eb..a0550a75 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -132,7 +132,7 @@ export class LanguageServerAPI { #subscribeToConfigChanges (): void { const tryReloadConfig = (): void => { void this.#sendRequest(RELOAD_CONFIGURATION).catch((e: any) => { - void window.showErrorMessage(`Failed to reload configuration: ${e}`) + void window.showErrorMessage(`Failed to reload configuration: ${String(e)}`) }) } From eafa81aa776ac279baf36d5e90ae771d0ffd97fe Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 11:57:15 -0700 Subject: [PATCH 11/17] fixup --- extension/src/server/language-server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index a0550a75..21fac70a 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -136,14 +136,14 @@ export class LanguageServerAPI { }) } - 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 @@ -151,7 +151,7 @@ export class LanguageServerAPI { tryReloadConfig() }) } - })) + }.bind(this))) this.#subscriptions.push(this.#config.pathChanged$.subscribe(() => { // Restart client From 740015ebc99197fbf9b9a6d71b4464767dab4452 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 12:22:40 -0700 Subject: [PATCH 12/17] address feedback --- extension/src/server/flow-config.ts | 2 +- extension/src/server/language-server.ts | 6 ++++-- extension/src/settings/settings.ts | 4 ++-- extension/test/mock/mockSettings.ts | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/extension/src/server/flow-config.ts b/extension/src/server/flow-config.ts index c6b9de44..bb27ec43 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.settings$(config => config.customConfigPath).subscribe(() => { + return this.#settings.watch$(config => config.customConfigPath).subscribe(() => { void this.reloadConfigPath() }) } diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 21fac70a..343e9f27 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -33,9 +33,11 @@ export class LanguageServerAPI { await this.deactivate() this.#isActive = true - await this.startClient().catch(() => {}) + this.#subscribeToConfigChanges() this.#subscribeToSettingsChanges() + + await this.startClient() } async deactivate (): Promise { @@ -167,7 +169,7 @@ export class LanguageServerAPI { const subscription = zip( this.#cliProvider.currentBinary$.pipe(skip(1)), - this.#settings.settings$((config) => config.flowCommand).pipe(skip(1)) + this.#settings.watch$((config) => config.flowCommand).pipe(skip(1)) ).subscribe(onChange) this.#subscriptions.push(subscription) } diff --git a/extension/src/settings/settings.ts b/extension/src/settings/settings.ts index 771bafb3..e759e068 100644 --- a/extension/src/settings/settings.ts +++ b/extension/src/settings/settings.ts @@ -37,9 +37,9 @@ export class Settings implements Disposable { * @template T The type of the selected value * @example * // Emit whenever the flow command changes - * settings.settings$(config => config.flowCommand) + * settings.watch$(config => config.flowCommand) */ - settings$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { + watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { return this.#configuration$.pipe( map(selector), distinctUntilChanged(isEqual) diff --git a/extension/test/mock/mockSettings.ts b/extension/test/mock/mockSettings.ts index 9c322ec8..9a6f7a5e 100644 --- a/extension/test/mock/mockSettings.ts +++ b/extension/test/mock/mockSettings.ts @@ -4,7 +4,7 @@ import * as path from 'path' import { isEqual } from 'lodash' export function getMockSettings (_settings$: BehaviorSubject> | Partial | null = null): Settings { - const mockSettings: Settings = { getSettings, settings$ } as any + const mockSettings: Settings = { getSettings, watch$ } as any function getSettings (): Partial { if (!(_settings$ instanceof BehaviorSubject) && _settings$ != null) return _settings$ @@ -19,7 +19,7 @@ export function getMockSettings (_settings$: BehaviorSubject (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { + function watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { if (!(_settings$ instanceof BehaviorSubject)) return of() return _settings$.asObservable().pipe( From 883d6392980e589d00479b94760e4ee4c28447da Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 12:29:19 -0700 Subject: [PATCH 13/17] fixup --- extension/src/server/language-server.ts | 31 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 343e9f27..9b059efc 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -3,7 +3,7 @@ import { window } from 'vscode' import { Settings } from '../settings/settings' import { exec } from 'child_process' import { ExecuteCommandRequest } from 'vscode-languageclient' -import { BehaviorSubject, Subscription, filter, firstValueFrom, skip, zip } 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' @@ -28,6 +28,9 @@ export class LanguageServerAPI { 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() @@ -36,8 +39,14 @@ export class LanguageServerAPI { this.#subscribeToConfigChanges() this.#subscribeToSettingsChanges() + this.#subscribeToBinaryChanges() - await this.startClient() + // 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 { @@ -162,15 +171,21 @@ export class LanguageServerAPI { } #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() - } + }) + } - const subscription = zip( - this.#cliProvider.currentBinary$.pipe(skip(1)), - this.#settings.watch$((config) => config.flowCommand).pipe(skip(1)) - ).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) } From de10cc8db0ac7d86d010a5f4946c4fc5ba5f0bf7 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 21 Mar 2024 12:31:33 -0700 Subject: [PATCH 14/17] fixup --- extension/src/flow-cli/cli-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 8b6712e8..94a15eab 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -30,7 +30,7 @@ export class CliProvider { this.#settings = settings this.#selectedBinaryName = new BehaviorSubject(settings.getSettings().flowCommand) - this.#settings.settings$(config => config.flowCommand).subscribe((flowCommand) => { + this.#settings.watch$(config => config.flowCommand).subscribe((flowCommand) => { this.#selectedBinaryName.next(flowCommand) }) From efd29b6cb447b25cfb35f3fd645dd5531f367cf4 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 22 Mar 2024 07:21:26 -0700 Subject: [PATCH 15/17] fix LS restarting --- extension/src/server/language-server.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 9b059efc..2536ed12 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -3,10 +3,11 @@ import { window } from 'vscode' import { Settings } from '../settings/settings' import { exec } from 'child_process' import { ExecuteCommandRequest } from 'vscode-languageclient' -import { BehaviorSubject, Subscription, filter, firstValueFrom, skip } from 'rxjs' +import { BehaviorSubject, Subscription, distinctUntilChanged, filter, firstValueFrom, skip } from 'rxjs' import { envVars } from '../utils/shell/env-vars' import { FlowConfig } from './flow-config' import { CliProvider } from '../flow-cli/cli-provider' +import { isEqual } from 'lodash' // Identities for commands handled by the Language server const RELOAD_CONFIGURATION = 'cadence.server.flow.reloadConfiguration' @@ -118,20 +119,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() + this.client?.dispose() this.client = null } @@ -158,9 +156,7 @@ export class LanguageServerAPI { }) } else { // Start client - void this.startClient().then(() => { - tryReloadConfig() - }) + void this.startClient() } }.bind(this))) @@ -182,7 +178,7 @@ export class LanguageServerAPI { #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(() => { + const subscription = this.#cliProvider.currentBinary$.pipe(skip(1), distinctUntilChanged(isEqual)).subscribe(() => { // Restart client void this.restart() }) From c99db102ea4cef20ade37965f40cf54091acf91d Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 22 Mar 2024 07:31:59 -0700 Subject: [PATCH 16/17] fix error message --- extension/src/flow-cli/cli-provider.ts | 9 +++++---- extension/src/server/language-server.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/extension/src/flow-cli/cli-provider.ts b/extension/src/flow-cli/cli-provider.ts index 94a15eab..3cfe03cc 100644 --- a/extension/src/flow-cli/cli-provider.ts +++ b/extension/src/flow-cli/cli-provider.ts @@ -4,6 +4,7 @@ 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'] @@ -51,9 +52,9 @@ export class CliProvider { return await this.#availableBinaries[name].getValue() }) - // Display warning to user if binary doesn't exist + // Display warning to user if binary doesn't exist (only if not using the default binary) this.#currentBinary$.subscribe((binary) => { - if (binary === null) { + 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.`) } }) @@ -119,7 +120,7 @@ export class CliProvider { this.#availableBinaries$.subscribe((binaries) => { subscriber.next(binaries) }) - }) + }).pipe(distinctUntilChanged(isEqual)) } async getAvailableBinaries (): Promise { @@ -134,7 +135,7 @@ export class CliProvider { } get currentBinary$ (): Observable { - return this.#currentBinary$ + return this.#currentBinary$.pipe(distinctUntilChanged(isEqual)) } async getCurrentBinary (): Promise { diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 2536ed12..ed52f5de 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -129,7 +129,7 @@ export class LanguageServerAPI { this.clientState$.next(State.Stopped) await this.client?.stop() - this.client?.dispose() + await this.client?.dispose() this.client = null } @@ -178,7 +178,7 @@ export class LanguageServerAPI { #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), distinctUntilChanged(isEqual)).subscribe(() => { + const subscription = this.#cliProvider.currentBinary$.pipe(skip(1)).subscribe(() => { // Restart client void this.restart() }) From b15b68f5ad393c3bfe8a9d8ff863e93f0297fb30 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 22 Mar 2024 07:32:21 -0700 Subject: [PATCH 17/17] format --- extension/src/server/language-server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index ed52f5de..1973cd34 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -3,11 +3,10 @@ import { window } from 'vscode' import { Settings } from '../settings/settings' import { exec } from 'child_process' import { ExecuteCommandRequest } from 'vscode-languageclient' -import { BehaviorSubject, Subscription, distinctUntilChanged, filter, firstValueFrom, skip } 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' -import { isEqual } from 'lodash' // Identities for commands handled by the Language server const RELOAD_CONFIGURATION = 'cadence.server.flow.reloadConfiguration'