From bd5c7c6bcf434fd1a592a33cca8a7f723eee0aa4 Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Mon, 8 Jun 2026 13:11:26 +0800 Subject: [PATCH 1/2] fix: drive project settings dropdowns via native change events After the React 19 / @vscode-elements/elements migration, the vscode-single-select element renders its options in the shadow DOM and only emits a native 'change' event on the host. The onClick handlers on the slotted vscode-option children never fired, so selecting a project never updated activeProjectIndex and the compliance/source/target dropdowns never updated. As a result, project settings (e.g. 'Store information about method parameters') always read from and wrote to the first project in a multi-root/multi-project workspace. Switch both ProjectSelector and CompilerConfigurationView to listen for the native 'change' event (reading selectedIndex/value) and sync the rendered selection back from redux state, matching the working JdkRuntime pattern. Fixes #1650 --- .../features/CompilerConfigurationView.tsx | 95 +++++++++++++------ .../features/component/ProjectSelector.tsx | 42 +++++++- 2 files changed, 101 insertions(+), 36 deletions(-) diff --git a/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx b/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx index 28d702f9..6344d8e9 100644 --- a/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx +++ b/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx @@ -11,7 +11,7 @@ import "@vscode-elements/elements/dist/vscode-table-row/index.js"; import "@vscode-elements/elements/dist/vscode-table-cell/index.js"; -import { Dispatch, useEffect } from "react"; +import { Dispatch, useCallback, useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; import { updateCompilerSettings, updateAvailableComplianceLevels } from "./compilerConfigurationViewSlice"; import { CompilerRequest } from "../../vscode/utils"; @@ -54,6 +54,10 @@ const CompilerConfigurationView = (): JSX.Element | null => { Number(sourceLevel) > currentJdkComplianceLevel || Number(targetLevel) > currentJdkComplianceLevel; + const complianceRef = useRef(null); + const sourceRef = useRef(null); + const targetRef = useRef(null); + const onMessage = (event: any) => { const message = event.data; if (message.command === "compiler.onDidGetAvailableComplianceLevels") { @@ -73,7 +77,58 @@ const CompilerConfigurationView = (): JSX.Element | null => { } }, []); - const jdkLevels = (selectedLevel: string, label: string, onClick: (value: string) => void) => { + // The vscode-single-select element renders its options in the shadow DOM and + // only emits a native `change` event on the host element, so the click handlers + // on the slotted vscode-option children never fire. Listen to `change` instead. + const useSelectChange = (ref: React.RefObject, onChange: (value: string) => void) => { + useEffect(() => { + const el = ref.current; + if (!el) { + return; + } + const handler = (e: Event) => { + const value = (e.target as any).value; + if (value) { + onChange(value); + } + }; + el.addEventListener("change", handler); + return () => el.removeEventListener("change", handler); + }, [ref, onChange]); + }; + + const onChangeComplianceLevel = useCallback((value: string) => { + dispatch(updateCompilerSettings({ activeProjectIndex, complianceLevel: value })); + }, [dispatch, activeProjectIndex]); + const onChangeSourceLevel = useCallback((value: string) => { + dispatch(updateCompilerSettings({ activeProjectIndex, sourceLevel: value })); + }, [dispatch, activeProjectIndex]); + const onChangeTargetLevel = useCallback((value: string) => { + dispatch(updateCompilerSettings({ activeProjectIndex, targetLevel: value })); + }, [dispatch, activeProjectIndex]); + + useSelectChange(complianceRef, onChangeComplianceLevel); + useSelectChange(sourceRef, onChangeSourceLevel); + useSelectChange(targetRef, onChangeTargetLevel); + + // Keep the rendered selection in sync with the redux state. + useEffect(() => { + if (complianceRef.current && complianceLevel) { + (complianceRef.current as any).value = complianceLevel; + } + }, [complianceLevel, availableComplianceLevels]); + useEffect(() => { + if (sourceRef.current && sourceLevel) { + (sourceRef.current as any).value = sourceLevel; + } + }, [sourceLevel, availableComplianceLevels]); + useEffect(() => { + if (targetRef.current && targetLevel) { + (targetRef.current as any).value = targetLevel; + } + }, [targetLevel, availableComplianceLevels]); + + const jdkLevels = (selectedLevel: string, label: string) => { return availableComplianceLevels.map((level) => { return ( @@ -81,8 +136,7 @@ const CompilerConfigurationView = (): JSX.Element | null => { className="setting-section-option" key={`${label}-${level}`} value={level} - selected={level === selectedLevel} - onClick={() => onClick(level)} + selected={level === selectedLevel ? true : undefined} > {level} @@ -104,27 +158,6 @@ const CompilerConfigurationView = (): JSX.Element | null => { })); }; - const onClickComplianceLevel = (value: string) => { - dispatch(updateCompilerSettings({ - activeProjectIndex, - complianceLevel: value - })); - }; - - const onClickSourceLevel = (value: string) => { - dispatch(updateCompilerSettings({ - activeProjectIndex, - sourceLevel: value - })); - }; - - const onClickTargetLevel = (value: string) => { - dispatch(updateCompilerSettings({ - activeProjectIndex, - targetLevel: value - })); - }; - const onClickGenerateDebugInfo = (e: any) => { dispatch(updateCompilerSettings({ activeProjectIndex, @@ -158,8 +191,8 @@ const CompilerConfigurationView = (): JSX.Element | null => { Bytecode version: - - {jdkLevels(complianceLevel, "compliance", onClickComplianceLevel)} + + {jdkLevels(complianceLevel, "compliance")} @@ -168,8 +201,8 @@ const CompilerConfigurationView = (): JSX.Element | null => { Source compatibility: - - {jdkLevels(sourceLevel, "source", onClickSourceLevel)} + + {jdkLevels(sourceLevel, "source")} @@ -178,8 +211,8 @@ const CompilerConfigurationView = (): JSX.Element | null => { Target compatibility: - - {jdkLevels(targetLevel, "target", onClickTargetLevel)} + + {jdkLevels(targetLevel, "target")} diff --git a/src/project-settings/assets/mainpage/features/component/ProjectSelector.tsx b/src/project-settings/assets/mainpage/features/component/ProjectSelector.tsx index cc1aedf9..085627ce 100644 --- a/src/project-settings/assets/mainpage/features/component/ProjectSelector.tsx +++ b/src/project-settings/assets/mainpage/features/component/ProjectSelector.tsx @@ -4,7 +4,7 @@ import "@vscode-elements/elements/dist/vscode-single-select/index.js"; import "@vscode-elements/elements/dist/vscode-option/index.js"; -import { useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { useSelector, useDispatch } from "react-redux"; import { ProjectInfo } from "../../../../types"; import { Dispatch } from "@reduxjs/toolkit"; @@ -17,10 +17,37 @@ const ProjectSelector = (): JSX.Element | null => { const projects: ProjectInfo[] = useSelector((state: any) => state.commonConfig.data.projects); const dispatch: Dispatch = useDispatch(); + const selectRef = useRef(null); - const handleActiveProjectChange = (index: number) => { + const handleActiveProjectChange = useCallback((index: number) => { dispatch(activeProjectChange(index)); - }; + }, [dispatch]); + + // The vscode-single-select element renders its options in the shadow DOM and + // only emits a native `change` event on the host element, so the click handlers + // on the slotted vscode-option children never fire. Listen to `change` instead. + useEffect(() => { + const el = selectRef.current; + if (!el) { + return; + } + const onChange = (e: Event) => { + const index = (e.target as any).selectedIndex; + if (typeof index === "number" && index >= 0) { + handleActiveProjectChange(index); + } + }; + el.addEventListener("change", onChange); + return () => el.removeEventListener("change", onChange); + }, [handleActiveProjectChange]); + + // Keep the rendered selection in sync with the active project index. + useEffect(() => { + const el = selectRef.current; + if (el && projects.length > 0) { + (el as any).selectedIndex = activeProjectIndex; + } + }, [activeProjectIndex, projects]); useEffect(() => { if (projects.length === 0) { @@ -38,7 +65,12 @@ const ProjectSelector = (): JSX.Element | null => { } return ( - handleActiveProjectChange(index)}> + {project.name} ); @@ -48,7 +80,7 @@ const ProjectSelector = (): JSX.Element | null => {
Project: - + {projectSelections}
From d1bc5f950dc46fbdce0f1dd4f5160e288e3c291f Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Mon, 8 Jun 2026 13:36:03 +0800 Subject: [PATCH 2/2] fix: always sync compiler dropdowns to redux state Set the vscode-single-select value unconditionally in the sync effects so the dropdown reflects redux state even when the level is empty (e.g. transiently while switching projects), instead of keeping the previous project's selection. Addresses PR review feedback on #1655. --- .../compiler/features/CompilerConfigurationView.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx b/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx index 6344d8e9..fa60a71f 100644 --- a/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx +++ b/src/project-settings/assets/compiler/features/CompilerConfigurationView.tsx @@ -111,19 +111,21 @@ const CompilerConfigurationView = (): JSX.Element | null => { useSelectChange(sourceRef, onChangeSourceLevel); useSelectChange(targetRef, onChangeTargetLevel); - // Keep the rendered selection in sync with the redux state. + // Keep the rendered selection in sync with the redux state. Set the value + // unconditionally (even when the level is "") so the dropdown always reflects + // state instead of keeping the previous project's selection. useEffect(() => { - if (complianceRef.current && complianceLevel) { + if (complianceRef.current) { (complianceRef.current as any).value = complianceLevel; } }, [complianceLevel, availableComplianceLevels]); useEffect(() => { - if (sourceRef.current && sourceLevel) { + if (sourceRef.current) { (sourceRef.current as any).value = sourceLevel; } }, [sourceLevel, availableComplianceLevels]); useEffect(() => { - if (targetRef.current && targetLevel) { + if (targetRef.current) { (targetRef.current as any).value = targetLevel; } }, [targetLevel, availableComplianceLevels]);