package dev.hgh.jai.settings import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.components.StoragePathMacros import com.intellij.openapi.project.Project import com.intellij.openapi.project.RootsChangeRescanningInfo import com.intellij.openapi.roots.ex.ProjectRootManagerEx import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import java.io.File /** Project-level paths used to find Jai modules and external Jai source files. */ @State( name = "JaiProjectSettings", storages = [Storage(StoragePathMacros.WORKSPACE_FILE)], ) class JaiProjectSettings( private val project: Project, ) : PersistentStateComponent { /** The persisted form deliberately contains paths, not VirtualFiles. */ class State { @JvmField var rootPaths: MutableList = mutableListOf() override fun equals(other: Any?): Boolean = other is State && rootPaths == other.rootPaths override fun hashCode(): Int = rootPaths.hashCode() } private var state = State() /** Configured roots in precedence order. Invalid paths remain visible in Settings. */ fun rootPaths(): List = state.rootPaths.toList() /** Existing configured directories, in the same order as their persisted paths. */ fun configuredRoots(): List = rootPaths() .mapNotNull { LocalFileSystem.getInstance().findFileByPath(it) } .filter { it.isValid && it.isDirectory } fun setRootPaths(paths: Collection) { val normalized = paths.mapNotNull(::normalizePath).distinct() if (normalized == state.rootPaths) return state = State().also { it.rootPaths.addAll(normalized) } } override fun getState(): State = State().also { it.rootPaths.addAll(state.rootPaths) } override fun loadState(loadedState: State) { val normalized = loadedState.rootPaths.mapNotNull(::normalizePath).distinct() if (normalized == state.rootPaths) return state = State().also { it.rootPaths.addAll(normalized) } } private fun normalizePath(rawPath: String): String? { val trimmed = rawPath.trim() if (trimmed.isEmpty()) return null val expanded = when { trimmed == "~" -> { System.getProperty("user.home") } trimmed.startsWith("~/") -> { System.getProperty("user.home") + trimmed.removePrefix("~") } else -> { trimmed } } val file = File(expanded) val absolute = if (file.isAbsolute) { file } else { project.basePath?.let { File(it, expanded) } ?: file.absoluteFile } return FileUtil.toSystemIndependentName( absolute .toPath() .normalize() .toAbsolutePath() .toString(), ) } /** Rebuilds the project/index roots after Settings has been applied. */ fun refreshProjectRoots() { if (!project.isInitialized || project.isDisposed) return val refresh = Runnable { if (!project.isDisposed) { ProjectRootManagerEx .getInstanceEx(project) .makeRootsChange(Runnable {}, RootsChangeRescanningInfo.TOTAL_RESCAN) } } val application = ApplicationManager.getApplication() if (application.isWriteAccessAllowed) { refresh.run() } else { application.runWriteAction(refresh) } } companion object { @JvmStatic fun getInstance(project: Project): JaiProjectSettings = project.getService(JaiProjectSettings::class.java) } }