Skip to content

Commit acf37b4

Browse files
committed
feat: resolve and parse elide project manifest
Signed-off-by: Dario Valdespino <[email protected]>
1 parent 09b7a7b commit acf37b4

File tree

8 files changed

+213
-19
lines changed

8 files changed

+213
-19
lines changed

packages/plugin-idea/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@
1414
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
1515

1616
plugins {
17-
id("java")
1817
alias(libs.plugins.elide.conventions)
1918
alias(libs.plugins.intellij.platform)
2019
kotlin("jvm")
20+
id("java")
2121
}
2222

2323
elide {
2424
jvm {
2525
// Intellij plugins can only target up to JVM 21
2626
target = JvmTarget.JVM_21
2727
}
28+
kotlin {
29+
customKotlinCompilerArgs += "-Xskip-prerelease-check"
30+
}
2831
}
2932

3033
repositories {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,25 @@
1313

1414
package dev.elide.intellij
1515

16+
import com.intellij.DynamicBundle
1617
import com.intellij.openapi.diagnostic.Logger
1718
import com.intellij.openapi.externalSystem.model.ProjectSystemId
1819
import com.intellij.openapi.fileChooser.FileChooserDescriptor
1920
import com.intellij.openapi.util.IconLoader
2021
import com.intellij.ui.IconManager
2122
import com.intellij.ui.PlatformIcons
23+
import dev.elide.intellij.Constants.Strings.get
24+
import org.jetbrains.annotations.PropertyKey
2225
import javax.swing.Icon
2326

2427
/** Useful constants used by the Elide plugin. */
2528
object Constants {
2629
/** External System ID for Elide. */
2730
val SYSTEM_ID = ProjectSystemId("ELIDE")
2831

32+
/** Elide plugin ID. */
33+
const val PLUGIN_ID = "dev.elide.intellij"
34+
2935
/** ID used to reference settings panels. */
3036
const val CONFIGURABLE_ID = "reference.settingsdialog.project.elide"
3137

@@ -69,4 +75,14 @@ object Constants {
6975
}
7076
}
7177
}
78+
79+
/**
80+
* Localized strings provided by a resource bundle, use the indexing operator or the static [get] function to obtain
81+
* formatted messages.
82+
*/
83+
data object Strings : DynamicBundle("i18n.Strings") {
84+
@JvmStatic operator fun get(@PropertyKey(resourceBundle = "i18n.Strings") key: String, vararg params: Any): String {
85+
return getMessage(key, params = params)
86+
}
87+
}
7288
}

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
package dev.elide.intellij
1515

1616
import com.intellij.execution.configurations.SimpleJavaParameters
17+
import com.intellij.ide.plugins.PluginManagerCore
1718
import com.intellij.openapi.diagnostic.Logger
19+
import com.intellij.openapi.extensions.PluginId
1820
import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware
1921
import com.intellij.openapi.externalSystem.ExternalSystemManager
2022
import com.intellij.openapi.externalSystem.model.ProjectSystemId
@@ -28,6 +30,7 @@ import dev.elide.intellij.project.ElideProjectResolver
2830
import dev.elide.intellij.settings.*
2931
import dev.elide.intellij.tasks.ElideTaskManager
3032
import java.io.File
33+
import kotlin.io.path.*
3134

