Files
intellijai/src/main/kotlin/dev/hgh/jai/settings/JaiProjectSettings.kt
2026-08-05 16:32:40 -04:00

135 lines
4.9 KiB
Kotlin

package dev.hgh.jai.settings
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PathMacroManager
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<JaiProjectSettings.State> {
/** The persisted form deliberately contains paths, not VirtualFiles. */
class State {
@JvmField
var rootPaths: MutableList<String> = 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<String> = state.rootPaths.toList()
/** Existing configured directories, in the same order as their persisted paths. */
fun configuredRoots(): List<VirtualFile> =
rootPaths()
.mapNotNull { LocalFileSystem.getInstance().findFileByPath(it) }
.filter { it.isValid && it.isDirectory }
fun setRootPaths(paths: Collection<String>) {
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
var expanded =
when {
trimmed == "~" -> {
System.getProperty("user.home")
}
trimmed.startsWith("~/") -> {
System.getProperty("user.home") + trimmed.removePrefix("~")
}
else -> {
trimmed
}
}
// Workspace files may persist project-relative roots using IntelliJ path macros. The
// settings UI normally supplies absolute paths, but loading a project saved elsewhere
// must expand these before the resolver tries to find the VFS root.
expanded = PathMacroManager.getInstance(project).expandPath(expanded)
expanded =
expanded
.replace("\$USER_HOME\$", System.getProperty("user.home"))
.replace("\$USER_HOME", System.getProperty("user.home"))
.let { value ->
val basePath = project.basePath ?: return@let value
value
.replace("\$PROJECT_DIR\$", basePath)
.replace("\$PROJECT_DIR", basePath)
}
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)
}
}