diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index a9f7f11a..9d1cf288 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -172,7 +172,6 @@ export const activate = async (context: ExtensionContext): Promise { eventHub = new SolutionEventHub(); completedListener = jest.fn(); eventHub.onDidConvertCompleted(completedListener); + jest.spyOn(eventHub, 'fireConfigureSolutionDataReady'); // Initialize convertRequestData with default test values convertRequestData = { @@ -137,7 +138,6 @@ describe('SolutionConverter', () => { solutionManager.getCsolution.mockReturnValue(mockCSolution); converter = new SolutionConverterImpl( - solutionManager, eventHub, mockConfigurationProvider, outputChannelProvider, @@ -281,6 +281,7 @@ describe('SolutionConverter', () => { it('run solution convert and discover layers', async () => { mockCsolutionService.convertSolution.mockResolvedValue({ success: false, undefinedLayers: ['$Board-Layer'] }); + mockCsolutionService.discoverLayers.mockResolvedValue({ success: true, configurations: [{ variables: [] }] }); await fireAndWaitForConversion(); expect(mockCsolutionService.discoverLayers).toHaveBeenCalledTimes(1); @@ -291,6 +292,29 @@ describe('SolutionConverter', () => { detection: true }) ); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledTimes(1); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledWith( + expect.objectContaining({ + availableCompilers: [], + availableConfigurations: [{ variables: [] }], + }) + ); + }); + + it('emits configure event with compilers when selectCompiler is returned', async () => { + mockCsolutionService.convertSolution.mockResolvedValue({ success: true, selectCompiler: ['GCC', 'AC6'] }); + await fireAndWaitForConversion(); + + expect(completedListener).toHaveBeenCalledWith( + expect.objectContaining({ detection: true }) + ); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledTimes(1); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledWith( + expect.objectContaining({ + availableCompilers: ['GCC', 'AC6'], + availableConfigurations: undefined, + }) + ); }); it('reports discoverLayers failure through toolsOutputMessages', async () => { diff --git a/src/solutions/solution-converter.ts b/src/solutions/solution-converter.ts index d935cef5..7943e257 100644 --- a/src/solutions/solution-converter.ts +++ b/src/solutions/solution-converter.ts @@ -19,7 +19,6 @@ import * as manifest from '../manifest'; import { ConfigurationProvider } from '../vscode-api/configuration-provider'; import { OutputChannelProvider } from '../vscode-api/output-channel-provider'; import { CmsisToolboxManager } from './cmsis-toolbox'; -import { SolutionManager } from './solution-manager'; import { CompileCommandsGenerator } from './intellisense/compile-commands-generator'; import { Mutex } from 'async-mutex'; import * as rpc from '../json-rpc/csolution-rpc-client'; @@ -39,7 +38,6 @@ export class SolutionConverterImpl implements SolutionConverter { private data: ConvertRequestData = { solutionPath: '', targetSet: '', updateRte: false, restartRpc: false }; constructor( - private readonly solutionManager: SolutionManager, private readonly eventHub: SolutionEventHub, private readonly configProvider: ConfigurationProvider, private readonly outputChannelProvider: OutputChannelProvider, @@ -143,7 +141,8 @@ export class SolutionConverterImpl implements SolutionConverter { let detection = false; let convertResult: rpc.ConvertSolutionResult = { success: false }; - const csolution = this.solutionManager.getCsolution(); + let availableCompilers: string[] = []; + let availableConfigurations: rpc.VariablesConfiguration[] | undefined; if (!missingPacksResult || missingPacksResult.success) { // rpc method: ConvertSolution outputChannel.append('Convert solution... '); @@ -160,14 +159,16 @@ export class SolutionConverterImpl implements SolutionConverter { return; } - // compilers and variables detection handling: apply select-compiler and discover layer configurations if any - csolution?.setSelectCompiler(convertResult.selectCompiler); + // compilers and variables detection: gather locally and emit configure event + availableCompilers = convertResult.selectCompiler ?? []; + detection = availableCompilers.length > 0; if (convertResult.undefinedLayers) { - const [discoverLayersDetected, discoverLayersOutput] = await this.checkDiscoverLayers(); - detection = discoverLayersDetected; + const result = await this.checkDiscoverLayers(); + const discoverLayersOutput = !result.success && result.message ? [`error csolution: ${result.message.trim()}`] : []; toolsOutputMessages = toolsOutputMessages.concat(discoverLayersOutput); + availableConfigurations = result.configurations; + detection = detection || result.success; } - detection = detection || !!convertResult.selectCompiler; } let logResult = undefined; @@ -212,6 +213,10 @@ export class SolutionConverterImpl implements SolutionConverter { logMessages: logResult, toolsOutputMessages, }); + // compilers and variables detection handling: + // apply select-compiler and discover layer configurations, reset state otherwise + this.eventHub.fireConfigureSolutionDataReady({ availableCompilers, availableConfigurations }); + } private async printErrorsWarnings(messages?: rpc.LogMessages): Promise { @@ -249,9 +254,8 @@ export class SolutionConverterImpl implements SolutionConverter { return formattedOutput; } - private async checkDiscoverLayers(): Promise<[boolean, string[]]> { + private async checkDiscoverLayers() { const outputChannel = this.outputChannelProvider.getOrCreate(manifest.CMSIS_SOLUTION_OUTPUT_CHANNEL); - this.solutionManager.getCsolution()?.setVariablesConfigurations(undefined); // rpc method: DiscoverLayers outputChannel.append('Discover Layers... '); const result = await this.cmsisToolboxManager.runCsolutionRpc( @@ -261,9 +265,7 @@ export class SolutionConverterImpl implements SolutionConverter { activeTarget: this.data?.targetSet ?? '', } ) as rpc.DiscoverLayersInfo; - this.solutionManager.getCsolution()?.setVariablesConfigurations(result.configurations); - const formattedOutput = !result.success && result.message ? [`error csolution: ${result.message.trim()}`] : []; - return [result.success, formattedOutput]; + return result; } private getSeverity(messages: rpc.LogMessages, lines?: string[]): Severity { diff --git a/src/solutions/solution-event-hub.test.ts b/src/solutions/solution-event-hub.test.ts index c65cfa0b..057dd34d 100644 --- a/src/solutions/solution-event-hub.test.ts +++ b/src/solutions/solution-event-hub.test.ts @@ -34,7 +34,7 @@ describe('EventHub', () => { it('should register emitters with context subscriptions', async () => { await eventHub.activate(mockContext); - expect(mockContext.subscriptions).toHaveLength(2); + expect(mockContext.subscriptions).toHaveLength(3); }); }); @@ -178,4 +178,30 @@ describe('EventHub', () => { expect(listener).toHaveBeenCalledTimes(2); }); }); + + describe('fireConfigureSolutionDataReady', () => { + it('should fire event with compilers and configurations', async () => { + const listener = jest.fn(); + eventHub.onDidConfigureSolutionDataReady(listener); + + const data = { availableCompilers: ['GCC', 'AC6'], availableConfigurations: undefined }; + await eventHub.fireConfigureSolutionDataReady(data); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith(data); + }); + + it('should notify multiple listeners', async () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + eventHub.onDidConfigureSolutionDataReady(listener1); + eventHub.onDidConfigureSolutionDataReady(listener2); + + const data = { availableCompilers: [], availableConfigurations: [{ variables: [] }] }; + await eventHub.fireConfigureSolutionDataReady(data); + + expect(listener1).toHaveBeenCalledWith(data); + expect(listener2).toHaveBeenCalledWith(data); + }); + }); }); diff --git a/src/solutions/solution-event-hub.ts b/src/solutions/solution-event-hub.ts index 5adbb687..de6e4613 100644 --- a/src/solutions/solution-event-hub.ts +++ b/src/solutions/solution-event-hub.ts @@ -15,7 +15,7 @@ */ import * as vscode from 'vscode'; import { constructor } from '../generic/constructor'; -import { LogMessages } from '../json-rpc/csolution-rpc-client'; +import { LogMessages, VariablesConfiguration } from '../json-rpc/csolution-rpc-client'; import { Severity } from './constants'; /** @@ -29,6 +29,14 @@ export interface ConvertRequestData { lockAbort?: boolean; } +/** + * Event data for configure solution readiness + */ +export interface ConfigureSolutionData { + availableCompilers: string[]; + availableConfigurations: VariablesConfiguration[] | undefined; +} + /** * Event data for solution conversion result */ @@ -69,6 +77,14 @@ export interface SolutionEventHub { * Event fired when solution conversion is completed */ readonly onDidConvertCompleted: vscode.Event; + /** + * Fire configure solution data ready event + */ + fireConfigureSolutionDataReady(data: ConfigureSolutionData): Promise; + /** + * Event fired when configure solution data is ready (compilers / layer configurations detected) + */ + readonly onDidConfigureSolutionDataReady: vscode.Event; } class SolutionEventHubImpl { @@ -79,9 +95,13 @@ class SolutionEventHubImpl { private readonly convertCompleteEmitter = new vscode.EventEmitter(); public readonly onDidConvertCompleted: vscode.Event = this.convertCompleteEmitter.event; + private readonly configureSolutionDataEmitter = new vscode.EventEmitter(); + public readonly onDidConfigureSolutionDataReady: vscode.Event = this.configureSolutionDataEmitter.event; + public async activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(this.convertRequestEmitter); context.subscriptions.push(this.convertCompleteEmitter); + context.subscriptions.push(this.configureSolutionDataEmitter); } public async fireConvertRequest(data: ConvertRequestData): Promise { @@ -91,6 +111,10 @@ class SolutionEventHubImpl { public async fireConvertCompleted(data: ConvertResultData): Promise { this.convertCompleteEmitter.fire(data); } + + public async fireConfigureSolutionDataReady(data: ConfigureSolutionData): Promise { + this.configureSolutionDataEmitter.fire(data); + } } export const SolutionEventHub = constructor(SolutionEventHubImpl); diff --git a/src/views/manage-layers/manage-layers-webview-main.test.ts b/src/views/manage-layers/manage-layers-webview-main.test.ts new file mode 100644 index 00000000..c2ede7e2 --- /dev/null +++ b/src/views/manage-layers/manage-layers-webview-main.test.ts @@ -0,0 +1,245 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'jest'; +import * as vscode from 'vscode'; +import { waitTimeout } from '../../__test__/test-waits'; +import { csolutionFactory, CSolutionMock } from '../../solutions/csolution.factory'; +import { SolutionEventHub } from '../../solutions/solution-event-hub'; +import { MockSolutionManager, solutionManagerFactory } from '../../solutions/solution-manager.factories'; +import { MockCommandsProvider, commandsProviderFactory } from '../../vscode-api/commands-provider.factories'; +import { MockMessageProvider, messageProviderFactory } from '../../vscode-api/message-provider.factories'; +import { getMockWebViewManager, MockWebviewManager } from '../__test__/mock-webview-manager'; +import { WebviewManager } from '../webview-manager'; +import { copyLayerToProject } from './board-layer'; +import { ManageLayersWebviewMain } from './manage-layers-webview-main'; +import * as Messages from './messages'; +import { TargetConfiguration } from './view/state/reducer'; + +jest.mock('./board-layer', () => ({ + copyLayerToProject: jest.fn().mockResolvedValue(undefined), +})); + +describe('ManageLayersWebviewMain', () => { + let manageLayersWebviewMain: ManageLayersWebviewMain; + let solutionManager: MockSolutionManager; + let csolution: CSolutionMock; + let eventHub: SolutionEventHub; + let commandsProvider: MockCommandsProvider; + let messageProvider: MockMessageProvider; + let webviewManager: MockWebviewManager; + let context: { subscriptions: vscode.Disposable[] }; + let compilerNode: { setValue: jest.Mock }; + let variablesNode: { getChildByValue: jest.Mock; createChild: jest.Mock }; + let variableItem: { setValue: jest.Mock }; + let targetTypeItem: { createChild: jest.Mock }; + let save: jest.Mock; + + const mockedCopyLayerToProject = jest.mocked(copyLayerToProject); + + const configureVariable = { + name: 'BOARD_LAYER', + clayer: 'Vendor::Board.Layer', + description: 'Board layer', + settings: [{ set: 'Debug' }], + path: '/source/layer', + file: 'board.clayer.yml', + 'copy-to': 'Config/Layer', + }; + + const configuredLayer: TargetConfiguration = { + variables: [{ + variableName: 'BOARD_LAYER', + variableValue: 'Vendor::Board.Layer', + description: 'Board layer', + settings: [{ set: 'Debug' }], + path: '/source/layer', + file: 'board.clayer.yml', + copyTo: 'Config/Layer', + copyToOrig: 'Config/Layer', + disabled: false, + }], + }; + + async function fireOutgoingMessage(message: Messages.OutgoingMessage): Promise { + webviewManager.didReceiveMessageEmitter.fire(message); + await waitTimeout(); + } + + async function fireConfigureSolutionDataReady(data: { availableCompilers: string[]; availableConfigurations: Array<{ variables: typeof configureVariable[] }> | undefined }): Promise { + await eventHub.fireConfigureSolutionDataReady(data); + await waitTimeout(); + } + + beforeEach(async () => { + compilerNode = { setValue: jest.fn() }; + variableItem = { setValue: jest.fn().mockReturnThis() }; + variablesNode = { + getChildByValue: jest.fn().mockReturnValue(undefined), + createChild: jest.fn().mockReturnValue(variableItem), + }; + targetTypeItem = { + createChild: jest.fn().mockReturnValue(variablesNode), + }; + save = jest.fn().mockResolvedValue(undefined); + + csolution = csolutionFactory({ + solutionDir: 'C:/workspace/solution', + getActiveTargetType: jest.fn().mockReturnValue('Debug'), + getDefaultTargetTypeItem: jest.fn().mockReturnValue(targetTypeItem as never), + csolutionYml: { + topItem: compilerNode, + save, + } as never, + }); + + solutionManager = solutionManagerFactory(); + solutionManager.getCsolution.mockReturnValue(csolution); + eventHub = new SolutionEventHub(); + commandsProvider = commandsProviderFactory(); + messageProvider = messageProviderFactory(); + webviewManager = getMockWebViewManager(); + context = { subscriptions: [] }; + mockedCopyLayerToProject.mockResolvedValue(undefined); + + manageLayersWebviewMain = new ManageLayersWebviewMain( + context as unknown as vscode.ExtensionContext, + commandsProvider, + messageProvider, + solutionManager, + eventHub, + webviewManager as unknown as WebviewManager, + ); + + await manageLayersWebviewMain.activate(context as unknown as vscode.ExtensionContext); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('activates and registers message/event subscriptions', () => { + expect(webviewManager.activate).toHaveBeenCalledWith(context); + expect(context.subscriptions).toHaveLength(2); + }); + + it('opens the panel and sends board layer data when configurations are available', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: [{ variables: [configureVariable] }], + }); + + expect(webviewManager.createOrShowPanel).toHaveBeenCalledTimes(1); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'BOARD_LAYER_DATA', + layers: [configuredLayer], + activeTargetType: 'Debug', + availableCompilers: ['AC6', 'GCC'], + }); + }); + + it('opens the panel and sends no-data when only multiple compilers are available', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: undefined, + }); + + expect(webviewManager.createOrShowPanel).toHaveBeenCalledTimes(1); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'BOARD_LAYER_NODATA', + activeTargetType: 'Debug', + availableCompilers: ['AC6', 'GCC'], + }); + }); + + it('auto-applies compiler when exactly one compiler is available', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6'], + availableConfigurations: undefined, + }); + + expect(compilerNode.setValue).toHaveBeenCalledWith('compiler', 'AC6'); + expect(save).toHaveBeenCalledTimes(1); + expect(webviewManager.createOrShowPanel).not.toHaveBeenCalled(); + expect(webviewManager.sendMessage).not.toHaveBeenCalled(); + }); + + it('handles CHECK_LAYER_DOES_NOT_EXIST without sending a success acknowledgement', async () => { + await fireOutgoingMessage({ + type: 'CHECK_LAYER_DOES_NOT_EXIST', + layerFolder: '__definitely_missing_layer_path__', + variableId: 7, + }); + + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'RESULT_LAYER_EXISTS_CHECK', + variableId: 7, + result: false, + }); + expect(webviewManager.sendMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'REQUEST_SUCCESSFUL' })); + }); + + it('applies layer and compiler from APPLY_CONFIGURE', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: [{ variables: [configureVariable] }], + }); + webviewManager.sendMessage.mockClear(); + save.mockClear(); + + await fireOutgoingMessage({ + type: 'APPLY_CONFIGURE', + layer: configuredLayer, + compiler: 'GCC', + }); + + expect(targetTypeItem.createChild).toHaveBeenCalledWith('variables', true); + expect(variablesNode.getChildByValue).toHaveBeenCalledWith('BOARD_LAYER'); + expect(variablesNode.createChild).toHaveBeenCalledWith('-', false); + expect(variableItem.setValue).toHaveBeenNthCalledWith(1, 'BOARD_LAYER', '$SolutionDir()$/Config/Layer/board.clayer.yml'); + expect(variableItem.setValue).toHaveBeenNthCalledWith(2, 'copied-from', 'Vendor::Board.Layer'); + expect(mockedCopyLayerToProject).toHaveBeenCalledWith(configuredLayer, 'C:/workspace/solution'); + expect(compilerNode.setValue).toHaveBeenCalledWith('compiler', 'GCC'); + expect(save).toHaveBeenCalledTimes(2); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'REQUEST_SUCCESSFUL', + requestType: 'APPLY_CONFIGURE', + }); + }); + + it('reports apply failures without sending a success acknowledgement', async () => { + mockedCopyLayerToProject.mockRejectedValueOnce(new Error('copy failed')); + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: [{ variables: [configureVariable] }], + }); + webviewManager.sendMessage.mockClear(); + + await fireOutgoingMessage({ + type: 'APPLY_CONFIGURE', + layer: configuredLayer, + compiler: 'GCC', + }); + + expect(messageProvider.showErrorMessage).toHaveBeenCalledWith('Failed to apply changes: copy failed'); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'REQUEST_FAILED', + requestType: 'APPLY_CONFIGURE', + errorMessage: 'Failed to apply changes: copy failed', + }); + expect(webviewManager.sendMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'REQUEST_SUCCESSFUL' })); + }); +}); diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index e2d49c42..13e6f6ef 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -17,16 +17,16 @@ import { existsSync } from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { CTreeItem, ITreeItem } from '../../generic/tree-item'; import * as manifest from '../../manifest'; import { SolutionManager } from '../../solutions/solution-manager'; +import { ConfigureSolutionData, SolutionEventHub } from '../../solutions/solution-event-hub'; import { CommandsProvider } from '../../vscode-api/commands-provider'; import { MessageProvider } from '../../vscode-api/message-provider'; import { WebviewManager, WebviewManagerOptions } from '../webview-manager'; import { copyLayerToProject } from './board-layer'; import * as Messages from './messages'; import { ChangeLayerMessage } from './messages'; -import { ConfigurationVariable, LayerError, TargetConfiguration, VariableSet } from './view/state/reducer'; +import { ConfigurationVariable, TargetConfiguration, VariableSet } from './view/state/reducer'; import { LayerVariable, VariablesConfiguration, SettingsType } from '../../json-rpc/csolution-rpc-client'; function mapSet(settings: SettingsType[]): VariableSet[] { @@ -70,40 +70,6 @@ function getDataBoardLayers(configurations: VariablesConfiguration[]): TargetCon return layers; } -function checkError(error: ITreeItem) { - return error.getValueAsString(); -} - -function filterError(cBuildError: ITreeItem) { - const message = cBuildError.getValueAsString(); - const found = message.indexOf('no compatible software layer found') != -1; - return found; -} - -function checkCbuildForErrors(cBuild: ITreeItem): LayerError { - return { - name: cBuild.getValueAsString('cbuild'), - project: cBuild.getValueAsString('project'), - configuration: cBuild.getValueAsString('configuration'), - messages: cBuild.findChild(['messages', 'errors'])?. - getChildren(). - filter(filterError). - map(checkError), - }; -} - -function filterErrorsTag(cBuild: ITreeItem) { - const cbuildErrors = cBuild.getValueAsString('errors') == 'true' ? true : false; - const cbuildMessages = !!cBuild.findChild(['messages', 'errors'])?.getChildren().filter(filterError).length; - return cbuildErrors && cbuildMessages; -} - -function getAvailableCbuildErrors(avCBuilds: ITreeItem[]) { - return avCBuilds?. - filter(filterErrorsTag). - map(checkCbuildForErrors); -} - export const MANAGE_LAYERS_WEBVIEW_OPTIONS: Readonly = { title: 'Configure Solution', scriptPath: path.join('dist', 'views', 'configureSolution.js'), @@ -117,27 +83,27 @@ export const MANAGE_LAYERS_WEBVIEW_OPTIONS: Readonly = { export class ManageLayersWebviewMain { private readonly webviewManager: WebviewManager; + private latestConfigureData: ConfigureSolutionData | undefined; constructor( readonly context: vscode.ExtensionContext, private readonly commandsProvider: CommandsProvider, private readonly messageProvider: MessageProvider, private readonly solutionManager: SolutionManager, + private readonly eventHub: SolutionEventHub, webviewManager?: WebviewManager, ) { this.webviewManager = webviewManager || new WebviewManager(context, MANAGE_LAYERS_WEBVIEW_OPTIONS, this.commandsProvider); - this.solutionManager.onLoadedBuildFiles((() => this.onLoadedBuildFiles()).bind(this)); } private async sendPlatform() { this.webviewManager.sendMessage({ type: 'PLATFORM', data: { name: 'vscode' } }); } - private async applyOrChangeLayer(layer: TargetConfiguration) { const csolution = this.solutionManager.getCsolution(); - if (csolution && csolution.variablesConfigurations) { + if (csolution && this.latestConfigureData?.availableConfigurations) { const activeTarget = csolution?.getActiveTargetType() || ''; const activeTargetTypeItem = csolution.getDefaultTargetTypeItem(activeTarget); if (!activeTargetTypeItem || !activeTarget) { @@ -163,8 +129,9 @@ export class ManageLayersWebviewMain { } } }); - await csolution.csolutionYml.save(); - await copyLayerToProject(layer, csolution.solutionDir); + await copyLayerToProject(layer, csolution.solutionDir); // first copy layer files + await csolution.csolutionYml.save(); // then save csolution.yml + this.latestConfigureData = undefined; } } @@ -178,10 +145,11 @@ export class ManageLayersWebviewMain { } - private async applyConfiguration(message: ChangeLayerMessage): Promise { + private async applyConfiguration(message: ChangeLayerMessage): Promise { try { await this.applyOrChangeLayer(message.layer); await this.applyOrChangeCompiler(message.compiler); + return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : error; this.messageProvider.showErrorMessage(`Failed to apply changes: ${errorMessage}`); @@ -190,47 +158,42 @@ export class ManageLayersWebviewMain { requestType: 'APPLY_CONFIGURE', errorMessage: `Failed to apply changes: ${errorMessage}` }); - return; + return false; } } public async activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push( this.webviewManager.onDidReceiveMessage(this.handleMessage, this), + this.eventHub.onDidConfigureSolutionDataReady(this.onConfigureSolutionDataReady, this), ); this.webviewManager.activate(context); } - private onLoadedBuildFiles(): void { - const csolution = this.solutionManager.getCsolution(); - const activeTarget = csolution?.getActiveTargetType() || ''; - if (!csolution) { - this.webviewManager.sendMessage({ - type: 'BOARD_LAYER_NODATA', - activeTargetType: activeTarget, - availableCompilers: [], - }); - return; - } + private onConfigureSolutionDataReady(data: ConfigureSolutionData): void { + if (data.availableCompilers.length === 0 && !data.availableConfigurations) { + this.latestConfigureData = undefined; + this.sendConfigurations(undefined, '', []); + } else { - this.getDataUserChoice(); + this.latestConfigureData = data; + this.getDataUserChoice(); + } } private showPanel(show: boolean) { - if (!show) { - return; + if (show) { + this.webviewManager.createOrShowPanel(); } - - this.webviewManager.createOrShowPanel(); } - private sendConfigurations(configurations: VariablesConfiguration[] | undefined, activeTarget: string, avComps: string[] | undefined) { + private sendConfigurations(configurations: VariablesConfiguration[] | undefined, activeTarget: string, avComps: string[]) { if (!configurations) { this.webviewManager.sendMessage({ type: 'BOARD_LAYER_NODATA', activeTargetType: activeTarget, - availableCompilers: avComps ?? [], + availableCompilers: avComps, }); return; } @@ -240,52 +203,26 @@ export class ManageLayersWebviewMain { type: 'BOARD_LAYER_DATA', layers, activeTargetType: activeTarget, - availableCompilers: avComps ?? [], + availableCompilers: avComps, }); } - private checkForLayerErrors(avCBuilds: ITreeItem[] | undefined): boolean { - if (!avCBuilds) { - return false; - } - - const layerErrors = getAvailableCbuildErrors(avCBuilds); - this.webviewManager.sendMessage({ - type: 'BOARD_LAYER_DATA_ERRORS', - layerErrors, - }); - - // send message to GUI with error strins - - return !!layerErrors?.length; - } + private getDataUserChoice(): void { + const activeTarget = this.solutionManager.getCsolution()?.getActiveTargetType() ?? ''; - private getDataUserChoice() { - const csolution = this.solutionManager.getCsolution(); - const cBuildIdxYml = csolution?.cbuildIdxYmlRoot; - if (!cBuildIdxYml) { - this.webviewManager.sendMessage({ - type: 'LOADING' - }); + if (!this.latestConfigureData) { + this.sendConfigurations(undefined, activeTarget, []); return; } + const { availableCompilers, availableConfigurations } = this.latestConfigureData; - const avComps = csolution?.selectCompiler; - const configurations = csolution?.variablesConfigurations; - const avCBuilds = cBuildIdxYml.findChild(['build-idx', 'cbuilds'])?.getChildren(); - const avErrors = this.checkForLayerErrors(avCBuilds); - const activeTarget = csolution?.getActiveTargetType() || ''; - - if (avComps?.length == 1) { // just one compiler, add it automagically - const availableCompilers = avComps; - if (availableCompilers.length) { - this.applyOrChangeCompiler(availableCompilers[0]); - return; // return here as the change triggers cbuild and another reload - } + if (availableCompilers.length === 1) { // just one compiler, add it automagically + this.applyOrChangeCompiler(availableCompilers[0]); + return; // return here as the change triggers cbuild and another reload } - this.showPanel(!!avComps || !!configurations || avErrors); - this.sendConfigurations(configurations, activeTarget, avComps); + this.showPanel(!!availableCompilers.length || !!availableConfigurations); + this.sendConfigurations(availableConfigurations, activeTarget, availableCompilers); } private localFolderExists(localPath: string): boolean { @@ -299,9 +236,10 @@ export class ManageLayersWebviewMain { private async handleMessage(message: Messages.OutgoingMessage): Promise { try { + let successful = true; switch (message.type) { case 'APPLY_CONFIGURE': - await this.applyConfiguration(message); + successful = await this.applyConfiguration(message); break; case 'GET_PLATFORM': await this.sendPlatform(); @@ -318,7 +256,9 @@ export class ManageLayersWebviewMain { return; // early exit } } - this.webviewManager.sendMessage({ type: 'REQUEST_SUCCESSFUL', requestType: message.type }); + if (successful) { + this.webviewManager.sendMessage({ type: 'REQUEST_SUCCESSFUL', requestType: message.type }); + } } catch (err) { const _err = err as Error; this.messageProvider.showErrorMessage(`Solution service failure: ${_err.message}\n${_err.stack}`);