3235
/**
3336
* Coordinator service for Elide as an external build system.
@@ -83,7 +86,27 @@ class ElideManager : ExternalSystemAutoImportAware, ExternalSystemManager<
8386
override fun getExternalProjectDescriptor(): FileChooserDescriptor = Constants.projectFileChooser()
8487

8588
override fun enhanceRemoteProcessing(params: SimpleJavaParameters) {
86-
// noop
89+
// the project resolver runs in a separate process, but it needs access to many plugin dependencies;
90+
// to avoid class loading issues, we enhance its classpath with all JARs in the plugin's 'lib' path
91+
try {
92+
val plugin = PluginManagerCore.getPlugin(PluginId.getId(Constants.PLUGIN_ID))
93+
if (plugin == null) {
94+
LOG.warn("Cannot find plugin ${Constants.PLUGIN_ID}")
95+
return
96+
}
97+
98+
val pluginPath = plugin.pluginPath
99+
LOG.info("Enhancing remote process classpath from: $pluginPath")
100+
101+
pluginPath.resolve("lib").takeIf { it.exists() && it.isDirectory() }?.useDirectoryEntries { entries ->
102+
entries.filter { it.isRegularFile() && it.extension == "jar" }.forEach { entry ->
103+
params.classPath.add(entry.toString())
104+
LOG.debug("Added remote classpath entry: $entry")
105+
}
106+
}
107+
} catch (e: Exception) {
108+
LOG.error("Failed to enhance remote processing, project resolver will not work", e)
109+
}
87110
}
88111

89112
override fun getAffectedExternalProjectPath(changedFileOrDirPath: String, project: Project): String? {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.manifests
15+
16+
import java.io.InputStream
17+
import java.io.OutputStream
18+
import java.nio.file.Path
19+
import elide.tooling.project.PackageManifestService
20+
import elide.tooling.project.ProjectEcosystem
21+
import elide.tooling.project.codecs.ElidePackageManifestCodec
22+
import elide.tooling.project.manifest.ElidePackageManifest
23+
import elide.tooling.project.manifest.PackageManifest
24+
25+
/** Basic manifest service that only resolves Elide manifests. */
26+
class ElideManifestService : PackageManifestService {
27+
private val elideCodec = ElidePackageManifestCodec()
28+
29+
private fun ProjectEcosystem.requireElide() {
30+
require(this == ProjectEcosystem.Elide) { "Only Elide package manifests are supported for resolution" }
31+
}
32+
33+
override fun resolve(root: Path, ecosystem: ProjectEcosystem): Path {
34+
ecosystem.requireElide()
35+
return root.resolve(elideCodec.defaultPath())
36+
}
37+
38+
override fun parse(source: Path): ElidePackageManifest {
39+
return elideCodec.parseAsFile(source)
40+
}
41+
42+
override fun parse(source: InputStream, ecosystem: ProjectEcosystem): ElidePackageManifest {
43+
ecosystem.requireElide()
44+
return elideCodec.parse(source)
45+
}
46+
47+
override fun merge(manifests: Iterable<PackageManifest>): ElidePackageManifest {
48+
throw UnsupportedOperationException()
49+
}
50+
51+
override fun export(manifest: ElidePackageManifest, ecosystem: ProjectEcosystem): PackageManifest {
52+
throw UnsupportedOperationException()
53+
}
54+
55+
override fun encode(manifest: PackageManifest, output: OutputStream) {
56+
require(manifest is ElidePackageManifest) { "Only Elide package manifests are supported" }
57+
elideCodec.write(manifest, output)
58+
}
59+
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,27 @@ import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectAware
1818
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId
1919
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectListener
2020
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectReloadContext
21+
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode
22+
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil
2123
import com.intellij.openapi.project.Project
2224
import dev.elide.intellij.Constants
2325

2426
/** A tracker for a project in the current IDE instance, used to trigger auto-import and sync operations. */
25-
class ElideProjectAware(private val project: Project, projectPath: String) : ExternalSystemProjectAware {
27+
class ElideProjectAware(private val project: Project, private val projectPath: String) : ExternalSystemProjectAware {
2628
override val projectId: ExternalSystemProjectId = ExternalSystemProjectId(Constants.SYSTEM_ID, projectPath)
2729
override val settingsFiles: Set<String> = setOf("$projectPath/${Constants.MANIFEST_NAME}")
2830

2931
override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) {
30-
// notify the listener once project sync is complete
32+
// TODO: notify the listener about project sync status
3133
}
3234

3335
override fun reloadProject(context: ExternalSystemProjectReloadContext) {
34-
// noop
36+
ExternalSystemUtil.refreshProject(
37+
/* project = */ project,
38+
/* externalSystemId = */ Constants.SYSTEM_ID,
39+
/* externalProjectPath = */ projectPath,
40+
/* isPreviewMode = */ false,
41+
/* progressExecutionMode = */ ProgressExecutionMode.IN_BACKGROUND_ASYNC,
42+
)
3543
}
3644
}

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

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,22 @@ import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceTyp
2121
import com.intellij.openapi.externalSystem.model.project.ModuleData
2222
import com.intellij.openapi.externalSystem.model.project.ProjectData
2323
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
24+
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationEvent
2425
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener
2526
import com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver
2627
import dev.elide.intellij.Constants
28+
import dev.elide.intellij.manifests.ElideManifestService
2729
import dev.elide.intellij.settings.ElideExecutionSettings
30+
import java.nio.file.Path
31+
import kotlinx.coroutines.runBlocking
32+
import kotlin.io.path.Path
33+
import kotlin.io.path.notExists
34+
import elide.tooling.lockfile.LockfileLoader
35+
import elide.tooling.lockfile.loadLockfileSafe
36+
import elide.tooling.project.ElideConfiguredProject
37+
import elide.tooling.project.ElideProjectInfo
38+
import elide.tooling.project.ElideProjectLoader
39+
import elide.tooling.project.SourceSetFactory
2840

2941
/**
3042
* A service capable of using the Elide manifest and lockfile to build a project model that can be understood by the
@@ -35,33 +47,81 @@ import dev.elide.intellij.settings.ElideExecutionSettings
3547
* tracker.
3648
*/
3749
class ElideProjectResolver : ExternalSystemProjectResolver<ElideExecutionSettings> {
38-
override fun cancelTask(id: ExternalSystemTaskId, listener: ExternalSystemTaskNotificationListener): Boolean = true
50+
/** Shortcut to emit a status change event with the given description [text]. */
51+
private fun ExternalSystemTaskNotificationListener.onStep(taskId: ExternalSystemTaskId, text: String) {
52+
val event = ExternalSystemTaskNotificationEvent(taskId, text)
53+
onStatusChange(event)
54+
}
55+
56+
override fun cancelTask(id: ExternalSystemTaskId, listener: ExternalSystemTaskNotificationListener): Boolean {
57+
return true
58+
}
3959

4060
override fun resolveProjectInfo(
4161
id: ExternalSystemTaskId,
4262
projectPath: String,
4363
isPreviewMode: Boolean,
4464
settings: ElideExecutionSettings?,
4565
listener: ExternalSystemTaskNotificationListener
46-
): DataNode<ProjectData?>? {
66+
): DataNode<ProjectData>? {
4767
LOG.debug("Resolving project at '$projectPath'")
68+
listener.onStart(projectPath, id)
69+
val projectModel = runCatching {
70+
// find a manifest in the project directory
71+
listener.onStep(id, Constants.Strings["resolve.steps.discovery"])
72+
val projectRoot = Path(projectPath)
73+
val manifestPath = projectRoot.resolve(Constants.MANIFEST_NAME)
74+
75+
if (manifestPath.notExists()) {
76+
LOG.debug("No Elide manifest found under $projectPath")
77+
return@runCatching null
78+
}
79+
80+
// parse the manifest
81+
listener.onStep(id, Constants.Strings["resolve.steps.parse"])
82+
val manifest = ElideManifestService().parse(manifestPath)
4883

84+
// call the CLI in case the lockfile is outdated
85+
// listener.onStep(id, Constants.Strings["resolve.steps.sync"])
86+
// TODO: launch sync task in the background and wait for it here
87+
88+
// configure the project
89+
listener.onStep(id, Constants.Strings["resolve.steps.configure"])
90+
val loader = buildProjectLoader(projectRoot, settings)
91+
val project = runBlocking { ElideProjectInfo(projectRoot, manifest, null).load(loader) }
92+
93+
// build the project model from the manifest and lockfile
94+
listener.onStep(id, Constants.Strings["resolve.steps.buildModel"])
95+
buildProjectModel(projectPath, project)
96+
}.onSuccess {
97+
listener.onSuccess(projectPath, id)
98+
}.onFailure { cause ->
99+
listener.onTaskOutput(id, "Failed to sync project: $cause", false)
100+
listener.onFailure(projectPath, id, RuntimeException(cause))
101+
}
102+
103+
listener.onEnd(projectPath, id)
104+
return projectModel.getOrThrow()
105+
}
106+
107+
/** Build the project model given a configured Elide [project]. */
108+
private fun buildProjectModel(projectPath: String, project: ElideConfiguredProject): DataNode<ProjectData> {
49109
// stubbed project model
50110
val projectData = ProjectData(
51-
Constants.SYSTEM_ID,
52-
"Elide Project",
53-
projectPath,
54-
projectPath,
111+
/* owner = */ Constants.SYSTEM_ID,
112+
/* externalName = */ project.manifest.name ?: Constants.Strings["project.defaults.name"],
113+
/* ideProjectFileDirectoryPath = */ projectPath,
114+
/* linkedExternalProjectPath = */ projectPath,
55115
)
56116

57-
// stubbed sample module
117+
// main project module
58118
val module = ModuleData(
59-
"sample",
60-
Constants.SYSTEM_ID,
61-
"JAVA_MODULE",
62-
"Sample",
63-
"$projectPath/src/main",
64-
"$projectPath/src/main",
119+
/* id = */ "main",
120+
/* owner = */ Constants.SYSTEM_ID,
121+
/* moduleTypeId = */ "JAVA_MODULE",
122+
/* externalName = */ "main",
123+
/* moduleFileDirectoryPath = */ "$projectPath/src/main",
124+
/* externalConfigPath = */ "$projectPath/src/main",
65125
)
66126

67127
val projectNode = DataNode(ProjectKeys.PROJECT, projectData, null)
@@ -79,6 +139,20 @@ class ElideProjectResolver : ExternalSystemProjectResolver<ElideExecutionSetting
79139
return projectNode
80140
}
81141

142+
/**
143+
* Construct a new [ElideProjectLoader] that uses the resources from the Elide distribution set in the execution
144+
* [settings].
145+
*/
146+
private fun buildProjectLoader(projectPath: Path, settings: ElideExecutionSettings?): ElideProjectLoader {
147+
return object : ElideProjectLoader {
148+
override val lockfileLoader: LockfileLoader = LockfileLoader { loadLockfileSafe(projectPath) }
149+
override val sourceSetFactory: SourceSetFactory = SourceSetFactory.Default
150+
151+
// TODO(@darvld): resolve from execution settings
152+
override val resourcesPath: Path get() = projectPath.resolve(".elide")
153+
}
154+
}
155+
82156
private companion object {
83157
@JvmStatic private val LOG = Logger.getInstance(ElideProjectResolver::class.java)
84158
}

packages/plugin-idea/src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<extensions defaultExtensionNs="com.intellij">
2727
<externalIconProvider key="ELIDE" implementationClass="dev.elide.intellij.ui.ElideIconProvider"/>
2828
<postStartupActivity implementation="dev.elide.intellij.startup.ElideStartupSyncActivity"/>
29-
<externalSystemManager implementation="dev.elide.intellij.ElideManager"/>
29+
<externalSystemManager id="ELIDE" implementation="dev.elide.intellij.ElideManager"/>
3030
<projectConfigurable
3131
key="elide"
3232
id="reference.settingsdialog.project.elide"

packages/plugin-idea/src/main/resources/i18n/Strings.properties

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,14 @@
1212
#
1313

1414
elide=Elide
15+
16+
# project resolution messages
17+
resolve.steps.discovery=Searching for an Elide project manifest
18+
resolve.steps.parse=Parsing Elide project manifest
19+
resolve.steps.configure=Configuring Elide project
20+
resolve.steps.buildModel=Building Elide project model
21+
resolve.steps.install=Installing project dependencies
22+
resolve.steps.sync=Syncing Elide project
23+
24+
# project model defaults
25+
project.defaults.name=Elide Project

0 commit comments

Comments
 (0)