Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 91 additions & 27 deletions libs/remix-ui/tabs/src/lib/components/CompileDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,106 @@ 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<CompileDropdownProps> = ({ tabPath, plugin, disabled, onNotify, onOpen, onRequestCompileAndPublish, compiledFileName, setCompileState }) => {
export const CompileDropdown: React.FC<CompileDropdownProps> = ({ tabPath, plugin, disabled, onOpen, onRequestCompileAndPublish, compiledFileName, setCompileState }) => {
const [scriptFiles, setScriptFiles] = useState<string[]>([])

const compileThen = async (nextAction: () => void) => {
const compileThen = async (nextAction: () => void, actionName: string) => {
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')
} else {
setCompileState('compiled')
nextAction()
try {
await plugin.call('fileManager', 'saveCurrentFile')
await plugin.call('manager', 'activatePlugin', 'solidity')

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
}
})

try {
await plugin.call('solidity', 'compile', tabPath)
} catch (e) {
console.error(e)
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}'.`)
}
}, 0)

const onFinished = async () => {
if (settled) return
settled = true
cleanup()

const fresh = await waitForFreshCompilationResult(targetPath, startedAt).catch(() => null)
if (!fresh) {
await finishWithErrorUI()
return
}

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()
}

plugin.on('solidity', 'compilationFinished', onFinished)

watchdog = setTimeout(() => { onFinished() }, 10000)

await plugin.call('solidity', 'compile', targetPath)

} catch (e) {
console.error(e)
setCompileState('idle')
}
}

const fetchScripts = async () => {
Expand All @@ -55,19 +124,16 @@ export const CompileDropdown: React.FC<CompileDropdownProps> = ({ 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")
}
}

const runScript = async (path: string) => {
await compileThen(async () => {
const content = await plugin.call('fileManager', 'readFile', path)
await plugin.call('scriptRunnerBridge', 'execute', content, path)
onNotify?.(`Executed script: ${path}`)
})
}, 'Run Script')
}

const runRemixAnalysis = async () => {
Expand All @@ -78,16 +144,15 @@ export const CompileDropdown: React.FC<CompileDropdownProps> = ({ tabPath, plugi
await plugin.call('manager', 'activatePlugin', 'solidityStaticAnalysis')
}
plugin.call('menuicons', 'select', 'solidityStaticAnalysis')
onNotify?.("Ran Remix static analysis")
})
}, 'Run Remix Analysis')
}

const handleScanContinue = async () => {
await compileThen(async () => {
const firstSlashIndex = compiledFileName.indexOf('/')
const finalPath = firstSlashIndex > 0 ? compiledFileName.substring(firstSlashIndex + 1) : compiledFileName
await handleSolidityScan(plugin, finalPath)
})
}, 'Run Solidity Scan')
}

const runSolidityScan = async () => {
Expand Down Expand Up @@ -119,7 +184,6 @@ export const CompileDropdown: React.FC<CompileDropdownProps> = ({ tabPath, plugi
await plugin.call('manager', 'activatePlugin', 'solidity')
}
plugin.call('menuicons', 'select', 'solidity')
onNotify?.("Ran Remix Solidity Compiler")
}

const items: MenuItem[] = [
Expand All @@ -139,7 +203,7 @@ export const CompileDropdown: React.FC<CompileDropdownProps> = ({ tabPath, plugi
icon: <ArrowRightBig />,
dataId: 'compile-run-analysis-menu-item',
submenu: [
{ label: 'Run Remix Analysis', icon: <AnalysisLogo />, onClick: runRemixAnalysis, dataId: 'run-remix-analysis-submenu-item' },
{ label: 'Run Remix Analysis', icon: <SettingsLogo />, onClick: runRemixAnalysis, dataId: 'run-remix-analysis-submenu-item' },
{ label: 'Run Solidity Scan', icon: <SolidityScanLogo />, onClick: runSolidityScan, dataId: 'run-solidity-scan-submenu-item' }
]
},
Expand Down
130 changes: 66 additions & 64 deletions libs/remix-ui/tabs/src/lib/components/DropdownMenu.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading