Skip to content

Commit df5d4cd

Browse files
committed
feat(idea): project and plugin settings
Signed-off-by: Dario Valdespino <dvaldespino00@gmail.com>
1 parent f2b8059 commit df5d4cd

16 files changed

+513
-49
lines changed

packages/plugin-idea/src/main/kotlin/dev/elide/intellij/Constants.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ object Constants {
4444
/** Name and extension of the Elide lockfile. */
4545
const val LOCKFILE_NAME = "elide.lock.bin"
4646

47+
/** Default installation directory for Elide under the user home path. */
48+
const val ELIDE_HOME = "elide"
49+
50+
/** Resources path relative to the root of the Elide distribution. */
51+
const val ELIDE_RESOURCES_DIR = "resources"
52+
53+
/** Relative path to the CLI binary in an Elide distribution. */
54+
const val ELIDE_BINARY = "elide"
55+
4756
/** Descriptor for a file chooser to be used when selecting an Elide project. */
4857
@JvmStatic fun projectFileChooser(): FileChooserDescriptor {
4958
return FileChooserDescriptor(
@@ -56,6 +65,18 @@ object Constants {
5665
).withFileFilter { it.name == MANIFEST_NAME }
5766
}
5867

68+
/** Descriptor for a file chooser to be used when selecting an Elide distribution. */
69+
@JvmStatic fun sdkFileChooser(): FileChooserDescriptor {
70+
return FileChooserDescriptor(
71+
/* chooseFiles = */ false,
72+
/* chooseFolders = */ true,
73+
/* chooseJars = */ false,
74+
/* chooseJarsAsFiles = */ false,
75+
/* chooseJarContents = */ false,
76+
/* chooseMultiple = */ false,
77+
)
78+
}
79+
5980
data object Icons {
6081
@JvmStatic private val LOG = Logger.getInstance(Icons::class.java)
6182

packages/plugin-idea/src/main/kotlin/dev/elide/intellij/ElideManager.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@ package dev.elide.intellij
1616
import com.intellij.execution.configurations.SimpleJavaParameters
1717
import com.intellij.openapi.diagnostic.Logger
1818
import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware
19+
import com.intellij.openapi.externalSystem.ExternalSystemConfigurableAware
1920
import com.intellij.openapi.externalSystem.ExternalSystemManager
2021
import com.intellij.openapi.externalSystem.model.ProjectSystemId
2122
import com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver
2223
import com.intellij.openapi.externalSystem.service.project.autoimport.CachingExternalSystemAutoImportAware
2324
import com.intellij.openapi.externalSystem.task.ExternalSystemTaskManager
2425
import com.intellij.openapi.fileChooser.FileChooserDescriptor
26+
import com.intellij.openapi.options.Configurable
2527
import com.intellij.openapi.project.Project
2628
import com.intellij.openapi.util.Pair
2729
import com.intellij.util.Function
2830
import dev.elide.intellij.project.ElideProjectResolver
31+
import dev.elide.intellij.service.ElideDistributionResolver
2932
import dev.elide.intellij.settings.*
3033
import dev.elide.intellij.tasks.ElideTaskManager
3134
import java.io.File
@@ -47,7 +50,7 @@ import java.io.File
4750
* actions, etc. Some additional parts, such as the [dev.elide.intellij.startup.ElideStartupActivity], are used
4851
* to complement those features and improve the experience (e.g. by scanning for a project on startup).
4952
*/
50-
class ElideManager : ExternalSystemAutoImportAware, ExternalSystemManager<
53+
class ElideManager : ExternalSystemAutoImportAware, ExternalSystemConfigurableAware, ExternalSystemManager<
5154
ElideProjectSettings,
5255
ElideSettingsListener,
5356
ElideSettings,
@@ -60,7 +63,7 @@ class ElideManager : ExternalSystemAutoImportAware, ExternalSystemManager<
6063
override fun getSystemId(): ProjectSystemId = Constants.SYSTEM_ID
6164

6265
override fun getSettingsProvider(): Function<Project, ElideSettings> = Function { project ->
63-
project.getService(ElideSettings::class.java)
66+
ElideSettings.getSettings(project)
6467
}
6568

6669
override fun getLocalSettingsProvider(): Function<Project, ElideLocalSettings> = Function { project ->
@@ -71,9 +74,14 @@ class ElideManager : ExternalSystemAutoImportAware, ExternalSystemManager<
7174
val project = it.first
7275
val path = it.second
7376

77+
val settings = ElideSettings.getSettings(project)
78+
7479
LOG.debug("Preparing execution settings for project at '$path': $project")
75-
val settings = ElideExecutionSettings()
76-
settings
80+
ElideExecutionSettings(
81+
elideHome = ElideDistributionResolver.getElideHome(project, path),
82+
downloadSources = settings.downloadSources,
83+
downloadDocs = settings.downloadDocs,
84+
)
7785
}
7886

7987
override fun getProjectResolverClass(): Class<out ExternalSystemProjectResolver<ElideExecutionSettings>> {
@@ -101,6 +109,10 @@ class ElideManager : ExternalSystemAutoImportAware, ExternalSystemManager<
101109
return autoImportDelegate.getAffectedExternalProjectFiles(projectPath, project)
102110
}
103111

112+
override fun getConfigurable(project: Project): Configurable {
113+
return ElideConfigurable(project)
114+
}
115+
104116
private companion object {
105117
@JvmStatic private val LOG = Logger.getInstance(ElideManager::class.java)
106118
}

packages/plugin-idea/src/main/kotlin/dev/elide/intellij/project/ElideProjectResolver.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.intellij.openapi.progress.runBlockingCancellable
2525
import dev.elide.intellij.Constants
2626
import dev.elide.intellij.manifests.ElideManifestService
2727
import dev.elide.intellij.project.model.ElideProjectModel
28+
import dev.elide.intellij.service.ElideDistributionResolver
2829
import dev.elide.intellij.settings.ElideExecutionSettings
2930
import org.jetbrains.annotations.PropertyKey
3031
import java.nio.file.Path
@@ -107,13 +108,17 @@ class ElideProjectResolver : ExternalSystemProjectResolver<ElideExecutionSetting
107108
* Construct a new [ElideProjectLoader] that uses the resources from the Elide distribution set in the execution
108109
* [settings].
109110
*/
110-
private fun buildProjectLoader(projectPath: Path, settings: ElideExecutionSettings?): ElideProjectLoader {
111+
private fun buildProjectLoader(
112+
projectPath: Path,
113+
settings: ElideExecutionSettings?
114+
): ElideProjectLoader {
111115
return object : ElideProjectLoader {
112116
override val lockfileLoader: LockfileLoader = LockfileLoader { loadLockfileSafe(projectPath) }
113117
override val sourceSetFactory: SourceSetFactory = SourceSetFactory.Default
114-
115-
// TODO(@darvld): resolve from execution settings
116-
override val resourcesPath: Path get() = projectPath.resolve(".elide")
118+
override val resourcesPath: Path by lazy {
119+
val elideHome = settings?.elideHome ?: ElideDistributionResolver.defaultDistributionPath()
120+
ElideDistributionResolver.resourcesPath(elideHome)
121+
}
117122
}
118123
}
119124

packages/plugin-idea/src/main/kotlin/dev/elide/intellij/project/ElideUnlinkedProjectAware.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,25 @@ import com.intellij.openapi.vfs.VirtualFile
2323
import dev.elide.intellij.Constants
2424
import dev.elide.intellij.settings.ElideProjectSettings
2525
import dev.elide.intellij.settings.ElideSettings
26-
import java.util.concurrent.ConcurrentSkipListSet
2726

2827
/**
2928
* Tracking service used to link Elide projects when they are opened by the IDE. This class enables the auto-import
3029
* and project sync features automatically, by configuring which files should be tracked by Intellij.
3130
*/
3231
@Suppress("UnstableApiUsage") class ElideUnlinkedProjectAware : ExternalSystemUnlinkedProjectAware {
33-
private val linkedProjects = ConcurrentSkipListSet<String>()
3432
override val systemId: ProjectSystemId = Constants.SYSTEM_ID
3533

3634
override fun isBuildFile(project: Project, buildFile: VirtualFile): Boolean {
3735
return buildFile.name == Constants.MANIFEST_NAME
3836
}
3937

4038
override fun isLinkedProject(project: Project, externalProjectPath: String): Boolean {
41-
val settings = project.getService(ElideSettings::class.java)
39+
val settings = ElideSettings.getSettings(project)
4240
return settings.getLinkedProjectSettings(externalProjectPath) != null
4341
}
4442

4543
override fun subscribe(project: Project, listener: ExternalSystemProjectLinkListener, parentDisposable: Disposable) {
46-
project.getService(ElideSettings::class.java).subscribe(
44+
ElideSettings.getSettings(project).subscribe(
4745
object : ExternalSystemSettingsListener<ElideProjectSettings?> {
4846
override fun onProjectsLinked(settings: Collection<ElideProjectSettings?>) {
4947
settings.forEach { if (it != null) listener.onProjectLinked(it.externalProjectPath) }
@@ -58,13 +56,11 @@ import java.util.concurrent.ConcurrentSkipListSet
5856
}
5957

6058
override suspend fun linkAndLoadProjectAsync(project: Project, externalProjectPath: String) {
61-
linkedProjects.add(externalProjectPath)
6259
ElideOpenProjectProvider().linkToExistingProjectAsync(externalProjectPath, project)
6360

6461
}
6562

6663
override suspend fun unlinkProject(project: Project, externalProjectPath: String) {
6764
ElideOpenProjectProvider().unlinkProject(project, externalProjectPath)
68-
linkedProjects.remove(externalProjectPath)
6965
}
7066
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package dev.elide.intellij.service
15+
16+
import com.intellij.openapi.components.Service
17+
import com.intellij.openapi.project.Project
18+
import com.intellij.openapi.util.io.toCanonicalPath
19+
import com.intellij.util.io.awaitExit
20+
import dev.elide.intellij.Constants
21+
import java.io.File
22+
import java.nio.file.Path
23+
import kotlinx.coroutines.*
24+
import kotlinx.coroutines.flow.*
25+
26+
/**
27+
* Project-level service responsible for interfacing with the Elide CLI binary. Methods in this class expose relevant
28+
* CLI commands using coroutines and provide a wrapper for the result with [ElideProcess].
29+
*/
30+
@Service(Service.Level.PROJECT)
31+
class ElideCliService(private val project: Project, private val serviceScope: CoroutineScope) {
32+
/**
33+
* Encapsulates a call to the Elide CLI running as a subprocess launched by [ProcessBuilder]. Awaiting this value
34+
* will return the exit code of the process once it finishes.
35+
*
36+
* Use [readStdOut] and [readStdErr] to consume the entirety of the process's output and error streams, or manually
37+
* collect the [stdOut] and [stdErr] flows for better control.
38+
*/
39+
class ElideProcess private constructor(
40+
private val process: Process,
41+
processScope: CoroutineScope,
42+
exitCode: Deferred<Int>
43+
) : Deferred<Int> by exitCode {
44+
/**
45+
* Shared flow connected to the process's standard output stream; a buffered reader is used to emit each line from
46+
* a background thread while subscribers are active.
47+
*/
48+
val stdOut: Flow<String> = flow {
49+
process.inputStream.bufferedReader().useLines { lines ->
50+
lines.forEach { emit(it) }
51+
}
52+
}.flowOn(Dispatchers.IO).shareIn(processScope, SharingStarted.WhileSubscribed())
53+
54+
/**
55+
* Shared flow connected to the process's standard error stream; a buffered reader is used to emit each line from
56+
* a background thread while subscribers are active.
57+
*/
58+
val stdErr: Flow<String> = flow {
59+
process.errorStream.bufferedReader().useLines { lines ->
60+
lines.forEach { emit(it) }
61+
}
62+
}.flowOn(Dispatchers.IO).shareIn(processScope, SharingStarted.WhileSubscribed())
63+
64+
/** Read the process's standard output stream in its entirety, suspending until it is closed. */
65+
suspend fun readStdOut() = withContext(Dispatchers.IO) {
66+
process.inputStream.bufferedReader().readText()
67+
}
68+
69+
/** Read the process's standard error stream in its entirety, suspending until it is closed. */
70+
suspend fun readStdErr() = withContext(Dispatchers.IO) {
71+
process.errorStream.bufferedReader().readText()
72+
}
73+
74+
companion object {
75+
/**
76+
* Launch the Elide CLI binary using [ProcessBuilder] and wrap it as an [ElideProcess]. This function is meant
77+
* to be used by the [ElideCliService], which automatically handles path resolution and provides an execution
78+
* context.
79+
*/
80+
fun launch(
81+
elideBinary: Path,
82+
projectPath: String,
83+
scope: CoroutineScope,
84+
buildCommand: MutableList<String>.() -> Unit
85+
): ElideProcess {
86+
val command = buildList {
87+
add(elideBinary.toCanonicalPath())
88+
buildCommand()
89+
}
90+
91+
val process = ProcessBuilder(command)
92+
.directory(File(projectPath))
93+
.start()
94+
95+
return ElideProcess(process, scope, scope.async { process.awaitExit() })
96+
}
97+
}
98+
}
99+
100+
/**
101+
* Launch the Elide CLI binary as a subprocess and wrap it in [ElideProcess] for easy handling. The settings of the
102+
* linked project at [projectPath] are used to select an Elide distribution to be invoked.
103+
*/
104+
private fun launchElide(projectPath: String, buildCommand: MutableList<String>.() -> Unit): ElideProcess {
105+
val elideHome = ElideDistributionResolver.getElideHome(project, projectPath)
106+
val elideBin = elideHome.resolve(Constants.ELIDE_BINARY)
107+
108+
return ElideProcess.launch(elideBin, projectPath, serviceScope, buildCommand)
109+
}
110+
111+
/** Invoke the Elide CLI with the `--version` option in a background context and return its output. */
112+
fun version(projectPath: String): Deferred<String> = serviceScope.async {
113+
val elide = launchElide(projectPath, buildCommand = { add("--version") })
114+
elide.readStdOut()
115+
}
116+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package dev.elide.intellij.service
15+
16+
import com.intellij.openapi.components.Service
17+
import com.intellij.openapi.project.Project
18+
import dev.elide.intellij.Constants
19+
import dev.elide.intellij.service.ElideDistributionResolver.Companion.defaultDistributionPath
20+
import dev.elide.intellij.service.ElideDistributionResolver.Companion.getElideHome
21+
import dev.elide.intellij.service.ElideDistributionResolver.Companion.resourcesPath
22+
import dev.elide.intellij.service.ElideDistributionResolver.Companion.validateDistributionPath
23+
import dev.elide.intellij.settings.ElideDistributionSetting
24+
import dev.elide.intellij.settings.ElideSettings
25+
import java.nio.file.Path
26+
import kotlin.io.path.Path
27+
import kotlin.io.path.isDirectory
28+
import kotlin.io.path.isRegularFile
29+
30+
/**
31+
* Service used to resolve an Elide distribution for a project; use the static [getElideHome] function to obtain a
32+
* distribution path that respects project configuration, with a fallback to [defaultDistributionPath].
33+
*
34+
* For simple validation cases, [validateDistributionPath] can be used to verify that a minimal distribution structure
35+
* is present in the selected Elide home directory.
36+
*/
37+
@Service(Service.Level.PROJECT)
38+
class ElideDistributionResolver(private val project: Project) {
39+
/**
40+
* Resolve the path to the preferred Elide distribution for the linked project at [externalProjectPath]. If no linked
41+
* settings are found, [defaultDistributionPath] is returned instead.
42+
*
43+
* The returned path is *not* validated by [validateDistributionPath] or in any other way; it is the responsibility
44+
* of the caller to properly check that the path correspond to a valid Elide distribution before using it as such.
45+
*/
46+
fun resolveDistributionPath(externalProjectPath: String): Path {
47+
val settings = ElideSettings.getSettings(project)
48+
.getLinkedProjectSettings(externalProjectPath)
49+
?: return defaultDistributionPath()
50+
51+
return when (settings.elideDistributionType) {
52+
ElideDistributionSetting.Custom -> Path(settings.elideDistributionPath).normalize()
53+
ElideDistributionSetting.AutoDetect -> defaultDistributionPath()
54+
}
55+
}
56+
57+
companion object {
58+
/**
59+
* Returns the path to the preferred Elide distribution for a linked external project, or the
60+
* [defaultDistributionPath] if no project settings are found.
61+
*
62+
* The returned path is *not* validated by [validateDistributionPath] or in any other way; it is the responsibility
63+
* of the caller to properly check that the path correspond to a valid Elide distribution before using it as such.
64+
*/
65+
@JvmStatic fun getElideHome(project: Project, externalProjectPath: String): Path {
66+
return project.getService(ElideDistributionResolver::class.java).resolveDistributionPath(externalProjectPath)
67+
}
68+
69+
/**
70+
* Returns the default path to the Elide installation in the user's home directory. Note that the path is not
71+
* guaranteed to contain a valid distribution or even exist.
72+
*/
73+
@JvmStatic fun defaultDistributionPath(): Path {
74+
return Path(System.getProperty("user.home")).resolve(Constants.ELIDE_HOME)
75+
}
76+
77+
/**
78+
* Shorthand for resolving the [resourcesPath] in the [preferred Elide distribution][getElideHome] for a linked
79+
* external project.
80+
*/
81+
@JvmStatic fun resourcesPath(project: Project, externalProjectPath: String): Path {
82+
val elideHome = getElideHome(project, externalProjectPath)
83+
return elideHome.resolve(Constants.ELIDE_RESOURCES_DIR)
84+
}
85+
86+
/** Returns the path to the resources directory inside the given Elide distribution. */
87+
@JvmStatic fun resourcesPath(elideHome: Path): Path {
88+
return elideHome.resolve(Constants.ELIDE_RESOURCES_DIR)
89+
}
90+
91+
/** Lightly validates an Elide distribution [path], verifying some basic directories and files are presents. */
92+
@JvmStatic fun validateDistributionPath(path: Path): Boolean {
93+
if (!path.resolve(Constants.ELIDE_RESOURCES_DIR).isDirectory()) return false
94+
if (!path.resolve(Constants.ELIDE_BINARY).isRegularFile()) return false
95+
96+
return true
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)