Phase 5a: add configurable Jai module roots

This commit is contained in:
hgranthorner
2026-08-05 10:24:32 -04:00
parent 9122b5a5c0
commit 2bc1b9f4be
13 changed files with 740 additions and 214 deletions

View File

@@ -0,0 +1,119 @@
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<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
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)
}
}