Skip to content

Commit 0c74977

Browse files
committed
Add action resolver
1 parent bada274 commit 0c74977

5 files changed

Lines changed: 282 additions & 151 deletions

File tree

src/manifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const CONFIG_AUTO_SHOW_CMSIS_VIEW = 'autoShowCMSISView';
4646
export const CONFIG_BUILD_OUTPUT_VERBOSITY = 'buildOutputVerbosity';
4747
export const MANAGE_COMPONENTS_PACKS_COMMAND_ID = `${PACKAGE_NAME}.manageComponentsPacks`;
4848
export const MERGE_FILE_COMMAND_ID = `${PACKAGE_NAME}.mergeFile`;
49+
export const RUN_GENERATOR_COMMAND_ID = `${PACKAGE_NAME}.runGenerator`;
4950

5051
export const MIN_TOOLBOX_VERSION = '2.12.0';
5152

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/**
2+
* Copyright 2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as path from 'node:path';
18+
import * as vscode from 'vscode';
19+
import { MANAGE_COMPONENTS_PACKS_COMMAND_ID, MERGE_FILE_COMMAND_ID, RUN_GENERATOR_COMMAND_ID } from '../manifest';
20+
import { stripVendor, stripVersion } from '../utils/string-utils';
21+
22+
export const MERGE_VIEW_LINK_LABEL = 'Open in Merge View';
23+
24+
type MergeUpdateLevel = 'required' | 'recommended' | 'suggested' | 'mandatory';
25+
26+
interface MergeMessageMatch {
27+
localPath: string;
28+
updateLevel: MergeUpdateLevel;
29+
}
30+
31+
interface QueryActionMatch {
32+
query: string;
33+
action: 'components-packs' | 'find-in-files';
34+
}
35+
36+
export interface ProblemDiagnosticActionContext {
37+
message: string;
38+
diagnosticFilePath: string;
39+
hasLocation: boolean;
40+
}
41+
42+
export interface ProblemDiagnosticActionResult {
43+
message?: string;
44+
code?: NonNullable<vscode.Diagnostic['code']>;
45+
}
46+
47+
const mergeMessagePatterns = [
48+
{
49+
pattern: /update\s+(required|recommended|suggested|mandatory)\s+for\s+file\s+'([^']+)'/i,
50+
getLocalPath: (match: RegExpExecArray) => match[2],
51+
getUpdateLevel: (match: RegExpExecArray) => match[1],
52+
},
53+
] as const;
54+
55+
const mergeComponentRegex = /(?:for|from)\s+component\s+'([^']+)'/i;
56+
const generatorMissingPattern = /cgen file was not found,\s*run generator '([^']+)' for context '([^']+)'/i;
57+
58+
const queryActionPatterns: ReadonlyArray<{ pattern: RegExp; action: 'components-packs' | 'find-in-files' }> = [
59+
{ pattern: /dependency validation for context '([^']+)' failed:/, action: 'components-packs' },
60+
{ pattern: /\/([^/\s']+\.[^/\s']+)/, action: 'find-in-files' },
61+
{ pattern: /'([^']+)'/, action: 'find-in-files' },
62+
{ pattern: /([A-Za-z0-9_.-]+::[A-Za-z0-9_.-]+(@[A-Za-z0-9_.-]+)*)/, action: 'find-in-files' },
63+
{ pattern: /([A-Za-z0-9_.-]+@[A-Za-z0-9_.-]*)/, action: 'find-in-files' },
64+
];
65+
66+
export class ProblemDiagnosticActionResolver {
67+
public resolve(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
68+
return this.resolveMergeAction(context)
69+
?? this.resolveGeneratorMissingAction(context)
70+
?? this.resolveManageComponentsAction(context)
71+
?? this.resolveGenericSearchAction(context);
72+
}
73+
74+
public createMergeDiagnosticAction(message: string, diagnosticFilePath: string): ProblemDiagnosticActionResult | undefined {
75+
const merge = this.parseMergeMessage(message);
76+
if (!merge) {
77+
return undefined;
78+
}
79+
80+
const componentId = mergeComponentRegex.exec(message)?.[1];
81+
const localPath = this.isAbsoluteFilePath(merge.localPath) ? merge.localPath : diagnosticFilePath;
82+
83+
return {
84+
message: this.createMergeDiagnosticMessage(localPath, merge.updateLevel, componentId),
85+
code: {
86+
value: MERGE_VIEW_LINK_LABEL,
87+
target: this.createMergeCommandUri(localPath),
88+
},
89+
};
90+
}
91+
92+
public createMergeCommandUri(localPath: string): vscode.Uri {
93+
const args = this.encodeCommandArgs([localPath]);
94+
return vscode.Uri.parse(`command:${MERGE_FILE_COMMAND_ID}?${args}`);
95+
}
96+
97+
public isAbsoluteFilePath(filePath: string): boolean {
98+
return path.isAbsolute(filePath) || path.win32.isAbsolute(filePath);
99+
}
100+
101+
private resolveMergeAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
102+
return this.createMergeDiagnosticAction(context.message, context.diagnosticFilePath);
103+
}
104+
105+
private resolveGeneratorMissingAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
106+
if (context.hasLocation) {
107+
return undefined;
108+
}
109+
110+
const match = context.message.match(generatorMissingPattern);
111+
if (!match) {
112+
return undefined;
113+
}
114+
115+
const [, generator, generatorContext] = match;
116+
return {
117+
code: {
118+
value: 'Run Generator',
119+
target: this.createRunGeneratorCommandUri(generator, generatorContext),
120+
},
121+
};
122+
}
123+
124+
private resolveManageComponentsAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
125+
if (context.hasLocation) {
126+
return undefined;
127+
}
128+
129+
const queryAction = this.findQueryActionInMessage(context.message, 'components-packs');
130+
if (!queryAction) {
131+
return undefined;
132+
}
133+
134+
const args = this.encodeCommandArgs([{ type: 'context', value: queryAction.query }]);
135+
return {
136+
code: {
137+
value: 'Manage Components',
138+
target: vscode.Uri.parse(`command:${MANAGE_COMPONENTS_PACKS_COMMAND_ID}?${args}`),
139+
},
140+
};
141+
}
142+
143+
private resolveGenericSearchAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
144+
if (context.hasLocation) {
145+
return undefined;
146+
}
147+
148+
const queryAction = this.findQueryActionInMessage(context.message, 'find-in-files');
149+
if (!queryAction) {
150+
return undefined;
151+
}
152+
153+
const args = this.encodeFindInFilesArgs(queryAction.query);
154+
return {
155+
code: {
156+
value: 'Find in Files',
157+
target: vscode.Uri.parse(`command:workbench.action.findInFiles?${args}`),
158+
},
159+
};
160+
}
161+
162+
private parseMergeMessage(message: string): MergeMessageMatch | undefined {
163+
for (const item of mergeMessagePatterns) {
164+
const match = item.pattern.exec(message);
165+
if (!match) {
166+
continue;
167+
}
168+
169+
return {
170+
localPath: item.getLocalPath(match),
171+
updateLevel: item.getUpdateLevel(match).toLowerCase() as MergeUpdateLevel,
172+
};
173+
}
174+
175+
return undefined;
176+
}
177+
178+
private findQueryActionInMessage(message: string, action: QueryActionMatch['action']): QueryActionMatch | undefined {
179+
for (const item of queryActionPatterns) {
180+
if (item.action !== action) {
181+
continue;
182+
}
183+
184+
const match = message.match(item.pattern);
185+
if (match?.[1]) {
186+
return { query: match[1], action: item.action };
187+
}
188+
}
189+
190+
return undefined;
191+
}
192+
193+
private createMergeDiagnosticMessage(localPath: string, updateLevel: MergeUpdateLevel, componentId: string | undefined): string {
194+
const fileName = path.basename(localPath);
195+
if (componentId === undefined) {
196+
return `update ${updateLevel} for config file '${fileName}' has a new version available for merge.`;
197+
}
198+
199+
const componentIdNoVersion = stripVersion(componentId);
200+
const componentDisplayName = stripVendor(componentIdNoVersion);
201+
return `update ${updateLevel} for config file '${fileName}' from component '${componentDisplayName}'.`;
202+
}
203+
204+
private createRunGeneratorCommandUri(generator: string, context: string): vscode.Uri {
205+
const args = this.encodeCommandArgs([{ generator, context }]);
206+
return vscode.Uri.parse(`command:${RUN_GENERATOR_COMMAND_ID}?${args}`);
207+
}
208+
209+
private encodeFindInFilesArgs(query: string): string {
210+
const args = {
211+
query: query,
212+
filesToInclude: '*.yml,*.yaml',
213+
filesToExclude: '*.cbuild-idx.yml,*.cbuild.yml,*.cbuild-run.yml',
214+
isRegex: false,
215+
isCaseSensitive: false,
216+
matchWholeWord: false,
217+
triggerSearch: true,
218+
focusResults: true,
219+
};
220+
return encodeURIComponent(JSON.stringify(args));
221+
}
222+
223+
private encodeCommandArgs(args: unknown[]): string {
224+
return encodeURIComponent(JSON.stringify(args));
225+
}
226+
}

src/solutions/solution-problems.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ExtensionContext } from 'vscode';
2020
import { MANAGE_COMPONENTS_PACKS_COMMAND_ID, MERGE_FILE_COMMAND_ID } from '../manifest';
2121
import * as fsUtils from '../utils/fs-utils';
2222
import * as vscodeUtils from '../utils/vscode-utils';
23+
import { ProblemDiagnosticActionResolver } from './problem-diagnostic-action-resolver';
2324
import { solutionManagerFactory, MockSolutionManager } from './solution-manager.factories';
2425
import { SolutionEventHub } from './solution-event-hub';
2526
import { enrichLogMessagesFromToolOutput, SolutionProblemsImpl } from './solution-problems';
@@ -49,12 +50,14 @@ describe('SolutionProblems', () => {
4950
let solutionManager: MockSolutionManager;
5051
let eventHub: SolutionEventHub;
5152
let solutionProblems: SolutionProblemsImpl;
53+
let diagnosticActionResolver: ProblemDiagnosticActionResolver;
5254

5355
beforeEach(() => {
5456
solutionManager = solutionManagerFactory();
5557
solutionManager.getCsolution.mockReturnValue(buildCsolution() as unknown as ReturnType<MockSolutionManager['getCsolution']>);
5658
eventHub = new SolutionEventHub();
5759
solutionProblems = new SolutionProblemsImpl(solutionManager, eventHub);
60+
diagnosticActionResolver = new ProblemDiagnosticActionResolver();
5861

5962
(vscode.workspace.openTextDocument as jest.Mock).mockResolvedValue({
6063
lineCount: 200,
@@ -263,15 +266,15 @@ describe('SolutionProblems', () => {
263266
});
264267

265268
it('creates a merge command uri with encoded local path', () => {
266-
const result = solutionProblems['createMergeCommandUri']('/packs/Component/config.c');
269+
const result = diagnosticActionResolver.createMergeCommandUri('/packs/Component/config.c');
267270
const [command, args] = result.toString().split('?');
268271

269272
expect(command).toBe(`command:${MERGE_FILE_COMMAND_ID}`);
270273
expect(JSON.parse(decodeURIComponent(args))).toEqual(['/packs/Component/config.c']);
271274
});
272275

273276
it('creates merge diagnostic action for merge messages with component context', () => {
274-
const result = solutionProblems['createMergeDiagnosticAction'](
277+
const result = diagnosticActionResolver.createMergeDiagnosticAction(
275278
"update required for file '/packs/Component/config.c' from component 'Arm::Device@2.3.4'",
276279
layerPath,
277280
);
@@ -287,7 +290,7 @@ describe('SolutionProblems', () => {
287290

288291
it('creates merge diagnostic action for current toolbox message wording', () => {
289292
const configPath = 'C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c';
290-
const result = solutionProblems['createMergeDiagnosticAction'](
293+
const result = diagnosticActionResolver.createMergeDiagnosticAction(
291294
`update recommended for file '${configPath}' from component 'CMSIS:RTOS2:Keil RTX5&Source'.\nMerge content from update file, rename update file to base file and remove previous base file`,
292295
layerPath,
293296
);
@@ -302,12 +305,12 @@ describe('SolutionProblems', () => {
302305
});
303306

304307
it('treats Windows-style merge paths as absolute', () => {
305-
expect(solutionProblems['isAbsoluteFilePath']('C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c')).toBe(true);
306-
expect(solutionProblems['isAbsoluteFilePath']('relative-config.c')).toBe(false);
308+
expect(diagnosticActionResolver.isAbsoluteFilePath('C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c')).toBe(true);
309+
expect(diagnosticActionResolver.isAbsoluteFilePath('relative-config.c')).toBe(false);
307310
});
308311

309312
it('returns undefined merge diagnostic action for non-merge messages', () => {
310-
const result = solutionProblems['createMergeDiagnosticAction'](
313+
const result = diagnosticActionResolver.createMergeDiagnosticAction(
311314
"component 'Arm::Device@2.3.4' is missing",
312315
layerPath,
313316
);

0 commit comments

Comments
 (0)