Skip to content

Commit 2a18825

Browse files
committed
Fix mix dependency handling: use mix deps for status checks.
Debounce notifications so users aren't spammed Add "Mix deps install" action that runs using run configuration so users can see what it is doing.
1 parent 15cbe84 commit 2a18825

20 files changed

+784
-105
lines changed

resources/META-INF/plugin.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<backgroundPostStartupActivity implementation="org.elixir_lang.status_bar_widget.ElixirSdkStatusWidgetStartupActivity"/>
3333

3434
<postStartupActivity implementation="org.elixir_lang.sdk.SdkTableListenerStartupActivity"/>
35+
<postStartupActivity implementation="org.elixir_lang.mix.DepsCheckerStartupActivity"/>
3536

3637
<!-- <errorHandler implementation="org.elixir_lang.errorreport.Submitter"/>-->
3738
<errorHandler implementation="com.intellij.diagnostic.JetBrainsMarketplaceErrorReportSubmitter"/>
@@ -46,6 +47,7 @@
4647

4748
<editorNotificationProvider implementation="org.elixir_lang.notification.setup_sdk.Provider"/>
4849
<notificationGroup displayType="BALLOON" id="Elixir"/>
50+
<notificationGroup displayType="STICKY_BALLOON" id="Elixir Mix Deps"/>
4951

5052
<!-- Distillery -->
5153
<configurationType implementation="org.elixir_lang.distillery.configuration.Type"/>
@@ -347,9 +349,9 @@
347349
<separator/>
348350
<!--suppress PluginXmlCapitalization -->
349351
<action id="Elixir.InstallMixDependencies" class="org.elixir_lang.action.InstallMixDependenciesAction"
350-
text="Install Mix Dependencies (hex, rebar, deps.get)"
352+
text="Install Mix Deps (hex, rebar, deps.get, deps.compile)"
351353
icon="org.elixir_lang.mix.configuration.Icons.TYPE"
352-
description="Installs hex, rebar, and fetches Mix dependencies"/>
354+
description="Installs hex, rebar, fetches Mix deps, and compiles them"/>
353355
<separator/>
354356
<add-to-group group-id="ToolsMenu" anchor="last"/>
355357
</group>

src/org/elixir_lang/PackageManager.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package org.elixir_lang
22

33
import com.intellij.openapi.extensions.ExtensionPointName
4+
import com.intellij.openapi.project.Project
5+
import com.intellij.openapi.projectRoots.Sdk
6+
import com.intellij.openapi.vfs.VirtualFile
47
import org.elixir_lang.package_manager.DepGatherer
8+
import org.elixir_lang.package_manager.DepsStatusResult
59

610
interface PackageManager {
711
val fileName: String
812
fun depGatherer(): DepGatherer
13+
fun depsStatus(project: Project, packageVirtualFile: VirtualFile, sdk: Sdk?): DepsStatusResult =
14+
DepsStatusResult.Unsupported
915
}
1016

