From b6b740df1f3f850119c7bf1f453713f6c59abaaa Mon Sep 17 00:00:00 2001 From: hsy822 Date: Tue, 19 Aug 2025 15:13:48 +0900 Subject: [PATCH 1/6] fix compile button --- .../src/lib/components/CompileDropdown.tsx | 54 ++++-- libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx | 156 ++++++++++++++++-- 2 files changed, 177 insertions(+), 33 deletions(-) diff --git a/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx b/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx index 4bdd1bfb233..01933ffd1b6 100644 --- a/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx +++ b/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx @@ -25,25 +25,47 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi const compileThen = async (nextAction: () => void) => { setCompileState('compiling') - setTimeout(async () => { - plugin.once('solidity', 'compilationFinished', (data) => { - const hasErrors = data.errors && data.errors.filter(e => e.severity === 'error').length > 0 - if (hasErrors) { - setCompileState('idle') - plugin.call('notification', 'toast', 'Compilation failed') + try { + await plugin.call('fileManager', 'saveCurrentFile') + await plugin.call('manager', 'activatePlugin', 'solidity') + + const mySeq = Date.now() + let watchdog: NodeJS.Timeout | null = null + + const onFinished = async () => { + if (watchdog) clearTimeout(watchdog) + + const fresh = await plugin.call('solidity', 'getCompilationResult').catch(() => null) + + if (!fresh) { + setCompileState('idle') + plugin.call('notification', 'toast', 'Compilation failed (no result). See compiler output.') } else { - setCompileState('compiled') - nextAction() + const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] + if (errs.length > 0) { + setCompileState('idle') + plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + } else { + setCompileState('compiled'); + nextAction(); + } } - }) - - try { - await plugin.call('solidity', 'compile', tabPath) - } catch (e) { - console.error(e) - setCompileState('idle') + try { plugin.off('solidity', 'compilationFinished', onFinished) } catch {} } - }, 0) + + try { plugin.off('solidity', 'compilationFinished', onFinished) } catch {} + plugin.on('solidity', 'compilationFinished', onFinished); + + watchdog = setTimeout(() => { + onFinished() + }, 10000) + + await plugin.call('solidity', 'compile', tabPath) + + } catch (e) { + console.error(e) + setCompileState('idle') + } } const fetchScripts = async () => { diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 0ff12488e58..05d74471241 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -70,7 +70,7 @@ const tabsReducer = (state: ITabsState, action: ITabsAction) => { return state } } -const PlayExtList = ['js', 'ts', 'sol', 'circom', 'vy', 'nr'] +const PlayExtList = ['js', 'ts', 'sol', 'circom', 'vy', 'nr', 'yul'] export const TabsUI = (props: TabsUIProps) => { @@ -83,6 +83,10 @@ export const TabsUI = (props: TabsUIProps) => { tabs.current = props.tabs // we do this to pass the tabs list to the onReady callbacks const appContext = useContext(AppContext) + const compileSeq = useRef(0) + const compileWatchdog = useRef(null) + const settledSeqRef = useRef(0) + const [compileState, setCompileState] = useState<'idle' | 'compiling' | 'compiled'>('idle') useEffect(() => { @@ -193,7 +197,7 @@ export const TabsUI = (props: TabsUIProps) => { setFileDecorations }) return () => { - tabsElement.current.removeEventListener('wheel', transformScroll) + if (tabsElement.current) tabsElement.current.removeEventListener('wheel', transformScroll) } }, []) @@ -208,6 +212,25 @@ export const TabsUI = (props: TabsUIProps) => { setCompileState('idle') }, [tabsState.selectedIndex]) + useEffect(() => { + if (!props.plugin || tabsState.selectedIndex < 0) return + + const currentPath = props.tabs[tabsState.selectedIndex]?.name + if (!currentPath) return + + const listener = (path: string) => { + if (currentPath.endsWith(path)) { + setCompileState('idle') + } + } + + props.plugin.on('editor', 'contentChanged', listener) + + return () => { + props.plugin.off('editor', 'contentChanged') + } + }, [tabsState.selectedIndex, props.plugin, props.tabs]) + const handleCompileAndPublish = async (storageType: 'ipfs' | 'swarm') => { setCompileState('compiling') await props.plugin.call('notification', 'toast', `Switching to Solidity Compiler to publish...`) @@ -307,17 +330,97 @@ export const TabsUI = (props: TabsUIProps) => { } } + const waitForFreshCompilationResult = async ( + mySeq: number, + targetPath: string, + startMs: number, + maxWaitMs = 1500, + intervalMs = 120 + ) => { + const norm = (p: string) => p.replace(/^\/+/, '') + const fileName = norm(targetPath).split('/').pop() || norm(targetPath) + + const hasFile = (res: any) => { + if (!res) return false + const byContracts = + res.contracts && typeof res.contracts === 'object' && + Object.keys(res.contracts).some(k => k.endsWith(fileName) || norm(k) === norm(targetPath)) + const bySources = + res.sources && typeof res.sources === 'object' && + Object.keys(res.sources).some(k => k.endsWith(fileName) || norm(k) === norm(targetPath)) + return byContracts || bySources + } + + let last: any = null + const until = startMs + maxWaitMs + while (Date.now() < until) { + if (mySeq !== compileSeq.current) return null + try { + const res = await props.plugin.call('solidity', 'getCompilationResult') + last = res + const ts = (res && (res.timestamp || res.timeStamp || res.time || res.generatedAt)) || null + const isFreshTime = typeof ts === 'number' ? ts >= startMs : true + if (res && hasFile(res) && isFreshTime) return res + } catch {} + await new Promise(r => setTimeout(r, intervalMs)) + } + return last + } + + const attachCompilationListener = (compilerName: string, mySeq: number, path: string, startedAt: number) => { + try { props.plugin.off(compilerName, 'compilationFinished') } catch {} + + const onFinished = async (_success: boolean) => { + if (mySeq !== compileSeq.current || settledSeqRef.current === mySeq) return + + if (compileWatchdog.current) { + clearTimeout(compileWatchdog.current) + compileWatchdog.current = null + } + + const fresh = await waitForFreshCompilationResult(mySeq, path, startedAt) + + if (!fresh) { + setCompileState('idle') + props.plugin.call('notification', 'toast', 'Compilation failed (no result). See compiler output.') + } else { + const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] + if (errs.length > 0) { + setCompileState('idle') + props.plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + } else { + setCompileState('compiled') + } + } + settledSeqRef.current = mySeq + try { props.plugin.off(compilerName, 'compilationFinished') } catch {} + } + props.plugin.on(compilerName, 'compilationFinished', onFinished) + } + const handleCompileClick = async () => { setCompileState('compiling') _paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt]) try { - const path = active().substr(active().indexOf('/') + 1, active().length) + const activePathRaw = active() + if (!activePathRaw || activePathRaw.indexOf('/') === -1) { + setCompileState('idle') + props.plugin.call('notification', 'toast', 'No file selected.') + return + } + const path = activePathRaw.substr(activePathRaw.indexOf('/') + 1) if (tabsState.currentExt === 'js' || tabsState.currentExt === 'ts') { - const content = await props.plugin.call('fileManager', 'readFile', path) - await props.plugin.call('scriptRunnerBridge', 'execute', content, path) - setCompileState('compiled') + try { + const content = await props.plugin.call('fileManager', 'readFile', path) + await props.plugin.call('scriptRunnerBridge', 'execute', content, path) + setCompileState('compiled') + } catch (e) { + console.error(e) + props.plugin.call('notification', 'toast', `Script error: ${e.message}`) + setCompileState('idle') + } return } @@ -329,26 +432,45 @@ export const TabsUI = (props: TabsUIProps) => { nr: 'noir-compiler' }[tabsState.currentExt] - if (!compilerName) { - setCompileState('idle') - return + if (!compilerName) { + setCompileState('idle') + return } - props.plugin.once(compilerName, 'compilationFinished', (fileName, source, languageVersion, data) => { - const hasErrors = data.errors && data.errors.filter(e => e.severity === 'error').length > 0 - - if (hasErrors) { - setCompileState('idle') - } else { - setCompileState('compiled') + await props.plugin.call('fileManager', 'saveCurrentFile') + await props.plugin.call('manager', 'activatePlugin', compilerName) + + const mySeq = ++compileSeq.current + const startedAt = Date.now() + + attachCompilationListener(compilerName, mySeq, path, startedAt) + + if (compileWatchdog.current) clearTimeout(compileWatchdog.current) + compileWatchdog.current = window.setTimeout(async () => { + if (mySeq !== compileSeq.current || settledSeqRef.current === mySeq) return + const maybe = await props.plugin.call('solidity', 'getCompilationResult').catch(() => null) + if (maybe) { + const fresh = await waitForFreshCompilationResult(mySeq, path, startedAt, 400, 120) + if (fresh) { + const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] + setCompileState(errs.length ? 'idle' : 'compiled') + if (errs.length) props.plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + settledSeqRef.current = mySeq + return + } } - }) + setCompileState('idle') + props.plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + settledSeqRef.current = mySeq + try { props.plugin.off(compilerName, 'compilationFinished') } catch {} + }, 3000) if (tabsState.currentExt === 'vy') { await props.plugin.call(compilerName, 'vyperCompileCustomAction') } else { await props.plugin.call(compilerName, 'compile', path) } + } catch (e) { console.error(e) setCompileState('idle') From 200fd0e16f9ef544b60ca0e8deec2311a2862cec Mon Sep 17 00:00:00 2001 From: hsy822 Date: Wed, 20 Aug 2025 17:50:49 +0900 Subject: [PATCH 2/6] Fix: Address compile button bug --- .../src/lib/components/CompileDropdown.tsx | 33 +++++++++---------- libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx | 25 +++++++++----- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx b/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx index 01933ffd1b6..757691fc496 100644 --- a/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx +++ b/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx @@ -22,7 +22,7 @@ interface CompileDropdownProps { export const CompileDropdown: React.FC = ({ tabPath, plugin, disabled, onNotify, onOpen, onRequestCompileAndPublish, compiledFileName, setCompileState }) => { const [scriptFiles, setScriptFiles] = useState([]) - const compileThen = async (nextAction: () => void) => { + const compileThen = async (nextAction: () => void, actionName: string) => { setCompileState('compiling') try { @@ -37,27 +37,24 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi const fresh = await plugin.call('solidity', 'getCompilationResult').catch(() => null) - if (!fresh) { - setCompileState('idle') - plugin.call('notification', 'toast', 'Compilation failed (no result). See compiler output.') + if (!fresh || (Array.isArray(fresh.errors) && fresh.errors.filter((e: any) => (e.severity || e.type) === 'error').length > 0)) { + setCompileState('idle') + plugin.call('notification', 'toast', `Compilation failed, skipping '${actionName}'.`) + await plugin.call('manager', 'activatePlugin', 'solidity') + await plugin.call('menuicons', 'select', 'solidity') } else { - const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] - if (errs.length > 0) { - setCompileState('idle') - plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') - } else { - setCompileState('compiled'); - nextAction(); - } + setCompileState('compiled') + nextAction() } - try { plugin.off('solidity', 'compilationFinished', onFinished) } catch {} + + try { plugin.off('solidity', 'compilationFinished') } catch {} } - try { plugin.off('solidity', 'compilationFinished', onFinished) } catch {} + try { plugin.off('solidity', 'compilationFinished') } catch {} plugin.on('solidity', 'compilationFinished', onFinished); watchdog = setTimeout(() => { - onFinished() + onFinished() }, 10000) await plugin.call('solidity', 'compile', tabPath) @@ -89,7 +86,7 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi const content = await plugin.call('fileManager', 'readFile', path) await plugin.call('scriptRunnerBridge', 'execute', content, path) onNotify?.(`Executed script: ${path}`) - }) + }, 'Run Script') } const runRemixAnalysis = async () => { @@ -101,7 +98,7 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi } plugin.call('menuicons', 'select', 'solidityStaticAnalysis') onNotify?.("Ran Remix static analysis") - }) + }, 'Run Remix Analysis') } const handleScanContinue = async () => { @@ -109,7 +106,7 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi const firstSlashIndex = compiledFileName.indexOf('/') const finalPath = firstSlashIndex > 0 ? compiledFileName.substring(firstSlashIndex + 1) : compiledFileName await handleSolidityScan(plugin, finalPath) - }) + }, 'Run Solidity Scan') } const runSolidityScan = async () => { diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 05d74471241..54884271bf9 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -233,7 +233,6 @@ export const TabsUI = (props: TabsUIProps) => { const handleCompileAndPublish = async (storageType: 'ipfs' | 'swarm') => { setCompileState('compiling') - await props.plugin.call('notification', 'toast', `Switching to Solidity Compiler to publish...`) await props.plugin.call('manager', 'activatePlugin', 'solidity') await props.plugin.call('menuicons', 'select', 'solidity') @@ -241,7 +240,7 @@ export const TabsUI = (props: TabsUIProps) => { await props.plugin.call('solidity', 'compile', active().substr(active().indexOf('/') + 1, active().length)) _paq.push(['trackEvent', 'editor', 'publishFromEditor', storageType]) - setTimeout(() => { + setTimeout(async () => { let buttonId if (storageType === 'ipfs') { buttonId = 'publishOnIpfs' @@ -254,13 +253,17 @@ export const TabsUI = (props: TabsUIProps) => { if (buttonToClick) { buttonToClick.click() } else { - props.plugin.call('notification', 'toast', 'Could not find the publish button.') + await props.plugin.call('notification', 'toast', `Compilation failed, skipping 'Publish'.`) + await props.plugin.call('manager', 'activatePlugin', 'solidity') + await props.plugin.call('menuicons', 'select', 'solidity') } }, 500) } catch (e) { console.error(e) - await props.plugin.call('notification', 'toast', `Error publishing: ${e.message}`) + await props.plugin.call('notification', 'toast', `Compilation failed, skipping 'Publish'.`) + await props.plugin.call('manager', 'activatePlugin', 'solidity') + await props.plugin.call('menuicons', 'select', 'solidity') } setCompileState('idle') @@ -382,12 +385,14 @@ export const TabsUI = (props: TabsUIProps) => { if (!fresh) { setCompileState('idle') - props.plugin.call('notification', 'toast', 'Compilation failed (no result). See compiler output.') + await props.plugin.call('manager', 'activatePlugin', 'solidity') + await props.plugin.call('menuicons', 'select', 'solidity') } else { const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] if (errs.length > 0) { setCompileState('idle') - props.plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + await props.plugin.call('manager', 'activatePlugin', 'solidity') + await props.plugin.call('menuicons', 'select', 'solidity') } else { setCompileState('compiled') } @@ -454,13 +459,17 @@ export const TabsUI = (props: TabsUIProps) => { if (fresh) { const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] setCompileState(errs.length ? 'idle' : 'compiled') - if (errs.length) props.plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + if (errs.length) { + await props.plugin.call('manager', 'activatePlugin', 'solidity') + await props.plugin.call('menuicons', 'select', 'solidity') + } settledSeqRef.current = mySeq return } } setCompileState('idle') - props.plugin.call('notification', 'toast', 'Compilation failed (errors). See compiler output.') + await props.plugin.call('manager', 'activatePlugin', 'solidity') + await props.plugin.call('menuicons', 'select', 'solidity') settledSeqRef.current = mySeq try { props.plugin.off(compilerName, 'compilationFinished') } catch {} }, 3000) From e4c09544992a74cdd8c84c6c4c2a8f32828732c0 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 21 Aug 2025 11:45:47 +0900 Subject: [PATCH 3/6] fix: prevent duplicate script execution and stale compile result state --- .../src/tests/compile_run_widget.test.ts | 8 +- .../src/lib/components/CompileDropdown.tsx | 107 +++++++++----- .../tabs/src/lib/components/DropdownMenu.css | 130 +++++++++--------- .../tabs/src/lib/components/DropdownMenu.tsx | 73 ++++++---- libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx | 2 - 5 files changed, 196 insertions(+), 124 deletions(-) diff --git a/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts b/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts index f9496639fbd..4818104df4d 100644 --- a/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts +++ b/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts @@ -12,7 +12,7 @@ module.exports = { return [] }, - 'Compile using the widget #group1': function (browser: NightwatchBrowser) { + 'Compile using the widget #group1 #pr': function (browser: NightwatchBrowser) { browser .openFile('contracts/3_Ballot.sol') .click('[data-id="compile-action"]') @@ -20,14 +20,14 @@ module.exports = { .verifyContracts(['Ballot']) }, - 'Run script using the widget #group2': function (browser: NightwatchBrowser) { + 'Run script using the widget #group2 #pr': function (browser: NightwatchBrowser) { browser .openFile('scripts/deploy_with_web3.ts') .click('[data-id="compile-action"]') .waitForElementVisible('[data-id="compile_group"] i.fa-check', 10000) }, - 'Should activate Solidity Static Analysis and show "Solidity Analyzers" title from compile dropdown #group3': function (browser: NightwatchBrowser) { + 'Should activate Solidity Static Analysis and show "Solidity Analyzers" title from compile dropdown #group3 #pr': function (browser: NightwatchBrowser) { browser .openFile('contracts/3_Ballot.sol') .click('[data-id="compile-dropdown-trigger"]') @@ -42,7 +42,7 @@ module.exports = { .verifyContracts(['Ballot']) }, - 'Should run Solidity Scan and display results in terminal #group4': function (browser: NightwatchBrowser) { + 'Should run Solidity Scan and display results in terminal #group4 #pr': function (browser: NightwatchBrowser) { browser .openFile('contracts/3_Ballot.sol') .click('[data-id="compile-dropdown-trigger"]') diff --git a/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx b/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx index 757691fc496..2166eec5734 100644 --- a/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx +++ b/libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx @@ -13,13 +13,12 @@ interface CompileDropdownProps { plugin?: any disabled?: boolean compiledFileName?: string - onNotify?: (msg: string) => void onOpen?: () => void onRequestCompileAndPublish?: (type: string) => void setCompileState: (state: 'idle' | 'compiling' | 'compiled') => void } -export const CompileDropdown: React.FC = ({ tabPath, plugin, disabled, onNotify, onOpen, onRequestCompileAndPublish, compiledFileName, setCompileState }) => { +export const CompileDropdown: React.FC = ({ tabPath, plugin, disabled, onOpen, onRequestCompileAndPublish, compiledFileName, setCompileState }) => { const [scriptFiles, setScriptFiles] = useState([]) const compileThen = async (nextAction: () => void, actionName: string) => { @@ -28,36 +27,87 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi try { await plugin.call('fileManager', 'saveCurrentFile') await plugin.call('manager', 'activatePlugin', 'solidity') - - const mySeq = Date.now() + + const startedAt = Date.now() + const targetPath = tabPath || '' + + const waitForFreshCompilationResult = async ( + path: string, + startMs: number, + maxWaitMs = 1500, + intervalMs = 120 + ) => { + const norm = (p: string) => p.replace(/^\/+/, '') + const fileName = (norm(path).split('/').pop() || norm(path)).toLowerCase() + const hasFile = (res: any) => { + if (!res) return false + const inContracts = + res.contracts && typeof res.contracts === 'object' && + Object.keys(res.contracts).some(k => k.toLowerCase().endsWith(fileName) || norm(k).toLowerCase() === norm(path).toLowerCase()) + const inSources = + res.sources && typeof res.sources === 'object' && + Object.keys(res.sources).some(k => k.toLowerCase().endsWith(fileName) || norm(k).toLowerCase() === norm(path).toLowerCase()) + return inContracts || inSources + } + + let last: any = null + const until = startMs + maxWaitMs + while (Date.now() < until) { + try { + const res = await plugin.call('solidity', 'getCompilationResult') + last = res + const ts = (res && (res.timestamp || res.timeStamp || res.time || res.generatedAt)) || null + const isFreshTime = typeof ts === 'number' ? ts >= startMs : true + if (res && hasFile(res) && isFreshTime) return res + } catch {} + await new Promise(r => setTimeout(r, intervalMs)) + } + return last + } + + let settled = false let watchdog: NodeJS.Timeout | null = null - + const cleanup = () => { + try { plugin.off('solidity', 'compilationFinished', onFinished) } catch {} + if (watchdog) { clearTimeout(watchdog); watchdog = null } + } + + const finishWithErrorUI = async () => { + setCompileState('idle') + await plugin.call('manager', 'activatePlugin', 'solidity') + await plugin.call('menuicons', 'select', 'solidity') + plugin.call('notification', 'toast', `Compilation failed, skipping '${actionName}'.`) + } + const onFinished = async () => { - if (watchdog) clearTimeout(watchdog) - - const fresh = await plugin.call('solidity', 'getCompilationResult').catch(() => null) - - if (!fresh || (Array.isArray(fresh.errors) && fresh.errors.filter((e: any) => (e.severity || e.type) === 'error').length > 0)) { - setCompileState('idle') - plugin.call('notification', 'toast', `Compilation failed, skipping '${actionName}'.`) - await plugin.call('manager', 'activatePlugin', 'solidity') - await plugin.call('menuicons', 'select', 'solidity') - } else { - setCompileState('compiled') - nextAction() + if (settled) return + settled = true + cleanup() + + const fresh = await waitForFreshCompilationResult(targetPath, startedAt).catch(() => null) + if (!fresh) { + await finishWithErrorUI() + return } - try { plugin.off('solidity', 'compilationFinished') } catch {} + const errs = Array.isArray(fresh.errors) + ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') + : [] + + if (errs.length > 0) { + await finishWithErrorUI() + return + } + + setCompileState('compiled') + nextAction() } - try { plugin.off('solidity', 'compilationFinished') } catch {} - plugin.on('solidity', 'compilationFinished', onFinished); - - watchdog = setTimeout(() => { - onFinished() - }, 10000) + plugin.on('solidity', 'compilationFinished', onFinished) + + watchdog = setTimeout(() => { onFinished() }, 10000) - await plugin.call('solidity', 'compile', tabPath) + await plugin.call('solidity', 'compile', targetPath) } catch (e) { console.error(e) @@ -74,10 +124,8 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi } const tsFiles = Object.keys(files).filter(f => f.endsWith('.ts')) setScriptFiles(tsFiles) - onNotify?.(`Loaded ${tsFiles.length} script files`) } catch (err) { console.error("Failed to read scripts directory:", err) - onNotify?.("Failed to read scripts directory") } } @@ -85,7 +133,6 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi await compileThen(async () => { const content = await plugin.call('fileManager', 'readFile', path) await plugin.call('scriptRunnerBridge', 'execute', content, path) - onNotify?.(`Executed script: ${path}`) }, 'Run Script') } @@ -97,7 +144,6 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi await plugin.call('manager', 'activatePlugin', 'solidityStaticAnalysis') } plugin.call('menuicons', 'select', 'solidityStaticAnalysis') - onNotify?.("Ran Remix static analysis") }, 'Run Remix Analysis') } @@ -138,7 +184,6 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi await plugin.call('manager', 'activatePlugin', 'solidity') } plugin.call('menuicons', 'select', 'solidity') - onNotify?.("Ran Remix Solidity Compiler") } const items: MenuItem[] = [ @@ -158,7 +203,7 @@ export const CompileDropdown: React.FC = ({ tabPath, plugi icon: , dataId: 'compile-run-analysis-menu-item', submenu: [ - { label: 'Run Remix Analysis', icon: , onClick: runRemixAnalysis, dataId: 'run-remix-analysis-submenu-item' }, + { label: 'Run Remix Analysis', icon: , onClick: runRemixAnalysis, dataId: 'run-remix-analysis-submenu-item' }, { label: 'Run Solidity Scan', icon: , onClick: runSolidityScan, dataId: 'run-solidity-scan-submenu-item' } ] }, diff --git a/libs/remix-ui/tabs/src/lib/components/DropdownMenu.css b/libs/remix-ui/tabs/src/lib/components/DropdownMenu.css index d2ebc8d8704..8b004ff04e8 100644 --- a/libs/remix-ui/tabs/src/lib/components/DropdownMenu.css +++ b/libs/remix-ui/tabs/src/lib/components/DropdownMenu.css @@ -1,87 +1,89 @@ .custom-dropdown-wrapper { - position: relative; - display: inline-block; - z-index: 1000; + position: relative; + display: inline-block; + z-index: 1000; } .custom-dropdown-trigger { - /* color: var(--text); */ - /* background: var(--custom-select, #2d2f3b); */ - padding: 4px 8px; - border-left: 1px solid var(--bs-secondary); - border-radius: 0 4px 4px 0; - cursor: pointer; - height: 28px; - font-size: 11px; - font-weight: 700; + padding: 4px 8px; + border-left: 1px solid var(--bs-border-color, var(--bs-secondary)); + border-radius: 0 4px 4px 0; + cursor: pointer; + height: 28px; + font-size: 11px; + font-weight: 700; } -.custom-dropdown-panel { - background: var(--custom-select); - border: 1px solid var(--bs-secondary); - border-radius: 4px; - position: absolute; - top: calc(100% + 4px); - left: 0; - color: var(--text); - padding: 0; +.custom-dropdown-panel.dropdown-menu { + padding: 0; + min-width: auto; + z-index: 1310; + isolation: isolate; + background-color: var(--bs-dropdown-bg, var(--bs-body-bg)) !important; + color: var(--bs-dropdown-color, var(--bs-body-color)); + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius, .375rem); + box-shadow: var(--bs-box-shadow, 0 .5rem 1rem rgba(0,0,0,.15)); } -.custom-dropdown-item { - display: flex; - align-items: center; - gap: 5px; - padding: 8px 16px; - color: var(--text); - cursor: pointer; - white-space: nowrap; - position: relative +.custom-dropdown-item.dropdown-item { + display: flex; + align-items: center; + gap: 5px; + color: var(--bs-dropdown-link-color, var(--bs-body-color)); + white-space: nowrap; + position: relative; + padding: 8px 16px; + background-color: transparent; } .custom-dropdown-item-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - display: block; - align-self: center; - line-height: 1; + width: 16px; + height: 16px; + flex-shrink: 0; + display: block; + line-height: 1; } - .custom-dropdown-item > span:not(.custom-dropdown-item-icon) { - flex-grow: 1; - display: flex; - align-items: center; - margin-right: 12px; + flex-grow: 1; + display: flex; + align-items: center; + margin-right: 12px; } - .custom-dropdown-item .custom-dropdown-item-icon:last-child { - margin-left: auto; + margin-left: auto; } -.custom-dropdown-item:hover { - background: var(--bs-secondary); +.custom-dropdown-item.dropdown-item:hover, +.custom-dropdown-submenu .custom-dropdown-item.dropdown-item:hover, +.custom-dropdown-item.dropdown-item:focus, +.custom-dropdown-submenu .custom-dropdown-item.dropdown-item:focus { + background-color: var(--bs-dropdown-link-hover-bg) !important; + color: var(--bs-dropdown-link-hover-color, var(--bs-body-color)) !important; + outline: none; + z-index: 1330; } -.custom-dropdown-submenu { - position: absolute; - top: 0; - left: 100%; - background: var(--custom-select); - border: 1px solid var(--bs-secondary); - border-radius: 4px; - min-width: 200px; - color: var(--text); -} +.custom-dropdown-item.disabled { opacity: 0.4; pointer-events: none; } +.custom-dropdown-item.border-top { border-top: 1px solid var(--bs-border-color); } +.custom-dropdown-item.border-bottom { border-bottom: 1px solid var(--bs-border-color); } -.custom-dropdown-item.disabled { - opacity: 0.4; - pointer-events: none; -} +.custom-dropdown-item.has-submenu { position: relative; } +.custom-dropdown-submenu.dropdown-menu { + position: absolute; + top: 0; + left: calc(100% - var(--bs-border-width, 1px)); + min-width: 200px; + padding: 0; + z-index: 1320; + transform: translateZ(0); + box-shadow: var(--bs-box-shadow); -.custom-dropdown-item.border-top { - border-top: 1px solid var(--bs-secondary); + background-color: var(--bs-dropdown-bg, var(--bs-body-bg)) !important; + color: var(--bs-dropdown-color, var(--bs-body-color)); + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius, .375rem); } - -.custom-dropdown-item.border-bottom { - border-bottom: 1px solid var(--bs-secondary); +.custom-dropdown-submenu .custom-dropdown-item.bg-light { + background-color: transparent !important; } diff --git a/libs/remix-ui/tabs/src/lib/components/DropdownMenu.tsx b/libs/remix-ui/tabs/src/lib/components/DropdownMenu.tsx index 0735b23b6cf..53ff83011fa 100644 --- a/libs/remix-ui/tabs/src/lib/components/DropdownMenu.tsx +++ b/libs/remix-ui/tabs/src/lib/components/DropdownMenu.tsx @@ -20,13 +20,15 @@ interface DropdownMenuProps { panelDataId?: string } -const DropdownMenu: React.FC = ({ items, disabled, onOpen, triggerDataId, panelDataId }) => { +const DropdownMenu: React.FC = ({ + items, disabled, onOpen, triggerDataId, panelDataId +}) => { const [open, setOpen] = useState(false) const [activeSubmenu, setActiveSubmenu] = useState(null) const ref = useRef(null) useEffect(() => { - function handleClickOutside(event: MouseEvent) { + const handleClickOutside = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) { setOpen(false) setActiveSubmenu(null) @@ -37,13 +39,17 @@ const DropdownMenu: React.FC = ({ items, disabled, onOpen, tr }, []) return ( -
+
- - {open && ( -
- {items.map((item, idx) => ( +
+ {items.map((item, idx) => { + const hasSub = !!item.submenu?.length + return (
!disabled && item.submenu && setActiveSubmenu(idx)} + className={[ + 'dropdown-item', + 'custom-dropdown-item', + disabled ? 'disabled' : '', + item.borderTop ? 'border-top' : '', + item.borderBottom ? 'border-bottom' : '', + hasSub ? 'has-submenu' : '' + ].join(' ')} + onMouseEnter={() => !disabled && hasSub && setActiveSubmenu(idx)} onMouseLeave={() => !disabled && setActiveSubmenu(null)} onClick={() => { if (!disabled && item.onClick) { @@ -67,25 +84,35 @@ const DropdownMenu: React.FC = ({ items, disabled, onOpen, tr } }} data-id={item.dataId} + role="menuitem" > {item.icon && {item.icon}} {item.label} - {item.submenu && } + {hasSub && ( + + + + )} - {activeSubmenu === idx && item.submenu && ( -
- {item.submenu.map((sub, subIdx) => ( + {activeSubmenu === idx && hasSub && ( +
+ {item.submenu!.map((sub, subIdx) => (
{ - if (!disabled && sub.onClick){ + if (!disabled && sub.onClick) { sub.onClick() setOpen(false) } - } - } + }} data-id={sub.dataId} + role="menuitem" > {sub.icon && {sub.icon}} {sub.label} @@ -94,11 +121,11 @@ const DropdownMenu: React.FC = ({ items, disabled, onOpen, tr
)}
- ))} -
- )} + ) + })} +
) } -export default DropdownMenu \ No newline at end of file +export default DropdownMenu diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 54884271bf9..00b4a963421 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -546,7 +546,6 @@ export const TabsUI = (props: TabsUIProps) => { console.log(msg)} disabled={!(PlayExtList.includes(tabsState.currentExt)) || compileState === 'compiling'} /> ) : ( @@ -556,7 +555,6 @@ export const TabsUI = (props: TabsUIProps) => { compiledFileName={active()} plugin={props.plugin} disabled={!(PlayExtList.includes(tabsState.currentExt)) || compileState === 'compiling'} - onNotify={(msg) => console.log(msg)} onRequestCompileAndPublish={handleCompileAndPublish} setCompileState={setCompileState} /> From 27beae315e1efb5228df0317efb9346b85618f4c Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 21 Aug 2025 17:48:13 +0900 Subject: [PATCH 4/6] remove #pr --- apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts b/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts index 4818104df4d..f9496639fbd 100644 --- a/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts +++ b/apps/remix-ide-e2e/src/tests/compile_run_widget.test.ts @@ -12,7 +12,7 @@ module.exports = { return [] }, - 'Compile using the widget #group1 #pr': function (browser: NightwatchBrowser) { + 'Compile using the widget #group1': function (browser: NightwatchBrowser) { browser .openFile('contracts/3_Ballot.sol') .click('[data-id="compile-action"]') @@ -20,14 +20,14 @@ module.exports = { .verifyContracts(['Ballot']) }, - 'Run script using the widget #group2 #pr': function (browser: NightwatchBrowser) { + 'Run script using the widget #group2': function (browser: NightwatchBrowser) { browser .openFile('scripts/deploy_with_web3.ts') .click('[data-id="compile-action"]') .waitForElementVisible('[data-id="compile_group"] i.fa-check', 10000) }, - 'Should activate Solidity Static Analysis and show "Solidity Analyzers" title from compile dropdown #group3 #pr': function (browser: NightwatchBrowser) { + 'Should activate Solidity Static Analysis and show "Solidity Analyzers" title from compile dropdown #group3': function (browser: NightwatchBrowser) { browser .openFile('contracts/3_Ballot.sol') .click('[data-id="compile-dropdown-trigger"]') @@ -42,7 +42,7 @@ module.exports = { .verifyContracts(['Ballot']) }, - 'Should run Solidity Scan and display results in terminal #group4 #pr': function (browser: NightwatchBrowser) { + 'Should run Solidity Scan and display results in terminal #group4': function (browser: NightwatchBrowser) { browser .openFile('contracts/3_Ballot.sol') .click('[data-id="compile-dropdown-trigger"]') From ec7b1bd3dfa8fa834ab88fe17f1ced8d6b71e1e9 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 21 Aug 2025 18:28:02 +0900 Subject: [PATCH 5/6] lint --- libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 00b4a963421..d7981f58d9e 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -219,15 +219,15 @@ export const TabsUI = (props: TabsUIProps) => { if (!currentPath) return const listener = (path: string) => { - if (currentPath.endsWith(path)) { - setCompileState('idle') + if (currentPath.endsWith(path)) { + setCompileState('idle') } } props.plugin.on('editor', 'contentChanged', listener) return () => { - props.plugin.off('editor', 'contentChanged') + props.plugin.off('editor', 'contentChanged') } }, [tabsState.selectedIndex, props.plugin, props.tabs]) @@ -376,9 +376,9 @@ export const TabsUI = (props: TabsUIProps) => { const onFinished = async (_success: boolean) => { if (mySeq !== compileSeq.current || settledSeqRef.current === mySeq) return - if (compileWatchdog.current) { - clearTimeout(compileWatchdog.current) - compileWatchdog.current = null + if (compileWatchdog.current) { + clearTimeout(compileWatchdog.current) + compileWatchdog.current = null } const fresh = await waitForFreshCompilationResult(mySeq, path, startedAt) @@ -437,9 +437,9 @@ export const TabsUI = (props: TabsUIProps) => { nr: 'noir-compiler' }[tabsState.currentExt] - if (!compilerName) { - setCompileState('idle') - return + if (!compilerName) { + setCompileState('idle') + return } await props.plugin.call('fileManager', 'saveCurrentFile') @@ -462,7 +462,7 @@ export const TabsUI = (props: TabsUIProps) => { if (errs.length) { await props.plugin.call('manager', 'activatePlugin', 'solidity') await props.plugin.call('menuicons', 'select', 'solidity') - } + } settledSeqRef.current = mySeq return } @@ -479,7 +479,7 @@ export const TabsUI = (props: TabsUIProps) => { } else { await props.plugin.call(compilerName, 'compile', path) } - + } catch (e) { console.error(e) setCompileState('idle') From 4696a6755bee79eec1b69e7b874a55c44d252d1b Mon Sep 17 00:00:00 2001 From: aniket-engg Date: Thu, 21 Aug 2025 18:46:25 +0530 Subject: [PATCH 6/6] activate relevant compiler plugin --- libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index d7981f58d9e..5b3d0598a9b 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -460,16 +460,16 @@ export const TabsUI = (props: TabsUIProps) => { const errs = Array.isArray(fresh.errors) ? fresh.errors.filter((e: any) => (e.severity || e.type) === 'error') : [] setCompileState(errs.length ? 'idle' : 'compiled') if (errs.length) { - await props.plugin.call('manager', 'activatePlugin', 'solidity') - await props.plugin.call('menuicons', 'select', 'solidity') + await props.plugin.call('manager', 'activatePlugin', compilerName) + await props.plugin.call('menuicons', 'select', compilerName) } settledSeqRef.current = mySeq return } } setCompileState('idle') - await props.plugin.call('manager', 'activatePlugin', 'solidity') - await props.plugin.call('menuicons', 'select', 'solidity') + await props.plugin.call('manager', 'activatePlugin', compilerName) + await props.plugin.call('menuicons', 'select', compilerName) settledSeqRef.current = mySeq try { props.plugin.off(compilerName, 'compilationFinished') } catch {} }, 3000)