From c5bf2d674c0bda06c550ff1684e9bf427cf17f67 Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Thu, 5 Mar 2026 11:48:29 +0100 Subject: [PATCH 1/8] Enhance file selection functionality with relative/absolute path configuration and update copyright notices --- src/debug/debug-adapters-yaml-file.ts | 4 +- .../manage-solution-custom-editor.ts | 2 +- .../manage-solution-webview-main.test.ts | 12 ++-- .../manage-solution-webview-main.ts | 7 +- src/views/manage-solution/messages.ts | 69 ++++++++++--------- .../view/components/manage-solution.tsx | 6 +- 6 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/debug/debug-adapters-yaml-file.ts b/src/debug/debug-adapters-yaml-file.ts index 6d08204b7..3e4858abf 100644 --- a/src/debug/debug-adapters-yaml-file.ts +++ b/src/debug/debug-adapters-yaml-file.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { PathType } from '../views/manage-solution/messages'; import { createInterfaceFactory } from '../generic/interface-factory'; import { ETextFileResult } from '../generic/text-file'; import { ITreeItem, CTreeItem } from '../generic/tree-item'; @@ -27,7 +28,8 @@ export type UIOption = { description?: string; //Hover over text. 'yml-node': string; //Name of the node in the csolution file under debugger: section. type: T; //Type of the option [number, enum, string, file] - pname?: string; //If present, the option is pname based. Then the same property will be created for each pname instance. + pname?: string; //If present, the option is pname based. Then the same property will be created for each pname instance. + 'path-type'?: PathType; //If the type is file, indicates if the path is absolute or relative to the solution file directory. Default is relative. } & V; export type UIEnumValue = { diff --git a/src/views/manage-solution/manage-solution-custom-editor.ts b/src/views/manage-solution/manage-solution-custom-editor.ts index 18d897f39..a2b12c213 100644 --- a/src/views/manage-solution/manage-solution-custom-editor.ts +++ b/src/views/manage-solution/manage-solution-custom-editor.ts @@ -1,5 +1,5 @@ /** - * Copyright 2026 Arm Limited + * Copyright (C) 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. diff --git a/src/views/manage-solution/manage-solution-webview-main.test.ts b/src/views/manage-solution/manage-solution-webview-main.test.ts index 300806646..a86534f9f 100644 --- a/src/views/manage-solution/manage-solution-webview-main.test.ts +++ b/src/views/manage-solution/manage-solution-webview-main.test.ts @@ -560,11 +560,13 @@ describe('ContextSelectionWebviewMain', () => { const dialogOptions = showOpenDialogSpy.mock.calls[0][0]; expect(dialogOptions?.defaultUri?.toString()).toBe(URI.file(path.dirname(defaultPath)).toString()); - expect(webviewManager.sendMessage).toHaveBeenCalledWith({ - type: 'FILE_SELECTED', - data: ['images/app.axf'], - for: 'image-path' - }); + expect(webviewManager.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'FILE_SELECTED', + data: expect.arrayContaining([expect.stringMatching(/root[/\\]images[/\\]app\.axf$/)]), + for: 'image-path' + }) + ); }); }); diff --git a/src/views/manage-solution/manage-solution-webview-main.ts b/src/views/manage-solution/manage-solution-webview-main.ts index 689f19734..aa3af1cd8 100644 --- a/src/views/manage-solution/manage-solution-webview-main.ts +++ b/src/views/manage-solution/manage-solution-webview-main.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024-2026 Arm Limited + * Copyright (C) 2024-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. @@ -440,7 +440,10 @@ export class ManageSolutionWebviewMain { if (fileUri && fileUri[0]) { const paths = fileUri.map(u => - backToForwardSlashes(path.relative(solutionDir, u.fsPath)) + backToForwardSlashes(options?.pathType === 'absolute' + ? u.fsPath + : path.relative(solutionDir, u.fsPath) + ) ); await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: paths, for: targetElementId }); diff --git a/src/views/manage-solution/messages.ts b/src/views/manage-solution/messages.ts index e59c4bc79..53be659a9 100644 --- a/src/views/manage-solution/messages.ts +++ b/src/views/manage-solution/messages.ts @@ -17,48 +17,51 @@ import { DebugAdapter } from '../../debug/debug-adapters-yaml-file'; import { ActiveTargetSet, SolutionData } from './view/state/manage-solution-state'; +export type PathType = 'absolute' | 'relative'; + export type FileSelectorOptionsType = { - canSelectMany?: boolean, - defaultUri?: string, - openLabel?: string, - title?: string, - filters?: { [key: string]: string[] } + canSelectMany?: boolean, + defaultUri?: string, + openLabel?: string, + title?: string, + pathType?: PathType, + filters?: { [key: string]: string[] } }; /** * Messages that the context selection view can pass to the extension. */ export type OutgoingMessage - = { type: 'GET_CONTEXT_SELECTION_DATA' } - | { type: 'SET_SELECTED_TARGET', target: string, set: string | undefined } - | { type: 'OPEN_FILE', path: string } - | { type: 'SET_SELECTED_CONTEXTS', data: SolutionData } - | { type: 'GET_DEBUG_ADAPTERS' } - | { type: 'SET_DEBUGGER', name: string } - | { type: 'ADD_NEW_CONTEXT' } - | { type: 'ADD_NEW_PROJECT' } - | { type: 'ADD_NEW_IMAGE' } - | { type: 'UNLINK_IMAGE', image: string } - | { type: 'SET_START_PROCESSOR', value: string } - | { type: 'SAVE_CONTEXT_SELECTION' } - | { type: 'OPEN_HELP' } - | { type: 'SET_DEBUG_ADAPTER_PROPERTY', service: string | undefined, key: string, value: string | number, pname?: string } - | { type: 'SELECT_FILE', targetElementId: string, options?: FileSelectorOptionsType } - | { type: 'SET_AUTO_UPDATE', value: boolean } - | { type: 'TOGGLE_DEBUGGER', value: boolean } - | { type: 'TOGGLE_DEBUG_ADAPTER_SECTION', section: string } - ; + = { type: 'GET_CONTEXT_SELECTION_DATA' } + | { type: 'SET_SELECTED_TARGET', target: string, set: string | undefined } + | { type: 'OPEN_FILE', path: string } + | { type: 'SET_SELECTED_CONTEXTS', data: SolutionData } + | { type: 'GET_DEBUG_ADAPTERS' } + | { type: 'SET_DEBUGGER', name: string } + | { type: 'ADD_NEW_CONTEXT' } + | { type: 'ADD_NEW_PROJECT' } + | { type: 'ADD_NEW_IMAGE' } + | { type: 'UNLINK_IMAGE', image: string } + | { type: 'SET_START_PROCESSOR', value: string } + | { type: 'SAVE_CONTEXT_SELECTION' } + | { type: 'OPEN_HELP' } + | { type: 'SET_DEBUG_ADAPTER_PROPERTY', service: string | undefined, key: string, value: string | number, pname?: string } + | { type: 'SELECT_FILE', targetElementId: string, options?: FileSelectorOptionsType } + | { type: 'SET_AUTO_UPDATE', value: boolean } + | { type: 'TOGGLE_DEBUGGER', value: boolean } + | { type: 'TOGGLE_DEBUG_ADAPTER_SECTION', section: string } + ; /** * Messages that the extension can pass to the context selection view. */ export type IncomingMessage - = { type: 'DATA_CONTEXT_SELECTION', data: SolutionData } - | { type: 'DEBUG_ADAPTERS', data: DebugAdapter[], sectionsInUse: string[] } - | { type: 'DEBUGGER', data: string } - | { type: 'ACTIVE_TARGET_SET', data: ActiveTargetSet } - | { type: 'IS_DIRTY', data: boolean } - | { type: 'IS_BUSY', data: boolean } - | { type: 'FILE_SELECTED', data: string[], for: string } - | { type: 'AUTO_UPDATE', data: boolean } - ; + = { type: 'DATA_CONTEXT_SELECTION', data: SolutionData } + | { type: 'DEBUG_ADAPTERS', data: DebugAdapter[], sectionsInUse: string[] } + | { type: 'DEBUGGER', data: string } + | { type: 'ACTIVE_TARGET_SET', data: ActiveTargetSet } + | { type: 'IS_DIRTY', data: boolean } + | { type: 'IS_BUSY', data: boolean } + | { type: 'FILE_SELECTED', data: string[], for: string } + | { type: 'AUTO_UPDATE', data: boolean } + ; diff --git a/src/views/manage-solution/view/components/manage-solution.tsx b/src/views/manage-solution/view/components/manage-solution.tsx index fb3911b4d..84f05c668 100644 --- a/src/views/manage-solution/view/components/manage-solution.tsx +++ b/src/views/manage-solution/view/components/manage-solution.tsx @@ -29,6 +29,7 @@ import { SolutionUpdateAction, contextUpdateReducer, initialState, manageSolutio import './manage-solution.css'; import { ProjectsTable } from './projects-table'; import { TargetsTable } from './targets-table'; +import { PathType } from '../../messages'; export interface ManageSolutionProps { messageHandler: MessageHandler; @@ -212,6 +213,7 @@ export const ManageSolution = (props: ManageSolutionProps) => { input?.setAttribute('id', id); } const title = input?.getAttribute('title') || 'Select File'; + const optionPathType = input?.getAttribute('data-option-path-type') as (PathType | undefined) ?? 'relative'; const currentValue = input?.value || ''; props.messageHandler.push({ type: 'SELECT_FILE', @@ -221,7 +223,8 @@ export const ManageSolution = (props: ManageSolutionProps) => { defaultUri: currentValue, openLabel: 'Select File', // title of the dialog button to choose the file title: title, // dialog title - filters: { 'All Files': ['*'] } + filters: { 'All Files': ['*'] }, + pathType: optionPathType } }); }; @@ -433,6 +436,7 @@ export const ManageSolution = (props: ManageSolutionProps) => { addonAfter={} value={(localValues[k] as string) ?? ''} data-yml-node={o['yml-node']} + data-option-path-type={o['path-type']} onPressEnter={blurOnEnter} onChange={(e) => { setLocalValues(prev => ({ ...prev, [k]: e.target.value })); From 6c2bdf61dcbeafb6b438bfc06fac1dc4b81a7987 Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Thu, 5 Mar 2026 12:00:12 +0100 Subject: [PATCH 2/8] Fix copyright formatting in manage-solution-custom-editor and manage-solution-webview-main files --- src/views/manage-solution/manage-solution-custom-editor.ts | 2 +- src/views/manage-solution/manage-solution-webview-main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/manage-solution/manage-solution-custom-editor.ts b/src/views/manage-solution/manage-solution-custom-editor.ts index a2b12c213..18d897f39 100644 --- a/src/views/manage-solution/manage-solution-custom-editor.ts +++ b/src/views/manage-solution/manage-solution-custom-editor.ts @@ -1,5 +1,5 @@ /** - * Copyright (C) 2026 Arm Limited + * 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. diff --git a/src/views/manage-solution/manage-solution-webview-main.ts b/src/views/manage-solution/manage-solution-webview-main.ts index aa3af1cd8..614a46062 100644 --- a/src/views/manage-solution/manage-solution-webview-main.ts +++ b/src/views/manage-solution/manage-solution-webview-main.ts @@ -1,5 +1,5 @@ /** - * Copyright (C) 2024-2026 Arm Limited + * Copyright 2024-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. From ea531817d11fdcc59af1102af6e1a79c150e032a Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Thu, 5 Mar 2026 15:37:13 +0100 Subject: [PATCH 3/8] Refactor context selection tests to include configuration provider and update file selection data format --- .../manage-solution/manage-solution-webview-main.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/views/manage-solution/manage-solution-webview-main.test.ts b/src/views/manage-solution/manage-solution-webview-main.test.ts index a86534f9f..0ba317002 100644 --- a/src/views/manage-solution/manage-solution-webview-main.test.ts +++ b/src/views/manage-solution/manage-solution-webview-main.test.ts @@ -63,8 +63,10 @@ describe('ContextSelectionWebviewMain', () => { }); it('sends selected context data on GET_CONTEXT_SELECTION_DATA', async () => { + const configurationProvider = configurationProviderFactory(); const main = manageSolutionWebviewMainFactory({ - webviewManager + webviewManager, + configurationProvider }); await main.activate(context as unknown as vscode.ExtensionContext); @@ -563,7 +565,7 @@ describe('ContextSelectionWebviewMain', () => { expect(webviewManager.sendMessage).toHaveBeenCalledWith( expect.objectContaining({ type: 'FILE_SELECTED', - data: expect.arrayContaining([expect.stringMatching(/root[/\\]images[/\\]app\.axf$/)]), + data: ['images/app.axf'], for: 'image-path' }) ); From 79df7f0e25af6c8d6b090bbe232d6aebda1abaf7 Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Wed, 11 Mar 2026 17:49:06 +0100 Subject: [PATCH 4/8] fix merge down conflicts --- src/debug/debug-adapters-yaml-file.ts | 1 - src/solutions/solution-manager.factories.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/debug/debug-adapters-yaml-file.ts b/src/debug/debug-adapters-yaml-file.ts index f206ec55d..4e9639002 100644 --- a/src/debug/debug-adapters-yaml-file.ts +++ b/src/debug/debug-adapters-yaml-file.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { PathType } from '../views/manage-solution/messages'; import { createInterfaceFactory } from '../generic/interface-factory'; import { ETextFileResult } from '../generic/text-file'; import { ITreeItem, CTreeItem } from '../generic/tree-item'; diff --git a/src/solutions/solution-manager.factories.ts b/src/solutions/solution-manager.factories.ts index a2c4c4818..f98da09ff 100644 --- a/src/solutions/solution-manager.factories.ts +++ b/src/solutions/solution-manager.factories.ts @@ -28,12 +28,14 @@ export const idleSolutionLoadStateFactory = makeFactory({ solutionPath: () => undefined, loaded: () => undefined, converted: () => undefined, + activated: () => undefined, }); export const activeSolutionLoadStateFactory = makeFactory({ solutionPath: () => path.join(faker.system.filePath(), `${faker.word.noun()}.csolution.yml`), loaded: () => undefined, converted: () => undefined, + activated: () => undefined, }); const fireOnDidChangeLoadState = (emitter: vscode.EventEmitter) => { From 9a0d97a05848fa8143a0c757c2015ec13079c592 Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Wed, 11 Mar 2026 18:07:21 +0100 Subject: [PATCH 5/8] Add type declaration for CSS modules and update imports in ManageSolution component --- src/style.d.ts | 17 +++++++++++++++++ .../view/components/manage-solution.tsx | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/style.d.ts diff --git a/src/style.d.ts b/src/style.d.ts new file mode 100644 index 000000000..8bebb2c3e --- /dev/null +++ b/src/style.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (C) 2025-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 + * + * http://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. + */ + +declare module '*.css'; diff --git a/src/views/manage-solution/view/components/manage-solution.tsx b/src/views/manage-solution/view/components/manage-solution.tsx index 593c93621..29b2bad2e 100644 --- a/src/views/manage-solution/view/components/manage-solution.tsx +++ b/src/views/manage-solution/view/components/manage-solution.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ +import './manage-solution.css'; +import '../../../common/style/antd-overrides.css'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, Checkbox, CheckboxChangeEvent, Col, ConfigProvider, Flex, Input, InputNumber, Row, Spin, Tabs, theme } from 'antd'; import { debounce } from 'lodash'; import * as React from 'react'; import { UISection, UISectionChildren } from '../../../../debug/debug-adapters-yaml-file'; import { CompactDropdown } from '../../../common/components/compact-dropdown'; -import '../../../common/style/antd-overrides.css'; import { useVSCodeTheme } from '../../../hooks/use-vscode-theme'; import { MessageHandler } from '../../../message-handler'; import { IncomingMessage, OutgoingMessage } from '../../messages'; import { GenericPropertyList } from '../state/manage-solution-state'; import { SolutionUpdateAction, contextUpdateReducer, initialState, manageSolutionReducer } from '../state/reducer'; -import './manage-solution.css'; import { ProjectsTable } from './projects-table'; import { TargetsTable } from './targets-table'; import { PathType } from '../../types'; From 3e4c5f025ef2a2c7400c1907b72231d5b4d747ae Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Wed, 11 Mar 2026 18:21:14 +0100 Subject: [PATCH 6/8] Refactor file selection handling to use requestId instead of targetElementId --- .../manage-solution-webview-main.test.ts | 4 +- .../manage-solution-webview-main.ts | 6 +- src/views/manage-solution/messages.ts | 4 +- .../view/components/manage-solution.test.tsx | 19 ++- .../view/components/manage-solution.tsx | 108 ++++++++++++------ .../view/state/reducer.test.ts | 2 +- 6 files changed, 88 insertions(+), 55 deletions(-) diff --git a/src/views/manage-solution/manage-solution-webview-main.test.ts b/src/views/manage-solution/manage-solution-webview-main.test.ts index 0ba317002..9be33b051 100644 --- a/src/views/manage-solution/manage-solution-webview-main.test.ts +++ b/src/views/manage-solution/manage-solution-webview-main.test.ts @@ -551,7 +551,7 @@ describe('ContextSelectionWebviewMain', () => { ]); await fireAndWait('SELECT_FILE', { - targetElementId: 'image-path', + requestId: 'image-path', options: { defaultUri: defaultPath, canSelectMany: false, @@ -566,7 +566,7 @@ describe('ContextSelectionWebviewMain', () => { expect.objectContaining({ type: 'FILE_SELECTED', data: ['images/app.axf'], - for: 'image-path' + requestId: 'image-path' }) ); }); diff --git a/src/views/manage-solution/manage-solution-webview-main.ts b/src/views/manage-solution/manage-solution-webview-main.ts index 8acc02aa9..3499da10d 100644 --- a/src/views/manage-solution/manage-solution-webview-main.ts +++ b/src/views/manage-solution/manage-solution-webview-main.ts @@ -249,7 +249,7 @@ export class ManageSolutionWebviewMain { await this.updateDebuggerParameter(message.service, message.key, message.value, message.pname); break; case 'SELECT_FILE': - await this.selectFileDialog(message.targetElementId, message.options); + await this.selectFileDialog(message.requestId, message.options); break; case 'SET_AUTO_UPDATE': await this.configurationProvider.setConfigVariable(manifest.CONFIG_AUTO_DEBUG_LAUNCH, message.value, undefined, true); @@ -423,7 +423,7 @@ export class ManageSolutionWebviewMain { }); } - private async selectFileDialog(targetElementId: string, options?: FileSelectorOptionsType): Promise { + private async selectFileDialog(requestId: string, options?: FileSelectorOptionsType): Promise { const solutionDir = this.getSolutionDir(); if (options?.defaultUri) { options.defaultUri = URI.file(dirname(options.defaultUri)).toString(); @@ -447,7 +447,7 @@ export class ManageSolutionWebviewMain { ) ); - await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: paths, for: targetElementId }); + await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: paths, requestId }); } } diff --git a/src/views/manage-solution/messages.ts b/src/views/manage-solution/messages.ts index 18ab29fa8..cfd4c6549 100644 --- a/src/views/manage-solution/messages.ts +++ b/src/views/manage-solution/messages.ts @@ -36,7 +36,7 @@ export type OutgoingMessage | { type: 'SAVE_CONTEXT_SELECTION' } | { type: 'OPEN_HELP' } | { type: 'SET_DEBUG_ADAPTER_PROPERTY', service: string | undefined, key: string, value: string | number, pname?: string } - | { type: 'SELECT_FILE', targetElementId: string, options?: FileSelectorOptionsType } + | { type: 'SELECT_FILE', requestId: string, options?: FileSelectorOptionsType } | { type: 'SET_AUTO_UPDATE', value: boolean } | { type: 'TOGGLE_DEBUGGER', value: boolean } | { type: 'TOGGLE_DEBUG_ADAPTER_SECTION', section: string } @@ -52,6 +52,6 @@ export type IncomingMessage | { type: 'ACTIVE_TARGET_SET', data: ActiveTargetSet } | { type: 'IS_DIRTY', data: boolean } | { type: 'IS_BUSY', data: boolean } - | { type: 'FILE_SELECTED', data: string[], for: string } + | { type: 'FILE_SELECTED', data: string[], requestId: string } | { type: 'AUTO_UPDATE', data: boolean } ; diff --git a/src/views/manage-solution/view/components/manage-solution.test.tsx b/src/views/manage-solution/view/components/manage-solution.test.tsx index 3be559b77..4dd913456 100644 --- a/src/views/manage-solution/view/components/manage-solution.test.tsx +++ b/src/views/manage-solution/view/components/manage-solution.test.tsx @@ -584,37 +584,34 @@ describe('ContextSelection', () => { describe('selectFile', () => { - it('does not update input value if FILE_SELECTED message has no data', () => { + it('ignores FILE_SELECTED message with empty data', () => { createContextSelectionComponent(); - - const mockInput = document.createElement('input'); - mockInput.setAttribute('id', 'test-input'); - container.appendChild(mockInput); + listener.mockClear(); React.act(() => { messageHandler.postWindowMessage({ type: 'FILE_SELECTED', - for: 'test-input', + requestId: 'test-input', data: [] }); }); - expect(mockInput.value).toBe(''); + expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_DEBUG_ADAPTER_PROPERTY' })); }); - it('does not update input value if FILE_SELECTED message is for a non-existent element', () => { + it('ignores FILE_SELECTED message for unknown request id', () => { createContextSelectionComponent(); + listener.mockClear(); React.act(() => { messageHandler.postWindowMessage({ type: 'FILE_SELECTED', - for: 'non-existent-input', + requestId: 'non-existent-input', data: ['/path/to/selected/file.exe'] }); }); - const nonExistentInput = container.querySelector('#non-existent-input'); - expect(nonExistentInput).toBeNull(); + expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_DEBUG_ADAPTER_PROPERTY' })); }); }); }); diff --git a/src/views/manage-solution/view/components/manage-solution.tsx b/src/views/manage-solution/view/components/manage-solution.tsx index 29b2bad2e..388f52f29 100644 --- a/src/views/manage-solution/view/components/manage-solution.tsx +++ b/src/views/manage-solution/view/components/manage-solution.tsx @@ -18,7 +18,6 @@ import './manage-solution.css'; import '../../../common/style/antd-overrides.css'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, Checkbox, CheckboxChangeEvent, Col, ConfigProvider, Flex, Input, InputNumber, Row, Spin, Tabs, theme } from 'antd'; -import { debounce } from 'lodash'; import * as React from 'react'; import { UISection, UISectionChildren } from '../../../../debug/debug-adapters-yaml-file'; import { CompactDropdown } from '../../../common/components/compact-dropdown'; @@ -35,10 +34,26 @@ export interface ManageSolutionProps { messageHandler: MessageHandler; } +type PendingFileSelection = { + service: string | undefined; + key: string; + localValueKey: string; +}; + +type SelectFileContext = { + service: string | undefined; + key: string; + localValueKey: string; + title?: string; + defaultUri?: string; + pathType?: PathType; +}; + export const ManageSolution = (props: ManageSolutionProps) => { const [state, dispatch] = React.useReducer(manageSolutionReducer, initialState); // Eager local editable values snapshot (display-layer values). Numbers are stored scaled for user editing. const [localValues, setLocalValues] = React.useState>({}); + const pendingFileSelections = React.useRef>(new Map()); const adapter = React.useMemo( () => state.debugAdapters.find(adapter => adapter.name === state.debugger), @@ -74,14 +89,28 @@ export const ManageSolution = (props: ManageSolutionProps) => { React.useEffect(() => { const handleFileSelected = (message: IncomingMessage) => { - if (message.type === 'FILE_SELECTED' && message.data && message.data.length > 0) { - const element = document.getElementById(message.for || ''); - if (element) { - const ymlNode = element.getAttribute('data-yml-node') || 'config'; - props.messageHandler.push({ type: 'SET_DEBUG_ADAPTER_PROPERTY', service: undefined, key: ymlNode, value: message.data[0] }); - (element as HTMLInputElement).value = message.data[0]; - } + if (message.type !== 'FILE_SELECTED') { + return; } + + const pendingSelection = pendingFileSelections.current.get(message.requestId); + if (!pendingSelection) { + return; + } + pendingFileSelections.current.delete(message.requestId); + + if (!message.data || message.data.length === 0) { + return; + } + + const selectedPath = message.data[0]; + setLocalValues(prev => ({ ...prev, [pendingSelection.localValueKey]: selectedPath })); + props.messageHandler.push({ + type: 'SET_DEBUG_ADAPTER_PROPERTY', + service: pendingSelection.service, + key: pendingSelection.key, + value: selectedPath + }); }; const unsubscribe = props.messageHandler.subscribe(handleFileSelected); @@ -203,34 +232,26 @@ export const ManageSolution = (props: ManageSolutionProps) => { const hasDebugger = !!state.debugger; - const selectFile = React.useMemo(() => { - const handleSelectFile = (e: React.MouseEvent) => { - const input = (e.target as HTMLElement)?.parentElement?.parentNode?.parentNode?.querySelector('input'); - let id = input?.getAttribute('id'); - if (id === null || id === undefined) { - // eslint-disable-next-line react-hooks/purity - id = Math.random().toString(36).substring(2, 15); - input?.setAttribute('id', id); - } - const title = input?.getAttribute('title') || 'Select File'; - const dataOptionPathType = input?.getAttribute('data-option-path-type'); - const optionPathType: PathType = dataOptionPathType === 'absolute' ? 'absolute' : 'relative'; - const currentValue = input?.value || ''; - props.messageHandler.push({ - type: 'SELECT_FILE', - targetElementId: id, - options: { - canSelectMany: false, - defaultUri: currentValue, - openLabel: 'Select File', // title of the dialog button to choose the file - title: title, // dialog title - filters: { 'All Files': ['*'] }, - pathType: optionPathType - } - }); - }; + const selectFile = React.useCallback((context: SelectFileContext) => { + const requestId = `manage-solution-file-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + pendingFileSelections.current.set(requestId, { + service: context.service, + key: context.key, + localValueKey: context.localValueKey, + }); - return debounce(handleSelectFile, 300); + props.messageHandler.push({ + type: 'SELECT_FILE', + requestId, + options: { + canSelectMany: false, + defaultUri: context.defaultUri, + openLabel: 'Select File', + title: context.title || 'Select File', + filters: { 'All Files': ['*'] }, + pathType: context.pathType ?? 'relative' + } + }); }, [props.messageHandler]); // Eager initialization/reset of localValues whenever adapter context changes. @@ -434,7 +455,22 @@ export const ManageSolution = (props: ManageSolutionProps) => { return ( Browse} + addonAfter={ + + } value={(localValues[k] as string) ?? ''} data-yml-node={o['yml-node']} data-option-path-type={o['path-type']} diff --git a/src/views/manage-solution/view/state/reducer.test.ts b/src/views/manage-solution/view/state/reducer.test.ts index 331489feb..347c56cc8 100644 --- a/src/views/manage-solution/view/state/reducer.test.ts +++ b/src/views/manage-solution/view/state/reducer.test.ts @@ -125,7 +125,7 @@ describe('contextSelectionReducer', () => { type: 'INCOMING_MESSAGE', message: { type: 'FILE_SELECTED', data: ['C:/path/to/my/file.txt'], - for: 'some-id', + requestId: 'some-id', } }); From 2c3335a466e03096465ca01fa3ca8a0781281cde Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Thu, 12 Mar 2026 11:55:02 +0100 Subject: [PATCH 7/8] Fix copyright notice formatting in style.d.ts --- src/style.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style.d.ts b/src/style.d.ts index 8bebb2c3e..6efdb8f40 100644 --- a/src/style.d.ts +++ b/src/style.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (C) 2025-2026 Arm Limited + * Copyright 2025-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. From 42a3e7d9e4435e2763c885244a04d98d88ad7c73 Mon Sep 17 00:00:00 2001 From: Arne Schmid Date: Thu, 12 Mar 2026 12:08:24 +0100 Subject: [PATCH 8/8] Add handling for empty file selection in ManageSolutionWebviewMain and corresponding test --- .../manage-solution-webview-main.ts | 2 + .../view/components/manage-solution.test.tsx | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/views/manage-solution/manage-solution-webview-main.ts b/src/views/manage-solution/manage-solution-webview-main.ts index 3499da10d..468a098b2 100644 --- a/src/views/manage-solution/manage-solution-webview-main.ts +++ b/src/views/manage-solution/manage-solution-webview-main.ts @@ -448,6 +448,8 @@ export class ManageSolutionWebviewMain { ); await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: paths, requestId }); + } else { + await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: [], requestId }); } } diff --git a/src/views/manage-solution/view/components/manage-solution.test.tsx b/src/views/manage-solution/view/components/manage-solution.test.tsx index 4dd913456..a4147de25 100644 --- a/src/views/manage-solution/view/components/manage-solution.test.tsx +++ b/src/views/manage-solution/view/components/manage-solution.test.tsx @@ -584,6 +584,50 @@ describe('ContextSelection', () => { describe('selectFile', () => { + it('emits SELECT_FILE on Browse and resolves FILE_SELECTED into adapter update', () => { + createContextSelectionComponent(); + postGenericDataContext(); + listener.mockClear(); + + const filePathInput = container.querySelector('input[data-yml-node="telnet-prop_d"]') as HTMLInputElement; + expect(filePathInput).toBeDefined(); + expect(filePathInput.value).toBe(''); + + const browseButton = container.querySelector('.file-button'); + expect(browseButton).toBeDefined(); + + React.act(() => { + fireEvent.click(browseButton as Element); + }); + + const selectFileMessage = listener.mock.calls + .map(([message]) => message) + .find(message => message.type === 'SELECT_FILE'); + + expect(selectFileMessage).toBeDefined(); + expect(selectFileMessage).toEqual(expect.objectContaining({ + type: 'SELECT_FILE', + requestId: expect.stringMatching(/^manage-solution-file-/), + })); + + const selectedPath = '/path/to/selected/file.axf'; + React.act(() => { + messageHandler.postWindowMessage({ + type: 'FILE_SELECTED', + requestId: selectFileMessage!.requestId, + data: [selectedPath] + }); + }); + + expect(listener).toHaveBeenCalledWith({ + type: 'SET_DEBUG_ADAPTER_PROPERTY', + service: undefined, + key: 'telnet-prop_d', + value: selectedPath, + }); + expect(filePathInput.value).toBe(selectedPath); + }); + it('ignores FILE_SELECTED message with empty data', () => { createContextSelectionComponent(); listener.mockClear();