1117
fun packageManagers(): Array<out PackageManager> {

src/org/elixir_lang/action/InstallMixDependenciesAction.kt

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
package org.elixir_lang.action
22

3+
import com.intellij.execution.ExecutionListener
4+
import com.intellij.execution.ExecutionManager
5+
import com.intellij.execution.ProgramRunnerUtil
6+
import com.intellij.execution.RunManager
7+
import com.intellij.execution.executors.DefaultRunExecutor
8+
import com.intellij.execution.process.ProcessHandler
9+
import com.intellij.execution.runners.ExecutionEnvironment
310
import com.intellij.openapi.actionSystem.AnAction
411
import com.intellij.openapi.actionSystem.AnActionEvent
12+
import com.intellij.openapi.components.service
513
import com.intellij.openapi.module.ModuleManager
614
import com.intellij.openapi.project.Project
715
import com.intellij.openapi.projectRoots.Sdk
816
import com.intellij.openapi.roots.ModuleRootManager
917
import com.intellij.openapi.roots.ProjectRootManager
1018
import com.intellij.openapi.vfs.VirtualFile
11-
import org.elixir_lang.mix.runner.MixTaskRunner
19+
import com.intellij.openapi.util.Disposer
20+
import org.elixir_lang.mix.createInstallMixDependenciesRunConfiguration
21+
import org.elixir_lang.mix.DepsCheckerService
1222
import org.elixir_lang.notification.setup_sdk.Notifier
23+
import org.elixir_lang.util.ElixirProjectDisposable
1324
import org.elixir_lang.sdk.elixir.Type as ElixirSdkType
1425

1526
class InstallMixDependenciesAction : AnAction() {
@@ -28,17 +39,42 @@ class InstallMixDependenciesAction : AnAction() {
2839
return
2940
}
3041

31-
val workingDirectory = projectRoot.path
32-
3342
try {
34-
val result = MixTaskRunner.installDependencies(project, workingDirectory, sdk)
43+
val settings = createInstallMixDependenciesRunConfiguration(project, projectRoot)
44+
if (settings == null) {
45+
Notifier.mixDepsError(project, "Could not determine module for ${projectRoot.path}")
46+
return
47+
}
3548

36-
result.fold(
37-
onSuccess = { Notifier.mixDepsInstallSuccess(project) },
38-
onFailure = { error ->
39-
Notifier.mixDepsInstallError(project, error.message ?: "Unknown error")
49+
val runManager = RunManager.getInstance(project)
50+
runManager.setTemporaryConfiguration(settings)
51+
runManager.selectedConfiguration = settings
52+
53+
val listenerDisposable = Disposer.newDisposable("mix-deps-install-listener")
54+
Disposer.register(ElixirProjectDisposable.getInstance(project), listenerDisposable)
55+
project.messageBus.connect(listenerDisposable).subscribe(
56+
ExecutionManager.EXECUTION_TOPIC,
57+
object : ExecutionListener {
58+
override fun processTerminated(
59+
executorId: String,
60+
env: ExecutionEnvironment,
61+
handler: ProcessHandler,
62+
exitCode: Int
63+
) {
64+
if (env.runProfile === settings.configuration) {
65+
project.service<DepsCheckerService>()
66+
.scheduleCheckNow("mix deps install completed")
67+
if (exitCode == 0) {
68+
Notifier.mixDepsInstallSuccess(project)
69+
} else {
70+
Notifier.mixDepsInstallError(project, "Non-zero exit code")
71+
}
72+
Disposer.dispose(listenerDisposable)
73+
}
4074
}
41-
)
75+
})
76+
77+
ProgramRunnerUtil.executeConfiguration(settings, DefaultRunExecutor.getRunExecutorInstance())
4278
} catch (e: Exception) {
4379
Notifier.mixDepsInstallError(project, e.message ?: "Unknown error")
4480
}

src/org/elixir_lang/mix/Configuration.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ open class Configuration(name: String, project: Project, configurationFactory: C
6161
mixArguments = value
6262
}
6363

64+
fun setProgramParameters(value: MutableList<String>) {
65+
mixArgumentList = value
66+
}
67+
6468
private var erlArgumentList: MutableList<String> = mutableListOf()
6569

6670
var erlArguments: String?
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package org.elixir_lang.mix
2+
3+
import com.intellij.openapi.Disposable
4+
import com.intellij.openapi.application.ApplicationManager
5+
import com.intellij.openapi.components.Service
6+
import com.intellij.openapi.diagnostic.logger
7+
import com.intellij.openapi.module.ModuleManager
8+
import com.intellij.openapi.project.Project
9+
import com.intellij.openapi.projectRoots.Sdk
10+
import com.intellij.openapi.roots.ModuleRootManager
11+
import com.intellij.openapi.roots.ProjectRootManager
12+
import com.intellij.openapi.util.io.FileUtil
13+
import com.intellij.openapi.vfs.VirtualFile
14+
import com.intellij.openapi.vfs.VirtualFileManager
15+
import com.intellij.openapi.vfs.newvfs.BulkFileListener
16+
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
17+
import com.intellij.util.Alarm
18+
import org.elixir_lang.notification.setup_sdk.Notifier
19+
import org.elixir_lang.package_manager.DepsStatusResult
20+
import org.elixir_lang.package_manager.virtualFile
21+
import org.elixir_lang.sdk.elixir.Type as ElixirSdkType
22+
import java.util.concurrent.atomic.AtomicBoolean
23+
24+
@Service(Service.Level.PROJECT)
25+
class DepsCheckerService(private val project: Project) : Disposable {
26+
private val alarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, this)
27+
private val checkInProgress = AtomicBoolean(false)
28+
@Volatile
29+
private var checkPending = false
30+
31+
init {
32+
project.messageBus.connect(this).subscribe(
33+
VirtualFileManager.VFS_CHANGES,
34+
object : BulkFileListener {
35+
override fun after(events: List<VFileEvent>) {
36+
if (events.any { isDepsChange(it) }) {
37+
scheduleCheck("deps change")
38+
}
39+
}
40+
}
41+
)
42+
}
43+
44+
fun scheduleInitialCheck() {
45+
scheduleCheck("startup", 0)
46+
}
47+
48+
fun scheduleCheckNow(reason: String) {
49+
scheduleCheck(reason, 0)
50+
}
51+
52+
private fun scheduleCheck(reason: String, delayMs: Int = DEPS_CHECK_DEBOUNCE_MS) {
53+
if (project.isDisposed) {
54+
return
55+
}
56+
57+
if (checkInProgress.get()) {
58+
checkPending = true
59+
return
60+
}
61+
62+
alarm.cancelAllRequests()
63+
alarm.addRequest({ runCheck(reason) }, delayMs)
64+
}
65+
66+
private fun runCheck(reason: String) {
67+
if (!checkInProgress.compareAndSet(false, true)) {
68+
return
69+
}
70+
71+
try {
72+
if (project.isDisposed) {
73+
return
74+
}
75+
LOG.debug("DepsCheckerService: Checking Mix deps ($reason)")
76+
val sdk = findElixirSdk(project)
77+
val projectRoots = selectTopLevelMixRoots(ProjectRootManager.getInstance(project).contentRootsFromAllModules)
78+
79+
var sawSupported = false
80+
var sawNonOk = false
81+
82+
for (root in projectRoots) {
83+
when (val statusResult = depsStatusResult(project, root, sdk)) {
84+
is DepsStatusResult.Available -> {
85+
sawSupported = true
86+
if (statusResult.status.hasNonOk) {
87+
sawNonOk = true
88+
}
89+
}
90+
is DepsStatusResult.Error -> {
91+
notifyOnEdt { Notifier.mixDepsCheckFailed(project, statusResult.message) }
92+
return
93+
}
94+
DepsStatusResult.Unsupported -> Unit
95+
}
96+
}
97+
98+
if (!sawSupported) {
99+
return
100+
}
101+
102+
if (sawNonOk) {
103+
notifyOnEdt { Notifier.mixDepsOutdated(project) }
104+
} else {
105+
notifyOnEdt { Notifier.clearMixDepsOutdated(project) }
106+
}
107+
} finally {
108+
checkInProgress.set(false)
109+
if (checkPending) {
110+
checkPending = false
111+
scheduleCheck("pending")
112+
}
113+
}
114+
}
115+
116+
private fun depsStatusResult(project: Project, projectRoot: VirtualFile, sdk: Sdk?): DepsStatusResult {
117+
val packageManagerVirtualFile = virtualFile(projectRoot)
118+
?: return DepsStatusResult.Unsupported
119+
val (packageManager, packageVirtualFile) = packageManagerVirtualFile
120+
return packageManager.depsStatus(project, packageVirtualFile, sdk)
121+
}
122+
123+
private fun findElixirSdk(project: Project): Sdk? {
124+
val elixirSdkType = ElixirSdkType.instance
125+
126+
val projectSdk = ProjectRootManager.getInstance(project).projectSdk
127+
if (projectSdk?.sdkType === elixirSdkType) {
128+
return projectSdk
129+
}
130+
131+
ModuleManager.getInstance(project).modules.forEach { module ->
132+
val moduleSdk = ModuleRootManager.getInstance(module).sdk
133+
if (moduleSdk?.sdkType === elixirSdkType) {
134+
return moduleSdk
135+
}
136+
}
137+
138+
return null
139+
}
140+
141+
private fun isDepsChange(event: VFileEvent): Boolean {
142+
val eventPath = FileUtil.toSystemIndependentName(event.path)
143+
val contentRoots = selectTopLevelMixRoots(ProjectRootManager.getInstance(project).contentRootsFromAllModules)
144+
145+
return contentRoots.any { root ->
146+
val depsPath = FileUtil.toSystemIndependentName(root.path) + "/deps"
147+
FileUtil.isAncestor(depsPath, eventPath, false)
148+
}
149+
}
150+
151+
internal fun selectTopLevelMixRoots(roots: List<VirtualFile>): List<VirtualFile> {
152+
val mixRoots = roots.filter { virtualFile(it) != null }
153+
if (mixRoots.size <= 1) {
154+
return mixRoots
155+
}
156+
157+
val mixRootPaths = mixRoots.map { FileUtil.toSystemIndependentName(it.path) }
158+
159+
return mixRoots.filter { root ->
160+
val rootPath = FileUtil.toSystemIndependentName(root.path)
161+
mixRootPaths.none { otherPath ->
162+
otherPath != rootPath && FileUtil.isAncestor(otherPath, rootPath, false)
163+
}
164+
}
165+
}
166+
167+
internal fun selectTopLevelMixRoots(roots: Array<out VirtualFile>): List<VirtualFile> =
168+
selectTopLevelMixRoots(roots.asList())
169+
170+
private fun notifyOnEdt(action: () -> Unit) {
171+
ApplicationManager.getApplication().invokeLater {
172+
if (!project.isDisposed) {
173+
action()
174+
}
175+
}
176+
}
177+
178+
override fun dispose() {}
179+
180+
companion object {
181+
private const val DEPS_CHECK_DEBOUNCE_MS: Int = 1500
182+
private val LOG = logger<DepsCheckerService>()
183+
}
184+
}

0 commit comments

Comments
 (0)