From 3bb251f99a83f224ceed9016e9533d3c9154d7a1 Mon Sep 17 00:00:00 2001 From: Daniel Brondani Date: Fri, 26 Jun 2026 10:59:12 +0200 Subject: [PATCH] Switch run generator command to use active target-set and extend diagnostics --- src/desktop/extension.ts | 2 +- ...problem-diagnostic-action-resolver.test.ts | 37 +++- .../problem-diagnostic-action-resolver.ts | 32 +++- src/solutions/solution-event-hub.test.ts | 2 +- src/solutions/solution-event-hub.ts | 16 ++ src/solutions/solution-problems.test.ts | 5 +- src/solutions/solution-problems.ts | 14 +- src/tasks/generator/generator-command.test.ts | 162 ++++++++++++++++-- src/tasks/generator/generator-command.ts | 52 ++++-- .../solution-outline-project-items.ts | 5 +- .../solution-outline-tree.test.ts | 49 ++++++ .../USBD/CmsisViewTreeOneProjRef.txt | 2 +- test-data/solutions/USBD/CmsisViewTreeRef.txt | 4 +- 13 files changed, 334 insertions(+), 48 deletions(-) diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index 0e979494..b935887f 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -237,7 +237,7 @@ export const activate = async (context: ExtensionContext): Promise { describe('run-generator action', () => { - it('encodes generator and context in the command URI arguments (CubeMX example)', () => { + it('uses active target set name when available', () => { + const resolverWithTargetSet = new ProblemDiagnosticActionResolver(() => 'STM32C531CBT6@fvp'); + + const { command, args } = decodeCodeTarget(resolverWithTargetSet, makeContext({ + message: "cgen file was not found, run generator 'CubeMX' for context 'CubeMX.Debug+STM32C531CBT6'", + })); + + expect(command).toBe(`command:${RUN_GENERATOR_COMMAND_ID}`); + expect(args).toEqual([{ generator: 'CubeMX', activeTarget: 'STM32C531CBT6@fvp' }]); + }); + + it('encodes generator and activeTarget in the command URI arguments (CubeMX example)', () => { const { command, args } = decodeCodeTarget(resolver, makeContext({ message: "cgen file was not found, run generator 'CubeMX' for context 'CubeMX.Debug+STM32C531CBT6'", })); expect(command).toBe(`command:${RUN_GENERATOR_COMMAND_ID}`); - expect(args).toEqual([{ generator: 'CubeMX', context: 'CubeMX.Debug+STM32C531CBT6' }]); + expect(args).toEqual([{ generator: 'CubeMX', activeTarget: 'STM32C531CBT6' }]); }); it('returns a formatted message and a code', () => { @@ -157,7 +168,7 @@ describe('ProblemDiagnosticActionResolver', () => { message: "cgen file was not found, run generator 'CubeMX' for context 'MyProject.Debug+STM32'", })); - expect(result?.message).toBe("Run generator 'CubeMX' for project 'MyProject.Debug+STM32'"); + expect(result?.message).toBe("Run generator 'CubeMX' for target 'STM32'"); expect(result?.code).toBeDefined(); }); @@ -175,7 +186,25 @@ describe('ProblemDiagnosticActionResolver', () => { })); expect(command).toBe(`command:${RUN_GENERATOR_COMMAND_ID}`); - expect(args).toEqual([{ generator: 'CubeMX', context: 'MyProject.Debug+STM32' }]); + expect(args).toEqual([{ generator: 'CubeMX', activeTarget: 'STM32' }]); + }); + + it('preserves @TargetSet in activeTarget when the context string contains one', () => { + const { command, args } = decodeCodeTarget(resolver, makeContext({ + message: "cgen file was not found, run generator 'CubeMX' for context 'CubeMX.Debug+STM32C531CBT6@fvp'", + })); + + expect(command).toBe(`command:${RUN_GENERATOR_COMMAND_ID}`); + expect(args).toEqual([{ generator: 'CubeMX', activeTarget: 'STM32C531CBT6@fvp' }]); + }); + + it('returns a message with TargetType@Set when the context string contains a target set', () => { + const result = resolver.resolve(makeContext({ + message: "cgen file was not found, run generator 'CubeMX' for context 'MyProject.Debug+STM32@board'", + })); + + expect(result?.message).toBe("Run generator 'CubeMX' for target 'STM32@board'"); + expect(result?.code).toBeDefined(); }); }); diff --git a/src/solutions/problem-diagnostic-action-resolver.ts b/src/solutions/problem-diagnostic-action-resolver.ts index 108f2765..1f707b83 100644 --- a/src/solutions/problem-diagnostic-action-resolver.ts +++ b/src/solutions/problem-diagnostic-action-resolver.ts @@ -25,6 +25,7 @@ import { RUN_GENERATOR_COMMAND_ID, } from '../manifest'; import { stripVendor, stripVersion } from '../utils/string-utils'; +import { contextDescriptorFromString } from './descriptors/descriptors'; type MergeUpdateLevel = 'recommended' | 'suggested' | 'required'; @@ -52,7 +53,7 @@ export interface ProblemDiagnosticActionResult { type ProblemActionDescriptor = | { kind: 'merge'; localPath: string; updateLevel: MergeUpdateLevel; componentId?: string } - | { kind: 'run-generator'; generator: string; context: string } + | { kind: 'run-generator'; generator: string; activeTarget: string } | { kind: 'manage-components'; query: string } | { kind: 'manage-pack'; packId: string } | { kind: 'open-environment-variables-settings' } @@ -95,6 +96,19 @@ const queryActionPatterns: ReadonlyArray<{ pattern: RegExp; action: 'components- ]; export class ProblemDiagnosticActionResolver { + /** + * Optional provider for the active target set name + * If not provided, falls back to the target type parsed from the diagnostic message. + * + * @param getActiveTargetSetName - Function returning the current active target set name, + * or undefined if no target set is active. Called lazily + * only when resolving generator diagnostics. + */ + constructor( + private readonly getActiveTargetSetName?: () => string | undefined, + ) { + } + public resolve(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined { const descriptor = this.resolveDescriptor(context); if (!descriptor) { @@ -125,10 +139,10 @@ export class ProblemDiagnosticActionResolver { if (descriptor.kind === 'run-generator') { return { - message: `Run generator '${descriptor.generator}' for project '${descriptor.context}'`, + message: `Run generator '${descriptor.generator}' for target '${descriptor.activeTarget || '""'}'`, code: { value: 'Run Generator', - target: this.createRunGeneratorCommandUri(descriptor.generator, descriptor.context), + target: this.createRunGeneratorCommandUri(descriptor.generator, descriptor.activeTarget), }, }; } @@ -200,7 +214,7 @@ export class ProblemDiagnosticActionResolver { return { kind: 'run-generator', generator: request.generator, - context: request.context, + activeTarget: request.activeTarget, }; } @@ -264,7 +278,7 @@ export class ProblemDiagnosticActionResolver { }; } - private parseGeneratorRequest(message: string): { generator: string; context: string } | undefined { + private parseGeneratorRequest(message: string): { generator: string; activeTarget: string } | undefined { const normalizedMessage = this.normalizeMessageForPatternMatching(message); for (const pattern of generatorMissingPatterns) { @@ -274,7 +288,9 @@ export class ProblemDiagnosticActionResolver { } const [, generator, context] = match; - return { generator, context }; + const targetType = contextDescriptorFromString(context).targetType; + const activeTarget = this.getActiveTargetSetName?.() ?? targetType; + return { generator, activeTarget }; } return undefined; @@ -342,8 +358,8 @@ export class ProblemDiagnosticActionResolver { return vscode.Uri.parse(`command:${MERGE_FILE_COMMAND_ID}?${args}`); } - private createRunGeneratorCommandUri(generator: string, context: string): vscode.Uri { - const args = this.encodeCommandArgs([{ generator, context }]); + private createRunGeneratorCommandUri(generator: string, activeTarget: string): vscode.Uri { + const args = this.encodeCommandArgs([{ generator, activeTarget }]); return vscode.Uri.parse(`command:${RUN_GENERATOR_COMMAND_ID}?${args}`); } diff --git a/src/solutions/solution-event-hub.test.ts b/src/solutions/solution-event-hub.test.ts index 8b3b9e6a..43b055f1 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(5); + expect(mockContext.subscriptions).toHaveLength(6); }); }); diff --git a/src/solutions/solution-event-hub.ts b/src/solutions/solution-event-hub.ts index 2ffa41a3..24729651 100644 --- a/src/solutions/solution-event-hub.ts +++ b/src/solutions/solution-event-hub.ts @@ -100,6 +100,14 @@ export interface SolutionEventHub { * Event fired when cbuild setup is requested */ readonly onDidCbuildSetupRequested: vscode.Event; + /** + * Fire generator run completion event + */ + fireGeneratorRunCompleted(data: CbuildResultData): Promise; + /** + * Event fired when generator run is completed + */ + readonly onDidGeneratorRunCompleted: vscode.Event; /** * Fire configure solution data ready event */ @@ -124,6 +132,9 @@ class SolutionEventHubImpl { private readonly cbuildSetupRequestEmitter = new vscode.EventEmitter(); public readonly onDidCbuildSetupRequested: vscode.Event = this.cbuildSetupRequestEmitter.event; + private readonly generatorRunCompleteEmitter = new vscode.EventEmitter(); + public readonly onDidGeneratorRunCompleted: vscode.Event = this.generatorRunCompleteEmitter.event; + private readonly configureSolutionDataEmitter = new vscode.EventEmitter(); public readonly onDidConfigureSolutionDataReady: vscode.Event = this.configureSolutionDataEmitter.event; @@ -132,6 +143,7 @@ class SolutionEventHubImpl { context.subscriptions.push(this.convertCompleteEmitter); context.subscriptions.push(this.cbuildCompleteEmitter); context.subscriptions.push(this.cbuildSetupRequestEmitter); + context.subscriptions.push(this.generatorRunCompleteEmitter); context.subscriptions.push(this.configureSolutionDataEmitter); } @@ -151,6 +163,10 @@ class SolutionEventHubImpl { this.cbuildSetupRequestEmitter.fire(); } + public async fireGeneratorRunCompleted(data: CbuildResultData): Promise { + this.generatorRunCompleteEmitter.fire(data); + } + public async fireConfigureSolutionDataReady(data: ConfigureSolutionData): Promise { this.configureSolutionDataEmitter.fire(data); } diff --git a/src/solutions/solution-problems.test.ts b/src/solutions/solution-problems.test.ts index 6a5c452c..319dac1a 100644 --- a/src/solutions/solution-problems.test.ts +++ b/src/solutions/solution-problems.test.ts @@ -42,6 +42,7 @@ const buildCsolution = () => { ['ctx', { fileName: '/work/ctx.cbuild.yml' }], ]), }, + getActiveTargetSetName: () => undefined, }; }; @@ -70,7 +71,7 @@ describe('SolutionProblems', () => { await solutionProblems.activate(context); - expect(context.subscriptions).toHaveLength(5); + expect(context.subscriptions).toHaveLength(6); }); it('clears diagnostics when solution path changes', async () => { @@ -366,7 +367,7 @@ describe('SolutionProblems', () => { const code = runGeneratorDiagnostics[0].code as { value: string; target: vscode.Uri }; const [command, args] = code.target.toString().split('?'); expect(command).toBe(`command:${RUN_GENERATOR_COMMAND_ID}`); - expect(JSON.parse(decodeURIComponent(args))).toEqual([{ generator: 'CubeMX2', context: 'CubeMX2.Debug+STM32C531CBT6' }]); + expect(JSON.parse(decodeURIComponent(args))).toEqual([{ generator: 'CubeMX2', activeTarget: 'STM32C531CBT6' }]); }); it('falls back to the diagnostic file path for relative merge paths', async () => { diff --git a/src/solutions/solution-problems.ts b/src/solutions/solution-problems.ts index d1d3e8b2..70fcc44a 100644 --- a/src/solutions/solution-problems.ts +++ b/src/solutions/solution-problems.ts @@ -233,7 +233,7 @@ export class SolutionProblemsImpl implements SolutionProblems { private readonly diagnosticCollection: vscode.DiagnosticCollection = vscode.languages.createDiagnosticCollection('csolution'); private readonly environmentDiagnosticCollection: vscode.DiagnosticCollection = vscode.languages.createDiagnosticCollection('csolution-environment'); - private readonly diagnosticActionResolver = new ProblemDiagnosticActionResolver(); + private readonly diagnosticActionResolver: ProblemDiagnosticActionResolver; private readonly environmentVariablesSetting = '"cmsis-csolution.environmentVariables"'; /** * source files for diagnostics mapping @@ -244,12 +244,16 @@ export class SolutionProblemsImpl implements SolutionProblems { private readonly solutionManager: SolutionManager, private readonly eventHub: SolutionEventHub, ) { + this.diagnosticActionResolver = new ProblemDiagnosticActionResolver( + () => this.solutionManager.getCsolution()?.getActiveTargetSetName(), + ); } public async activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push( this.eventHub.onDidConvertCompleted(this.handleConvertCompleted, this), this.eventHub.onDidCbuildCompleted(this.handleCbuildCompleted, this), + this.eventHub.onDidGeneratorRunCompleted(this.handleGeneratorRunCompleted, this), this.solutionManager.onDidChangeLoadState(this.handleLoadStateChanged, this), this.diagnosticCollection, this.environmentDiagnosticCollection, @@ -278,6 +282,14 @@ export class SolutionProblemsImpl implements SolutionProblems { await this.showProblemsViewIfNeeded(hasGeneralDiagnostics || hasEnvironmentDiagnostics); } + private async handleGeneratorRunCompleted(data: CbuildResultData): Promise { + // Generator run diagnostics are additive. Do not clear. + // This preserves existing convert and cbuild diagnostics while adding generator issues. + const logMessages: LogMessages = { success: true, errors: [], warnings: [], info: [] }; + const hasGeneralDiagnostics = await this.enrichAndUpdateDiagnostics(logMessages, data.toolsOutputMessages); + await this.showProblemsViewIfNeeded(hasGeneralDiagnostics); + } + private async updateEnvironmentDiagnosticsFromConvert(data: ConvertResultData): Promise { const messages: EnvironmentMessage[] = [ ...(data.logMessages.errors ?? []).map(message => ({ diff --git a/src/tasks/generator/generator-command.test.ts b/src/tasks/generator/generator-command.test.ts index 1c517b38..e7cbb00e 100644 --- a/src/tasks/generator/generator-command.test.ts +++ b/src/tasks/generator/generator-command.test.ts @@ -24,11 +24,13 @@ import { cmsisToolboxManagerFactory } from '../../solutions/cmsis-toolbox.factor import { extensionContextFactory } from '../../vscode-api/extension-context.factories'; import { csolutionFactory } from '../../solutions/csolution.factory'; import { CMSIS_SOLUTION_OUTPUT_CHANNEL } from '../../manifest'; +import { SolutionEventHub } from '../../solutions/solution-event-hub'; describe('GeneratorCommand', () => { let solutionManager: ReturnType; let outputChannelProvider: ReturnType; let cmsisToolboxManager: ReturnType; + let eventHub: SolutionEventHub; let generatorCommand: GeneratorCommand; const commandsProvider = commandsProviderFactory(); let context: ReturnType; @@ -39,7 +41,8 @@ describe('GeneratorCommand', () => { solutionManager = solutionManagerFactory(); outputChannelProvider = outputChannelProviderFactory(); cmsisToolboxManager = cmsisToolboxManagerFactory(); - generatorCommand = new GeneratorCommand(commandsProvider, solutionManager, outputChannelProvider, cmsisToolboxManager); + eventHub = new SolutionEventHub(); + generatorCommand = new GeneratorCommand(commandsProvider, solutionManager, outputChannelProvider, cmsisToolboxManager, eventHub); }); afterEach(async () => { @@ -48,6 +51,10 @@ describe('GeneratorCommand', () => { } }); + const createSolution = () => csolutionFactory({ + solutionPath: 'mock/path.csolution.yml', + }); + it('should register generator command on activation', async () => { await generatorCommand.activate(context as unknown as vscode.ExtensionContext); expect(commandsProvider.registerCommand).toHaveBeenCalledWith( @@ -60,19 +67,37 @@ describe('GeneratorCommand', () => { it('shows an error if no solution file is found', async () => { solutionManager.getCsolution.mockReturnValue(undefined); - await generatorCommand.handleRunGenerator('gen', 'ctx'); + await generatorCommand.handleRunGenerator('gen'); expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Solution file does not exist'); }); it('runs generator and shows info on success', async () => { - solutionManager.getCsolution.mockReturnValue(csolutionFactory({ solutionPath: 'mock/path.csolution.yml' })); + solutionManager.getCsolution.mockReturnValue(createSolution()); + + await generatorCommand.handleRunGenerator('my-gen'); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Starting generator my-gen...' + ); + + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + ['run', 'mock/path.csolution.yml', '-g', 'my-gen'], + expect.any(Function), + undefined, + undefined, + true + ); + }); + + it('passes context when provided', async () => { + solutionManager.getCsolution.mockReturnValue(createSolution()); await generatorCommand.handleRunGenerator('my-gen', 'debug'); expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( 'Starting generator my-gen for project debug...' ); - expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( 'csolution', ['run', 'mock/path.csolution.yml', '-g', 'my-gen', '-c', 'debug'], @@ -83,11 +108,65 @@ describe('GeneratorCommand', () => { ); }); + it('passes activeTarget when provided', async () => { + solutionManager.getCsolution.mockReturnValue(createSolution()); + + await generatorCommand.handleRunGenerator('my-gen', undefined, 'Release@TargetSet'); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Starting generator my-gen for target Release@TargetSet...' + ); + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + ['run', 'mock/path.csolution.yml', '-g', 'my-gen', '-a', 'Release@TargetSet'], + expect.any(Function), + undefined, + undefined, + true + ); + }); + + it('passes quoted empty activeTarget when provided as an empty string', async () => { + solutionManager.getCsolution.mockReturnValue(createSolution()); + + await generatorCommand.handleRunGenerator('my-gen', undefined, ''); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Starting generator my-gen for target ""...' + ); + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + ['run', 'mock/path.csolution.yml', '-g', 'my-gen', '-a', '""'], + expect.any(Function), + undefined, + undefined, + true + ); + }); + + it('passes both context and activeTarget when provided', async () => { + solutionManager.getCsolution.mockReturnValue(createSolution()); + + await generatorCommand.handleRunGenerator('my-gen', 'debug', 'Release@TargetSet'); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Starting generator my-gen for project debug and target Release@TargetSet...' + ); + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + ['run', 'mock/path.csolution.yml', '-g', 'my-gen', '-c', 'debug', '-a', 'Release@TargetSet'], + expect.any(Function), + undefined, + undefined, + true + ); + }); + it('shows error if generator fails', async () => { - solutionManager.getCsolution.mockReturnValue(csolutionFactory({ solutionPath: 'mock/path.csolution.yml' })); + solutionManager.getCsolution.mockReturnValue(createSolution()); cmsisToolboxManager.runCmsisTool.mockResolvedValue([1, undefined]); - await generatorCommand.handleRunGenerator('gen-fail', 'dev'); + await generatorCommand.handleRunGenerator('gen-fail'); const mockGetCreatedChannelByName = outputChannelProvider.mockGetCreatedChannelByName(CMSIS_SOLUTION_OUTPUT_CHANNEL); @@ -96,40 +175,40 @@ describe('GeneratorCommand', () => { }); it('does not show output channel on success', async () => { - solutionManager.getCsolution.mockReturnValue(csolutionFactory({ solutionPath: 'mock/path.csolution.yml' })); + solutionManager.getCsolution.mockReturnValue(createSolution()); cmsisToolboxManager.runCmsisTool.mockResolvedValue([0, undefined]); - await generatorCommand.handleRunGenerator('gen-ok', 'release'); + await generatorCommand.handleRunGenerator('gen-ok'); const channel = outputChannelProvider.mockGetCreatedChannelByName(CMSIS_SOLUTION_OUTPUT_CHANNEL); expect(channel?.show).not.toHaveBeenCalled(); }); it('appends the start message to the output channel', async () => { - solutionManager.getCsolution.mockReturnValue(csolutionFactory({ solutionPath: 'mock/path.csolution.yml' })); + solutionManager.getCsolution.mockReturnValue(createSolution()); - await generatorCommand.handleRunGenerator('my-gen', 'debug'); + await generatorCommand.handleRunGenerator('my-gen'); const channel = outputChannelProvider.mockGetCreatedChannelByName(CMSIS_SOLUTION_OUTPUT_CHANNEL); - expect(channel?.mockAppendedStrings).toContain('Starting generator my-gen for project debug...'); + expect(channel?.mockAppendedStrings).toContain('Starting generator my-gen...'); }); describe('command dispatch', () => { beforeEach(async () => { await generatorCommand.activate(context as unknown as vscode.ExtensionContext); - solutionManager.getCsolution.mockReturnValue(csolutionFactory({ solutionPath: 'mock/path.csolution.yml' })); + solutionManager.getCsolution.mockReturnValue(createSolution()); }); it('dispatches handleRunGenerator when input is a component-gen COutlineItem-like node', async () => { const node = { - getAttribute: (name: string) => ({ type: 'component-gen', generator: 'STM32CubeMX', 'cbuild-context': '.debug+target' }[name]), + getAttribute: (name: string) => ({ type: 'component-gen', generator: 'STM32CubeMX', activeTarget: 'TargetOnly' }[name]), }; await commandsProvider.mockRunRegistered(GeneratorCommand.runGeneratorCommandType, node); expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( 'csolution', - expect.arrayContaining(['-g', 'STM32CubeMX', '-c', '.debug+target']), + expect.arrayContaining(['-g', 'STM32CubeMX', '-a', 'TargetOnly']), expect.any(Function), undefined, undefined, @@ -140,7 +219,7 @@ describe('GeneratorCommand', () => { it('does not dispatch when input is a COutlineItem-like node with wrong type', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); const node = { - getAttribute: (name: string) => ({ type: 'component', generator: 'STM32CubeMX', 'cbuild-context': '.debug+target' }[name]), + getAttribute: (name: string) => ({ type: 'component', generator: 'STM32CubeMX', activeTarget: 'TargetOnly' }[name]), }; await commandsProvider.mockRunRegistered(GeneratorCommand.runGeneratorCommandType, node); @@ -153,12 +232,61 @@ describe('GeneratorCommand', () => { it('dispatches handleRunGenerator when input is a plain RunGeneratorRequest object', async () => { await commandsProvider.mockRunRegistered(GeneratorCommand.runGeneratorCommandType, { generator: 'plain-gen', - context: 'test+board', + activeTarget: 'Plain@Target', + }); + + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + expect.arrayContaining(['-g', 'plain-gen', '-a', 'Plain@Target']), + expect.any(Function), + undefined, + undefined, + true + ); + }); + + it('dispatches handleRunGenerator with quoted empty activeTarget when request carries an empty string', async () => { + await commandsProvider.mockRunRegistered(GeneratorCommand.runGeneratorCommandType, { + generator: 'empty-target-gen', + activeTarget: '', + }); + + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + expect.arrayContaining(['-g', 'empty-target-gen', '-a', '""']), + expect.any(Function), + undefined, + undefined, + true + ); + }); + + it('accepts a legacy RunGeneratorRequest object with context', async () => { + await commandsProvider.mockRunRegistered(GeneratorCommand.runGeneratorCommandType, { + generator: 'legacy-gen', + context: 'Legacy.Debug+Board', + }); + + expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( + 'csolution', + expect.arrayContaining(['-g', 'legacy-gen', '-c', 'Legacy.Debug+Board']), + expect.any(Function), + undefined, + undefined, + true + ); + }); + + it('accepts a request object with both context and activeTarget', async () => { + await commandsProvider.mockRunRegistered(GeneratorCommand.runGeneratorCommandType, { + generator: 'hybrid-gen', + context: 'Hybrid.Debug+Board', + activeTarget: 'Hybrid@TargetSet', }); expect(cmsisToolboxManager.runCmsisTool).toHaveBeenCalledWith( 'csolution', - expect.arrayContaining(['-g', 'plain-gen', '-c', 'test+board']), + expect.arrayContaining(['-g', 'hybrid-gen', '-c', 'Hybrid.Debug+Board', '-a', 'Hybrid@TargetSet']), expect.any(Function), undefined, undefined, diff --git a/src/tasks/generator/generator-command.ts b/src/tasks/generator/generator-command.ts index 2cb00bc2..73a18d10 100644 --- a/src/tasks/generator/generator-command.ts +++ b/src/tasks/generator/generator-command.ts @@ -21,10 +21,13 @@ import { COutlineItem } from '../../views/solution-outline/tree-structure/soluti import { OutputChannelProvider } from '../../vscode-api/output-channel-provider'; import { CmsisToolboxManager } from '../../solutions/cmsis-toolbox'; import { SolutionManager } from '../../solutions/solution-manager'; +import { SolutionEventHub } from '../../solutions/solution-event-hub'; +import { getToolsSeverity } from '../../solutions/solution-problems'; interface RunGeneratorRequest { generator: string; - context: string; + context?: string; + activeTarget?: string; } export class GeneratorCommand { @@ -35,6 +38,7 @@ export class GeneratorCommand { private readonly solutionManager: SolutionManager, private readonly outputChannelProvider: OutputChannelProvider, private readonly cmsisToolboxManager: CmsisToolboxManager, + private readonly eventHub: SolutionEventHub, ) { } public async activate(context: vscode.ExtensionContext): Promise { @@ -43,7 +47,7 @@ export class GeneratorCommand { this.commandsProvider.registerCommand(GeneratorCommand.runGeneratorCommandType, async (input: unknown) => { const request = this.getRunGeneratorRequest(input); if (request) { - await this.handleRunGenerator(request.generator, request.context); + await this.handleRunGenerator(request.generator, request.context, request.activeTarget); } else { console.error(`Tried to execute ${GeneratorCommand.runGeneratorCommandType} without a generator component`); } @@ -60,37 +64,65 @@ export class GeneratorCommand { return { generator: maybeNode.getAttribute('generator') ?? '', - context: maybeNode.getAttribute('cbuild-context') ?? '', + activeTarget: maybeNode.getAttribute('activeTarget') ?? undefined, }; } const maybeRequest = input as Partial | undefined; - if (typeof maybeRequest?.generator !== 'string' || typeof maybeRequest.context !== 'string') { + if (typeof maybeRequest?.generator !== 'string') { return undefined; } return { generator: maybeRequest.generator, - context: maybeRequest.context, + context: typeof maybeRequest.context === 'string' ? maybeRequest.context : undefined, + activeTarget: typeof maybeRequest.activeTarget === 'string' ? maybeRequest.activeTarget : undefined, }; } - public async handleRunGenerator(generator: string, context: string): Promise { + public async handleRunGenerator(generator: string, context?: string, activeTarget?: string): Promise { const solutionFilePath = this.solutionManager.getCsolution()?.solutionPath; if (!solutionFilePath) { vscode.window.showErrorMessage('Solution file does not exist'); return; } - const msg = `Starting generator ${generator} for project ${context}...`; + const normalizedActiveTarget = activeTarget !== undefined ? (activeTarget || '""') : undefined; + + const msg = context && normalizedActiveTarget + ? `Starting generator ${generator} for project ${context} and target ${normalizedActiveTarget}...` + : context + ? `Starting generator ${generator} for project ${context}...` + : normalizedActiveTarget + ? `Starting generator ${generator} for target ${normalizedActiveTarget}...` + : `Starting generator ${generator}...`; vscode.window.showInformationMessage(msg); - const executableArgs = ['run', solutionFilePath, '-g', generator, '-c', context]; + const executableArgs = ['run', solutionFilePath, '-g', generator]; + if (context) { + executableArgs.push('-c', context); + } + if (normalizedActiveTarget !== undefined) { + executableArgs.push('-a', normalizedActiveTarget); + } const outputChannel = this.outputChannelProvider.getOrCreate(CMSIS_SOLUTION_OUTPUT_CHANNEL); outputChannel.appendLine(msg); - const [result] = await this.cmsisToolboxManager.runCmsisTool('csolution', executableArgs, line => outputChannel.appendLine(line.trimEnd()), undefined, - undefined, true); + // Capture output lines for diagnostics + const outputLines: string[] = []; + const [result] = await this.cmsisToolboxManager.runCmsisTool('csolution', executableArgs, line => { + const trimmedLine = line.trimEnd(); + outputChannel.appendLine(trimmedLine); + outputLines.push(trimmedLine); + }, undefined, undefined, true); + + // Fire event with generator run result for diagnostics + const severity = getToolsSeverity(outputLines); + await this.eventHub.fireGeneratorRunCompleted({ + success: result === 0, + severity: severity, + toolsOutputMessages: outputLines, + }); if (result != 0) { outputChannel.show(); diff --git a/src/views/solution-outline/tree-structure/solution-outline-project-items.ts b/src/views/solution-outline/tree-structure/solution-outline-project-items.ts index 9c1b93b2..2c6fce17 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-project-items.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-project-items.ts @@ -25,6 +25,7 @@ import { getMapFilePath, setDocContext, setHeaderContext, setLinkerContext } fro import { CProjectYamlFile } from '../../../solutions/files/cproject-yaml-file'; import { SolutionOutlineItemBuilder } from './solution-outline-item-builder'; import { buildPackOverviewLink } from './pack-tooltip'; +import { contextDescriptorFromString } from '../../../solutions/descriptors/descriptors'; export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { private readonly _lastPrioritizedComponentList: COutlineItem[] = []; @@ -354,10 +355,12 @@ export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { if (generator) { const id = generator.getValueAsString('id'); + const targetType = contextDescriptorFromString(cbuild.getValueAsString('context')).targetType; + const activeTarget = this.csolution?.getActiveTargetSetName() ?? targetType; node.addFeature('component-gen'); node.setAttribute('type', 'component-gen'); node.setAttribute('generator', id); - node.setAttribute('cbuild-context', cbuild.getValue('context')); + node.setAttribute('activeTarget', activeTarget); tooltip += '\n' + '- generator: ` ' + id + ' `'; const fileName = component.resolvePath(generator.getValueAsString('path')); diff --git a/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts b/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts index 2a3a4964..b779c6b8 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts @@ -53,6 +53,17 @@ describe('CSolution', () => { .replace(/[ \t]+/g, ' ') // collapse spaces/tabs .trim(); // remove leading/trailing whitespace + function findComponentGenNodes(node: COutlineItem): COutlineItem[] { + const results: COutlineItem[] = []; + if (node.getAttribute('type') === 'component-gen') { + results.push(node); + } + for (const child of node.getChildren() as COutlineItem[]) { + results.push(...findComponentGenNodes(child)); + } + return results; + } + async function dumpOutline(tree: COutlineItem, solutionDir: string, dumpFileName: string, refFileName: string): Promise<{ dump: string; ref: string; }> { const res: { dump: string, ref: string } = { dump: '', ref: '' }; @@ -236,6 +247,44 @@ describe('CSolution', () => { expect(res.dump).toEqual(res.ref); }); + it('uses the full TargetType@Set as activeTarget on component-gen nodes when a target set is active', async () => { + const fileName = path.join(tmpSolutionDir, 'USBD', 'USB_Device.csolution.yml'); + const csolution = new CSolution(); + + const loadResult = await csolution.load(fileName); + expect(loadResult).toEqual(ETextFileResult.Success); + + jest.spyOn(csolution, 'getActiveTargetSetName').mockReturnValue('B-U585I-IOT02A@fvp'); + + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); + const tree = solutionOutlineTree.createTree(); + + const componentGenNodes = findComponentGenNodes(tree as COutlineItem); + expect(componentGenNodes.length).toBeGreaterThan(0); + for (const node of componentGenNodes) { + expect(node.getAttribute('activeTarget')).toBe('B-U585I-IOT02A@fvp'); + } + }); + + it('falls back to TargetType from context string as activeTarget when no target set is active', async () => { + const fileName = path.join(tmpSolutionDir, 'USBD', 'USB_Device.csolution.yml'); + const csolution = new CSolution(); + + const loadResult = await csolution.load(fileName); + expect(loadResult).toEqual(ETextFileResult.Success); + + jest.spyOn(csolution, 'getActiveTargetSetName').mockReturnValue(undefined); + + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); + const tree = solutionOutlineTree.createTree(); + + const componentGenNodes = findComponentGenNodes(tree as COutlineItem); + expect(componentGenNodes.length).toBeGreaterThan(0); + for (const node of componentGenNodes) { + expect(node.getAttribute('activeTarget')).toBe('B-U585I-IOT02A'); + } + }); + it('test tree content for West project', async () => { const fileName = path.join(tmpSolutionDir, 'WestSupport', 'solution.csolution.yml'); const csolution = new CSolution(); diff --git a/test-data/solutions/USBD/CmsisViewTreeOneProjRef.txt b/test-data/solutions/USBD/CmsisViewTreeOneProjRef.txt index 220b9a6b..29ac2d93 100644 --- a/test-data/solutions/USBD/CmsisViewTreeOneProjRef.txt +++ b/test-data/solutions/USBD/CmsisViewTreeOneProjRef.txt @@ -351,7 +351,7 @@ solution layerUri=TEST_DIR/solutions/USBD/Board/B-U585I-IOT02A/Board.clayer.yml type=component-gen generator=CubeMX - cbuild-context=MassStorage.Debug+B-U585I-IOT02A + activeTarget=B-U585I-IOT02A tooltip=- component: ` Keil::Device:CubeMX@1.0.0 ` - from pack: ` Keil::STM32U5xx_DFP@3.0.0-dev ` [$(link-external)](https://www.keil.arm.com/packs/stm32u5xx_dfp-keil/) - generator: ` CubeMX ` diff --git a/test-data/solutions/USBD/CmsisViewTreeRef.txt b/test-data/solutions/USBD/CmsisViewTreeRef.txt index 6cb8e9f4..9723f819 100644 --- a/test-data/solutions/USBD/CmsisViewTreeRef.txt +++ b/test-data/solutions/USBD/CmsisViewTreeRef.txt @@ -390,7 +390,7 @@ solution layerUri=TEST_DIR/solutions/USBD/Board/B-U585I-IOT02A/Board.clayer.yml type=component-gen generator=CubeMX - cbuild-context=HID.Release+B-U585I-IOT02A + activeTarget=B-U585I-IOT02A tooltip=- component: ` Keil::Device:CubeMX@1.0.0 ` - from pack: ` Keil::STM32U5xx_DFP@3.0.0-dev ` [$(link-external)](https://www.keil.arm.com/packs/stm32u5xx_dfp-keil/) - generator: ` CubeMX ` @@ -1141,7 +1141,7 @@ solution layerUri=TEST_DIR/solutions/USBD/Board/B-U585I-IOT02A/Board.clayer.yml type=component-gen generator=CubeMX - cbuild-context=MassStorage.Debug+B-U585I-IOT02A + activeTarget=B-U585I-IOT02A tooltip=- component: ` Keil::Device:CubeMX@1.0.0 ` - from pack: ` Keil::STM32U5xx_DFP@3.0.0-dev ` [$(link-external)](https://www.keil.arm.com/packs/stm32u5xx_dfp-keil/) - generator: ` CubeMX `