Skip to content
244 changes: 228 additions & 16 deletions apps/remix-ide/src/app/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
import { PluginViewWrapper } from '@remix-ui/helper'

import { startTypeLoadingProcess } from './type-fetcher'

const EventManager = require('../../lib/events')

const profile = {
Expand Down Expand Up @@ -71,12 +73,26 @@ export default class Editor extends Plugin {
this.api = {}
this.dispatch = null
this.ref = null

this.monaco = null
this.typeLoaderDebounce = null

this.tsModuleMappings = {}
this.processedPackages = new Set()

this.typesLoadingCount = 0
this.shimDisposers = new Map()
}


setDispatch (dispatch) {
this.dispatch = dispatch
}

setMonaco (monaco) {
this.monaco = monaco
}

updateComponent(state) {
return <EditorUI
editorAPI={state.api}
Expand All @@ -86,6 +102,7 @@ export default class Editor extends Plugin {
events={state.events}
plugin={state.plugin}
isDiff={state.isDiff}
setMonaco={(monaco) => this.setMonaco(monaco)}
/>
}

Expand Down Expand Up @@ -128,6 +145,26 @@ export default class Editor extends Plugin {

async onActivation () {
this.activated = true
this.on('editor', 'editorMounted', () => {
if (!this.monaco) return
const ts = this.monaco.languages.typescript
const tsDefaults = ts.typescriptDefaults

tsDefaults.setCompilerOptions({
moduleResolution: ts.ModuleResolutionKind.NodeNext,
module: ts.ModuleKind.NodeNext,
target: ts.ScriptTarget.ES2022,
lib: ['es2022', 'dom', 'dom.iterable'],
allowNonTsExtensions: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true,
baseUrl: 'file:///node_modules/',
paths: this.tsModuleMappings,
})
tsDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false })
ts.typescriptDefaults.setEagerModelSync(true)
console.log('[DIAGNOSE-SETUP] CompilerOptions set to NodeNext and diagnostics enabled')
})
this.on('sidePanel', 'focusChanged', (name) => {
this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
this.keepDecorationsFor(name, 'markerPerFile')
Expand Down Expand Up @@ -156,27 +193,202 @@ export default class Editor extends Plugin {
this.off('sidePanel', 'pluginDisabled')
}

async _onChange (file) {
this.triggerEvent('didChangeFile', [file])
const currentFile = await this.call('fileManager', 'file')
if (!currentFile) {
return
updateTsCompilerOptions() {
if (!this.monaco) return
console.log('[DIAGNOSE-PATHS] Updating TS compiler options...')
console.log('[DIAGNOSE-PATHS] Current path mappings:', JSON.stringify(this.tsModuleMappings, null, 2))

const tsDefaults = this.monaco.languages.typescript.typescriptDefaults
const currentOptions = tsDefaults.getCompilerOptions()

tsDefaults.setCompilerOptions({
...currentOptions,
paths: { ...currentOptions.paths, ...this.tsModuleMappings }
})
console.log('[DIAGNOSE-PATHS] TS compiler options updated.')
}

toggleTsDiagnostics(enable) {
if (!this.monaco) return
const ts = this.monaco.languages.typescript
ts.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: !enable,
noSyntaxValidation: false
})
console.log(`[DIAGNOSE-DIAG] Semantic diagnostics ${enable ? 'enabled' : 'disabled'}`)
}

addShimForPackage(pkg) {
if (!this.monaco) return
const tsDefaults = this.monaco.languages.typescript.typescriptDefaults

const shimMainPath = `file:///__shims__/${pkg}.d.ts`
const shimWildPath = `file:///__shims__/${pkg}__wildcard.d.ts`

if (!this.shimDisposers.has(shimMainPath)) {
const d1 = tsDefaults.addExtraLib(`declare module '${pkg}' { const _default: any\nexport = _default }`, shimMainPath)
this.shimDisposers.set(shimMainPath, d1)
}
if (currentFile !== file) {
return

if (!this.shimDisposers.has(shimWildPath)) {
const d2 = tsDefaults.addExtraLib(`declare module '${pkg}/*' { const _default: any\nexport = _default }`, shimWildPath)
this.shimDisposers.set(shimWildPath, d2)
}
const input = this.get(currentFile)
if (!input) {
return

this.tsModuleMappings[pkg] = [shimMainPath.replace('file:///', '')]
this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`]
}

removeShimsForPackage(pkg) {
const keys = [`file:///__shims__/${pkg}.d.ts`, `file:///__shims__/${pkg}__wildcard.d.ts`]
for (const k of keys) {
const disp = this.shimDisposers.get(k)
if (disp && typeof disp.dispose === 'function') {
disp.dispose()
this.shimDisposers.delete(k)
}
}
}

beginTypesBatch() {
if (this.typesLoadingCount === 0) {
this.toggleTsDiagnostics(false)
this.triggerEvent('typesLoading', ['start'])
console.log('[DIAGNOSE-BATCH] Types batch started')
}
// if there's no change, don't do anything
if (input === this.previousInput) {
return
this.typesLoadingCount++
}

endTypesBatch() {
this.typesLoadingCount = Math.max(0, this.typesLoadingCount - 1)
if (this.typesLoadingCount === 0) {
this.updateTsCompilerOptions()
this.toggleTsDiagnostics(true)
this.triggerEvent('typesLoading', ['end'])
console.log('[DIAGNOSE-BATCH] Types batch ended')
}
}

addExtraLibs(libs) {
if (!this.monaco || !libs || libs.length === 0) return
console.log(`[DIAGNOSE-LIBS] Adding ${libs.length} new files to Monaco...`)

const tsDefaults = this.monaco.languages.typescript.typescriptDefaults

libs.forEach(lib => {
if (!tsDefaults.getExtraLibs()[lib.filePath]) {
tsDefaults.addExtraLib(lib.content, lib.filePath)
}
})
console.log(`[DIAGNOSE-LIBS] Files added. Total extra libs now: ${Object.keys(tsDefaults.getExtraLibs()).length}.`)
}

// [2/4] The conductor, called on every editor content change to parse 'import' statements and trigger the type loading process.
async _onChange (file) {
this.triggerEvent('didChangeFile', [file])

if (this.monaco && (file.endsWith('.ts') || file.endsWith('.js'))) {
clearTimeout(this.typeLoaderDebounce)
this.typeLoaderDebounce = setTimeout(async () => {
if (!this.monaco) return
const model = this.monaco.editor.getModel(this.monaco.Uri.parse(file))
if (!model) return
const code = model.getValue()

try {
const IMPORT_ANY_RE =
/(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g

const rawImports = [...code.matchAll(IMPORT_ANY_RE)]
.map(m => (m[1] || m[2] || m[3] || '').trim())
.filter(p => p && !p.startsWith('.') && !p.startsWith('file://'))

const uniqueImports = [...new Set(rawImports)]
const getBasePackage = (p) => p.startsWith('@') ? p.split('/').slice(0, 2).join('/') : p.split('/')[0]

const newBasePackages = [...new Set(uniqueImports.map(getBasePackage))]
.filter(p => !this.processedPackages.has(p))

if (newBasePackages.length === 0) return

console.log('[DIAGNOSE] New base packages for analysis:', newBasePackages)

// Temporarily disable type checking during type loading to prevent error flickering.
this.beginTypesBatch()

// [Phase 1: Fast Feedback]
// Add temporary type definitions (shims) first to immediately remove red underlines on import statements.
uniqueImports.forEach(pkgImport => {
this.addShimForPackage(pkgImport)
})

this.updateTsCompilerOptions()
console.log('[DIAGNOSE] Shims added. Red lines should disappear.')

// [Phase 2: Deep Analysis]
// In the background, fetch the actual type files to enable autocompletion.
await Promise.all(newBasePackages.map(async (basePackage) => {
this.processedPackages.add(basePackage)

const activeRunnerLibs = await this.call('scriptRunnerBridge', 'getActiveRunnerLibs')

const libInfo = activeRunnerLibs.find(lib => lib.name === basePackage)
const packageToLoad = libInfo ? `${libInfo.name}@${libInfo.version}` : basePackage

console.log(`[DIAGNOSE] Preparing to load types for: "${packageToLoad}"`)

try {
const result = await startTypeLoadingProcess(packageToLoad)
if (result && result.libs && result.libs.length > 0) {
console.log(`[DIAGNOSE-DEEP-PASS] "${basePackage}" deep pass complete. Adding ${result.libs.length} files.`)
// Add all fetched type files to Monaco.
this.addExtraLibs(result.libs)

// Update path mappings so TypeScript can find the types.
if (result.subpathMap) {
for (const [subpath, virtualPath] of Object.entries(result.subpathMap)) {
this.tsModuleMappings[subpath] = [virtualPath]
}
}
if (result.mainVirtualPath) {
this.tsModuleMappings[basePackage] = [result.mainVirtualPath.replace('file:///node_modules/', '')]
}
this.tsModuleMappings[`${basePackage}/*`] = [`${basePackage}/*`]

// Remove the temporary shims now that the real types are loaded.
uniqueImports
.filter(p => getBasePackage(p) === basePackage)
.forEach(p => this.removeShimsForPackage(p))

} else {
// Shim will remain if no types are found.
console.warn(`[DIAGNOSE-DEEP-PASS] No types found for "${basePackage}". Shim will remain.`)
}
} catch (e) {
// Crawler can fail, but we don't want to crash the whole process.
console.error(`[DIAGNOSE-DEEP-PASS] Crawler failed for "${basePackage}":`, e)
}
}))

console.log('[DIAGNOSE] All processes finished.')
// After all type loading is complete, re-enable type checking and apply the final state.
this.endTypesBatch()

} catch (error) {
console.error('[DIAGNOSE-ONCHANGE] Critical error during type loading process:', error)
this.endTypesBatch()
}
}, 1500)
}

const currentFile = await this.call('fileManager', 'file')
if (!currentFile || currentFile !== file) return

const input = this.get(currentFile)
if (!input || input === this.previousInput) return

this.previousInput = input

// fire storage update
// NOTE: save at most once per 5 seconds
if (this.saveTimeout) {
window.clearTimeout(this.saveTimeout)
}
Expand Down Expand Up @@ -232,7 +444,7 @@ export default class Editor extends Plugin {
this.emit('addModel', contentDep, 'typescript', pathDep, this.readOnlySessions[path])
}
} else {
console.log("The file ", pathDep, " can't be found.")
// console.log("The file ", pathDep, " can't be found.")
}
} catch (e) {
console.log(e)
Expand Down
Loading