diff --git a/.github/actions/setup-jbr/action.yml b/.github/actions/setup-jbr/action.yml
index 591d6d183..2c368984d 100644
--- a/.github/actions/setup-jbr/action.yml
+++ b/.github/actions/setup-jbr/action.yml
@@ -9,3 +9,4 @@ runs:
distribution: 'jetbrains'
java-version: 21
cache: 'gradle'
+ java-package: 'jdk+jcef'
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d8c5b006b..96e2f21d1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
- ideaVersion: [ "2024.3.6" ]
+ ideaVersion: [ "2024.3.6", "2025.2.1" ]
steps:
- uses: actions/checkout@v5
diff --git a/.gitignore b/.gitignore
index 833cd8831..a526c5610 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ jps-shared/build
jps-shared/out
libs/
.intellijPlatform
+/.kotlin
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cb26a9eb..6d0432a49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## v22.0.0 (Unreleased)
+### Enhancements
+* [#3712](https://github.com/KronicDeth/intellij-elixir/pull/3712) - [@joshuataylor](https://github.com/joshuataylor)
+ * Upgrade Kotlin to 2.2.10, as it's deprecated in IntelliJ 2025.1+, various deprecations are also fixed.
+ * Fixed adding Erlang/Elixir SDKs in 2025.1+ IDEs.
+
## v21.0.0
### Enhancements
* [#3651](https://github.com/KronicDeth/intellij-elixir/pull/3681) - [@joshuataylor](https://github.com/joshuataylor)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1b8f55f84..54ff95e1a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,7 +31,7 @@
6. In "Import Project from Gradle"
1. Check "Use auto-import"
2. Check "Create separate module per source set"
- 3. Ensure Gradle JVM is **AT LEAST** Java 1.7 / 7. (Java 1.8 / 8 is recommended.)
+ 3. Ensure Gradle JVM is **AT LEAST** Java 21+.
Your import settings should look something like this:

4. Click Finish
diff --git a/README.md b/README.md
index 06a40c688..b0e5e84c7 100644
--- a/README.md
+++ b/README.md
@@ -233,6 +233,11 @@ Table of Contents[
* [Visibility](#visibility)
* [Call to Element](#call-to-element)
+ * [Experimental Features](#experimental-features)
+ * [~H Sigil HTML Injection](#h-sigil-html-injection-support)
+ * [How to enable ~H sigil HTML Injection](#how-to-enable-h-sigil-html-injection)
+ * [Providing feedback and reporting issues for the ~H Sigil HTML Injection Experimental Feature](#providing-feedback-and-reporting-issues-for-the-h-sigil-html-injection-experimental-feature))
+ * [Removing the green background for Injected language fragments](#removing-the-green-background-for-injected-language-fragments)
* [Installation](#installation)
* [Stable releases](#stable-releases)
* [Inside IDE using JetBrains repository](#inside-ide-using-jetbrains-repository)
@@ -5761,6 +5766,79 @@ The Visibility icons indicated whether the element is usable outside its definin
+## Experimental Features
+
+As we develop new functionality that requires additional testing and feedback, we offer an opt-in system for Experimental Features via the `Elixir Experimental Settings` page.
+
+You can view the currently available Experimental Features by navigating to `Languages & Frameworks` and selecting `Elixir Experimental Settings`, which is marked with the [BETA icon](https://plugins.jetbrains.com/docs/intellij/settings-guide.html#l6vycg_378). Alternatively, you can access it directly via [Settings | Languages & Frameworks | Elixir Experimental Settings](jetbrains://Idea/settings?name=Languages+%26+Frameworks--Elixir+Experimental+Settings).
+
+
+
+### ~H Sigil HTML Injection Support
+
+**Experimental Feature – available from version 2024.3+ (243.21565.180) and later**
+
+When working with Phoenix Live View templates within the IntelliJ Elixir plugin, you'll notice that sigils such as `~H` are rendered as strings, which means that out of the box there is no HTML syntax highlighting or autocomplete when working with Phoenix Live View, which is used for writing HEEx templates inside source files. `HEEx` is a HTML-aware and component-friendly extension of Elixir Embedded language, this can make editing templates tedious.
+
+This Experimental Feature introduces preliminary HTML injection support within `~H` sigils, enabling HTML syntax highlighting and autocomplete.
+
+**Before, you would see this rendered as a string:**
+
+
+
+**After enabling ~H Sigil HTML Injection:**
+
+
+
+> [!TIP]
+> The [Phoenix LiveView Documentation on sigil_H](https://hexdocs.pm/phoenix_live_view/1.0.3/Phoenix.Component.html#sigil_H/2) is a fantastic resource for understanding how the `~H` sigil works.
+
+**Note:** Elixir code completion within HTML attributes is not yet supported.
+
+However, it does open the door, thanks to [MultihostInjector](https://plugins.jetbrains.com/docs/intellij/language-injection.html#multihostinjector), we could possibly mix HTML+Elixir, allowing autocomplete of Elixir within HTML.. if anyone is daring enough to wrangle the MultihostInjector API!
+
+#### IntelliLang Plugin Requirement
+
+This functionality has a dependency on the [IntelliLang](https://plugins.jetbrains.com/plugin/13374-intellilang) plugin, which comes bundled with both IntelliJ Community/Ultimate, and other IDEs.
+
+However this is marked as an optional dependency for the plugin, and does not need to be enabled if you are not using the functionality. THe code won't run, and you won't see injections.
+
+More information about [Language Injections](https://www.jetbrains.com/help/idea/using-language-injections.html) is available in the IntelliJ IDEA documentation.
+
+#### How to Enable ~H Sigil HTML Injection
+
+To enable support for HTML syntax highlighting and autocomplete:
+
+1. Open [Settings](https://www.jetbrains.com/help/idea/configure-project-settings.html).
+2. Navigate to [Settings | Languages & Frameworks | Elixir Experimental Settings](jetbrains://Idea/settings?name=Languages+%26+Frameworks--Elixir+Experimental+Settings).
+3. Enable the **~H Sigil HTML Injection** feature.
+
+
+
+> [!NOTE]
+> This Experimental Feature is currently enabled on a **per-project** basis. We are considering adding application-level support or enabling it by default in future versions based on feedback
+
+#### Providing feedback and reporting issues for the ~H Sigil HTML Injection Experimental Feature
+
+Have feedback or encountered issues? Please share your thoughts, Exception Stacktraces on the dedicated [**\[Experimental Feature\] ~H Sigil HTML Injection #3678**](https://github.com/KronicDeth/intellij-elixir/issues/3678).
+
+#### Removing the Green Background for Injected Language Fragments
+
+By default, IntelliLang highlights injected content with a green background, which can be changed by within [Color Scheme settings](https://www.jetbrains.com/help/idea/settings-colors-and-fonts.html).
+
+However, note that this change will apply to **all injected language fragments**, not just `~H` sigils HTML injections.
+
+> We are investigating the use of [InjectionBackgroundSuppressor](https://github.com/JetBrains/intellij-community/blob/idea/243.21565.193/platform/analysis-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionBackgroundSuppressor.java) to selectively disable background highlighting, but this is still a work in progress.
+
+If you're okay with disabling the background for all injections:
+
+1. Open [Settings](https://www.jetbrains.com/help/idea/configure-project-settings.html).
+2. Navigate to [Settings | Editor | Color Scheme | General](jetbrains://Idea/settings?name=Editor--Color+Scheme).
+3. Under the `Code` section, find `Injected Language Fragment`.
+4. Uncheck **Background** or change the colour to your preference.
+
+
+
## Installation
### Stable releases
diff --git a/build.gradle b/build.gradle
index 0f44701dd..6d801039d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,11 +1,15 @@
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
+import org.jetbrains.intellij.platform.gradle.models.ProductRelease
import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins {
- id "org.jetbrains.intellij.platform" version "2.1.0"
- id "org.jetbrains.kotlin.jvm" version "1.9.25"
- id "de.undercouch.download" version "4.1.2"
+ id "org.jetbrains.intellij.platform" version "2.9.0"
+ id "org.jetbrains.kotlin.jvm" version "2.2.20"
+ id "de.undercouch.download" version "5.6.0"
id 'com.adarshr.test-logger' version '4.0.0'
}
@@ -24,23 +28,31 @@ ext {
quoterZipPath = "${cachePath}/intellij_elixir-${quoterVersion}.zip"
quoterZipRootPath = "${cachePath}/intellij_elixir-${quoterVersion}"
- if (project.hasProperty("isRelease") && isRelease) {
+ def baseVersion = providers.gradleProperty("pluginVersion").get()
+
+ publishChannelProperty = providers.gradleProperty("publishChannel").getOrElse("canary")
+ if (publishChannelProperty == "default") {
versionSuffix = ""
- channel = "default"
+ }
+ // if versionSuffix gradle property is set, use it, it will append - to the base version
+ // Check if versionSuffix exists and is not empty
+ else if (providers.gradleProperty("versionSuffix").isPresent() && !providers.gradleProperty("versionSuffix").get().isEmpty()) {
+ versionSuffix = "-${providers.gradleProperty("versionSuffix").get()}"
} else {
def date = new Date().format("yyyyMMddHHmmss", TimeZone.getTimeZone("UTC"))
versionSuffix = "-pre+$date"
- channel = "canary"
}
- version "$pluginVersion$versionSuffix"
+ pluginVersion = "$baseVersion$versionSuffix"
}
allprojects {
apply plugin: 'java'
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
- tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }
+ java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+ tasks.withType(JavaCompile).tap { configureEach { options.encoding = 'UTF-8' } }
}
subprojects {
apply plugin: 'org.jetbrains.intellij.platform.module'
@@ -58,7 +70,7 @@ subprojects {
create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion"))
bundledPlugins providers.gradleProperty("platformBundledPlugins").map { it.split(',').toList() }
- plugins providers.gradleProperty("platformPlugins").map { it.split(',').toList() }
+ bundledModules providers.gradleProperty("platformBundledModules").map { it.split(',').toList() }
instrumentationTools()
pluginVerifier()
@@ -76,8 +88,8 @@ subprojects {
java.srcDir 'tests'
}
}
-
}
+
sourceSets {
main {
java.srcDirs 'src', 'gen'
@@ -88,14 +100,18 @@ sourceSets {
}
}
intellijPlatform {
+ // don't buildSearchableOptions/instrumentCode if publishChannel is canary.
+ if (publishChannelProperty == "canary") {
+ buildSearchableOptions = false
+ instrumentCode = false
+ }
pluginConfiguration {
def stripTag = { text, tag -> text.replace("<${tag}>", "").replace("${tag}>", "") }
- def bodyInnerHTML = { path ->
- stripTag(stripTag(file(path).text, "html"), "body")
- }
+ def bodyInnerHTML = { path -> stripTag(stripTag(file(path).text, "html"), "body") }
id = providers.gradleProperty("pluginGroup")
name = providers.gradleProperty("pluginName")
+ version = pluginVersion
changeNotes.set(bodyInnerHTML("resources/META-INF/changelog.html"))
description.set(bodyInnerHTML("resources/META-INF/description.html"))
@@ -112,12 +128,26 @@ intellijPlatform {
}
}
+ signPlugin {
+ certificateChain = providers.environmentVariable("IJ_CERTIFICATE_CHAIN")
+ privateKey = providers.environmentVariable("IJ_PRIVATE_KEY")
+ password = providers.environmentVariable("IJ_PRIVATE_KEY_PASSWORD")
+ }
+
+
+ buildPlugin {
+ // if -PpluginName exists, use it, or fallback to intellij-elixir. Not sure how to override otherwise.
+ archiveBaseName = providers.gradleProperty("pluginDistributionName").getOrElse("intellij-elixir")
+ }
+
+ publishing {
+ channels = [publishChannelProperty]
+ }
+
publishPlugin {
token = provider {
System.getenv("JET_BRAINS_MARKETPLACE_TOKEN")
}
- channels = publishChannels.split(',').toList()
-
// Use the path from the -PdistributionFile property if it exists,
// otherwise fall back to the old environment variable method.
if (project.hasProperty("distributionFile")) {
@@ -128,15 +158,28 @@ intellijPlatform {
}
pluginVerification {
ides {
- ide(IntelliJPlatformType.IntellijIdeaCommunity, "243.12818.47")
+ // since 253.* (2025.3+), IntelliJ IDEA Community and Ultimate have been merged into IntelliJ IDEA
+ select {
+ it.types = [IntelliJPlatformType.IntellijIdeaCommunity]
+ it.untilBuild = '252.*'
+ }
+ select {
+ it.types = [IntelliJPlatformType.IntellijIdeaUltimate]
+ it.sinceBuild = '253'
+ }
+ ide(IntelliJPlatformType.IntellijIdeaCommunity, "2024.2.6")
+ ide(IntelliJPlatformType.IntellijIdeaCommunity, "2024.3.6")
+ ide(IntelliJPlatformType.IntellijIdeaCommunity, "2025.2.2")
}
}
+
}
apply plugin: "kotlin"
-tasks.withType(RunIdeTask) {
+// Configure all RunIdeTask instances (including the new platform-specific ones)
+tasks.withType(RunIdeTask).configureEach {
// Set JVM arguments
- jvmArguments.addAll(["-Didea.debug.mode=true", "-Didea.is.internal=true", "-Dlog4j2.debug=true", "-Dlogger.org=TRACE", "-XX:+AllowEnhancedClassRedefinition"])
+ jvmArguments.addAll(["-Didea.debug.mode=true", "-Didea.is.internal=true", "-Dlog4j2.debug=true", "-Dlogger.org=TRACE", "-XX:+AllowEnhancedClassRedefinition", "-XXHotswapAgent=fatjar"])
// Set system properties to debug log
systemProperty "idea.log.debug.categories", "org.elixir_lang"
@@ -148,19 +191,26 @@ tasks.withType(RunIdeTask) {
if (project.hasProperty("runIdeWorkingDirectory") && !project.property("runIdeWorkingDirectory").isEmpty()) {
workingDir = file(project.property("runIdeWorkingDirectory"))
}
+
+ def compatiblePluginsList = providers.gradleProperty("runIdeCompatiblePlugins").get().with { it.isEmpty() ? [] : it.split(",") }
+ if (compatiblePluginsList.size() > 0) {
+ dependencies {
+ intellijPlatform {
+ compatiblePlugins(compatiblePluginsList)
+ }
+ }
+ }
}
kotlin {
jvmToolchain(21)
}
-//noinspection GroovyAssignabilityCheck,GrUnresolvedAccess
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
- //noinspection GrUnresolvedAccess
- kotlinOptions {
- apiVersion = "1.7"
- jvmTarget = "21"
- freeCompilerArgs = ["-Xjvm-default=all"]
+tasks.withType(KotlinJvmCompile).configureEach {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ freeCompilerArgs.add("-Xjvm-default=all")
+ apiVersion.set(KotlinVersion.KOTLIN_2_2)
}
}
@@ -182,50 +232,30 @@ test {
}
}
+// Get the list of platforms from gradle.properties
+def runIdePlatformsList = providers.gradleProperty("runIdePlatforms").get().split(",")
+
intellijPlatformTesting {
- // Get the list of platforms from gradle.properties
- def platformsList = providers.gradleProperty("platformsList").get().split(",")
-
- // Use providers.gradleProperty to get the 'runIdePlugins' property
- def runIdePluginsProperty = providers.gradleProperty("runIdePlugins").getOrElse("")
- def runIdePluginsList = runIdePluginsProperty.split(",")
-
- platformsList.each { platform ->
- runIde.create("run${platform}") {
- type = IntelliJPlatformType."${platform}"
- version = providers.gradleProperty("platformVersion${platform}").get()
- prepareSandboxTask {
- sandboxDirectory = project.layout.buildDirectory.dir("${platform.toLowerCase()}-sandbox")
- }
+ runIde {
+ runIdePlatformsList.each { platform ->
+ "run${platform}" {
+ type = IntelliJPlatformType."${platform}"
+ version = providers.gradleProperty("platformVersion${platform}").get()
- // if runIdePluginsList is not empty, set the plugins
- if (runIdePluginsList.size() > 0) {
- plugins {
- // Apply each plugin from the 'runIdePluginsList'
- runIdePluginsList.each { plugin ->
- plugins(plugin.trim())
- }
+ prepareSandboxTask {
+ sandboxDirectory = project.layout.buildDirectory.dir("${platform.toLowerCase()}-sandbox")
}
}
- }
- // if enableEAPIDEs is true, create an EAP instance
- if (providers.gradleProperty("enableEAPIDEs").get().toLowerCase() == "true") {
- runIde.create("run${platform}EAP") {
- type = IntelliJPlatformType."${platform}"
- version = providers.gradleProperty("platformVersion${platform}EAP").get()
- prepareSandboxTask {
- sandboxDirectory = project.layout.buildDirectory.dir("${platform.toLowerCase()}_eap-sandbox")
- }
- useInstaller = false
-
- // if runIdePluginsList is not empty, set the plugins
- if (runIdePluginsList.size() > 0) {
- plugins {
- // Apply each plugin from the 'runIdePluginsList'
- runIdePluginsList.each { plugin ->
- plugins(plugin.trim())
- }
+ // if enableEAPIDEs is true, create an EAP instance
+ if (providers.gradleProperty("enableEAPIDEs").get().toLowerCase() == "true") {
+ "run${platform}EAP" {
+ type = IntelliJPlatformType."${platform}"
+ version = providers.gradleProperty("platformVersion${platform}EAP").get()
+ useInstaller = false
+
+ prepareSandboxTask {
+ sandboxDirectory = project.layout.buildDirectory.dir("${platform.toLowerCase()}_eap-sandbox")
}
}
}
@@ -243,7 +273,7 @@ task testCompilation(type: Test, group: 'Verification', dependsOn: [classes, tes
}
repositories {
- maven { url 'https://maven-central.storage.googleapis.com' }
+ maven { url = 'https://maven-central.storage.googleapis.com' }
mavenCentral()
intellijPlatform {
defaultRepositories()
@@ -254,9 +284,8 @@ dependencies {
intellijPlatform {
create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion"))
- bundledPlugins providers.gradleProperty("platformBundledPlugins").map { it.split(',').toList() }
- plugins providers.gradleProperty("platformPlugins").map { it.split(',').toList() }
-
+ bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',').toList() })
+ bundledModules(providers.gradleProperty("platformBundledModules").map { it.split(',').toList() })
instrumentationTools()
pluginVerifier()
zipSigner()
@@ -267,13 +296,13 @@ dependencies {
implementation project(':jps-builder')
implementation project(':jps-shared')
implementation files('lib/OtpErlang.jar')
- implementation group: 'commons-io', name: 'commons-io', version: '2.5'
+ implementation group: 'commons-io', name: 'commons-io', version: '2.20.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "org.opentest4j:opentest4j:1.3.0"
- testImplementation group: 'org.mockito', name: 'mockito-core', version: '2.2.9'
- testImplementation group: 'org.objenesis', name: 'objenesis', version: '2.4'
+ testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.19.0'
+ testImplementation group: 'org.objenesis', name: 'objenesis', version: '3.4'
}
compileJava {
dependsOn ':jps-shared:composedJar'
@@ -291,12 +320,12 @@ idea {
}
}
-task getElixir {
+tasks.register('getElixir') {
doLast {
- def folder = new File(elixirPath)
+ def folder = new File(elixirPath as String)
if (!folder.isDirectory() || folder.list().size() == 0) {
- download {
+ download.run {
src "https://github.com/elixir-lang/elixir/archive/v${elixirVersion}.zip"
dest "${rootDir}/cache/Elixir.${elixirVersion}.zip"
overwrite false
@@ -320,7 +349,7 @@ task getElixir {
task getQuoter {
doLast {
- download {
+ download.run {
src "https://github.com/KronicDeth/intellij_elixir/archive/v${quoterVersion}.zip"
dest quoterZipPath
overwrite false
@@ -390,7 +419,7 @@ task stopQuoter(type: Exec, dependsOn: releaseQuoter) {
}
runIde {
- systemProperty "idea.log.debug.categories", "org.elixir_lang=TRACE"
+ systemProperty "idea.log.debug.categories", "org.elixir_lang"
// When wanting to disable EDT slow assertion..
// systemProperty "ide.slow.operations.assertion", "true"
@@ -408,6 +437,7 @@ runIde {
//This disables the throwing of ProcessCanceledException, which is typically used to cancel long-running processes in IntelliJ IDEA. Disabling it can be useful in certain debugging scenarios.
jvmArgs "-Didea.debug.mode=true", "-XX:+AllowEnhancedClassRedefinition", "-Didea.is.internal=true", "-Dlog4j2.debug=true", "-Dlogger.org=TRACE", "-Didea.ProcessCanceledException=disabled"
maxHeapSize = "7g"
+ autoReload = false
// get from runIdeWorkingDirectory
if (project.hasProperty("runIdeWorkingDirectory") && !project.property("runIdeWorkingDirectory").isEmpty()) {
workingDir = file(project.property("runIdeWorkingDirectory"))
@@ -429,3 +459,32 @@ idea {
generatedSourceDirs += file('gen')
}
}
+
+tasks {
+ printProductsReleases {
+ channels = [ProductRelease.Channel.EAP]
+ // From gradle.properties.
+ types = [
+ IntelliJPlatformType.IntellijIdeaCommunity,
+ IntelliJPlatformType.IntellijIdeaUltimate,
+ IntelliJPlatformType.PhpStorm,
+ IntelliJPlatformType.PyCharmCommunity,
+ IntelliJPlatformType.PyCharmProfessional,
+ IntelliJPlatformType.WebStorm,
+ IntelliJPlatformType.RubyMine
+ ]
+ untilBuild = null
+
+ doLast {
+ def latestEap = productsReleases.get().max()
+ }
+ }
+}
+
+// Uncomment to allow using build-scan.
+//if (hasProperty('buildScan')) {
+// buildScan {
+// termsOfServiceUrl = 'https://gradle.com/terms-of-service'
+// termsOfServiceAgree = 'yes'
+// }
+//}
diff --git a/gradle.properties b/gradle.properties
index a4282d658..5b2c30756 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,61 +2,75 @@
pluginGroup=org.elixir_lang
pluginName=Elixir
pluginRepositoryUrl=https://github.com/KronicDeth/intellij-elixir/
-pluginVersion=21.0.0
+pluginVersion=22.0.0
vendorName=Elle Imhoff
vendorEmail=Kronic.Deth@gmail.com
# https://youtrack.jetbrains.com/articles/IDEA-A-2100661899/IntelliJ-IDEA-2024.1-241.14494.240-build-Release-Notes
-pluginSinceBuild=243.21565.180
+pluginSinceBuild=253.22441.1
pluginUntilBuild=
+#pluginVerificationIDEAVersions="251.26927.53"
+#pluginVerificationRubyMineVersions="251.26927.47"
# Set this to open
runIdeWorkingDirectory=
# Define versions for running the IDEs, as each IDE can release at different release versions.
-platformVersionIntellijIdeaCommunity=2024.3.2.2
-platformVersionIntellijIdeaUltimate=2024.3.2.2
-platformVersionRubyMine=2024.3
-platformVersionPyCharmCommunity=2024.3
-platformVersionPyCharmProfessional=2024.3
-platformVersionWebStorm=2024.3
+platformVersionIntellijIdeaCommunity=2025.2.1
+platformVersionIntellijIdeaUltimate=2025.2.1
+platformVersionRubyMine=2025.2.1
+platformVersionPyCharmCommunity=2025.2.1.1
+platformVersionPyCharmProfessional=2025.2.1.1
+platformVersionWebStorm=2025.2.1
enableEAPIDEs=true
-platformVersionIntellijIdeaCommunityEAP=251-EAP-SNAPSHOT
-platformVersionIntellijIdeaUltimateEAP=251-EAP-SNAPSHOT
-platformVersionPyCharmCommunityEAP=251-EAP-SNAPSHOT
-platformVersionPyCharmProfessionalEAP=251-EAP-SNAPSHOT
-platformVersionRubyMineEAP=251-EAP-SNAPSHOT
-platformVersionWebStormEAP=251-EAP-SNAPSHOT
+platformVersionIntellijIdeaCommunityEAP=253-EAP-SNAPSHOT
+platformVersionIntellijIdeaUltimateEAP=253-EAP-SNAPSHOT
+platformVersionPyCharmCommunityEAP=253-EAP-SNAPSHOT
+platformVersionPyCharmProfessionalEAP=253-EAP-SNAPSHOT
+platformVersionRubyMineEAP=253-EAP-SNAPSHOT
+platformVersionWebStormEAP=253-EAP-SNAPSHOT
# Comma-separated list of platforms to include
-platformsList=IntellijIdeaCommunity,IntellijIdeaUltimate,RubyMine,PyCharmCommunity,PyCharmProfessional,WebStorm
+runIdePlatforms=IntellijIdeaCommunity,IntellijIdeaUltimate,RubyMine,PyCharmCommunity,PyCharmProfessional,WebStorm
# The versions we target, 21 is needed for IntelliJ Plugins
javaVersion=21
javaTargetVersion=21
# Defined in `.tool-versions`, check via `elixir --version`
elixirVersion=1.13.4
+quoterVersion=2.1.0
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
# Target IntelliJ Community by default
-platformType=IC
-platformVersion=2024.3
-# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
-# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
-# https://plugins.jetbrains.com/plugin/24468-classic-ui
-# https://plugins.jetbrains.com/plugin/7641-action-tracker - Act
-# https://plugins.jetbrains.com/plugin/15104-ide-perf - IDE Performande
-# https://plugins.jetbrains.com/plugin/227-psiviewer - View PSI
-platformPlugins = PsiViewer:243.7768, com.google.ide-perf:1.3.2, org.jetbrains.action-tracker:0.3.3, com.intellij.classic.ui:243.21565.122,krasa.CpuUsageIndicator:1.18.0-IJ2023
-# Example: platformBundledPlugins = com.intellij.java
+platformType=IU
+platformVersion=253.22441.33
+pluginDistributionName=intellij-elixir
+# Plugins which will run ONLY when running `runIde` tasks, not for testing/release.
+#
+# Usage:
+# When using `./gradlew runIde`, pass the Gradle Property:
+# -PrunIdeCompatiblePlugins="PsiViewer,com.google.ide-perf,org.jetbrains.action-tracker,com.intellij.classic.ui,krasa.CpuUsageIndicator,IdeaVIM"
+#
+# Recommendations:
+# https://plugins.jetbrains.com/plugin/24468-classic-ui - Classic UI (old UI)
+# https://plugins.jetbrains.com/plugin/7641-action-tracker - Action Tracker (Allows to record actions performed by user in IntelliJ IDEs)
+# https://plugins.jetbrains.com/plugin/15104-ide-perf - IDE Performance
+# https://plugins.jetbrains.com/plugin/227-psiviewer - PSI Viewer (A Program Structure Interface (PSI) tree viewer)
+# IdeaVIM
+runIdeCompatiblePlugins=
# We need com.intellij.java to compile JPS, and markdown.
+# @todo fix org.intellij.intelliLang
platformBundledPlugins=org.intellij.plugins.markdown,com.intellij.java
+platformBundledModules=intellij.platform.langInjection,intellij.spellchecker
+
# Gradle Releases -> https://github.com/gradle/gradle/releases
# 8.5 is set because newer versions have weird run time caching issues, even with caching turned off.
# See https://github.com/gradle/gradle/issues/28974
-gradleVersion=8.12.1
+gradleVersion=8.14.3
# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency=false
-publishChannels=canary
-runIdePlugins=IdeaVim:2.16.0
+# Channel for plugin publishing - can be overridden to control publishing channel
+# Valid values: default, canary, beta, alpha, eap, etc
+publishChannel=canary
+versionSuffix=
# These must be set, or Out of Memory (OOM) errors will occur during compiling.
-org.gradle.jvmargs=-Xmx4096m
-kotlin.daemon.jvmargs=-Xmx4906m
+org.gradle.jvmargs=-Xmx7096m
+kotlin.daemon.jvmargs=-Xmx7906m
# @todo Once this has been tested to be stable with the intellij-elixir codebase, enable.
# Others have it on without issues, so I'm not overly worried - just want to confirm stability.
# Can always just turn it off for CI.
@@ -67,3 +81,4 @@ kotlin.daemon.jvmargs=-Xmx4906m
org.gradle.configuration-cache=false
org.gradle.caching=false
org.gradle.parallel=false
+#org.gradle.daemon=false
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index a4b76b953..1b33c55ba 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e18bc253b..7705927e9 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index f3b75f3b0..23d15a936 100755
--- a/gradlew
+++ b/gradlew
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -205,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 9b42019c7..5eed7ee84 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+set CLASSPATH=
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/jps-builder/build.gradle b/jps-builder/build.gradle
index 1d22d5d1d..a08226d5f 100644
--- a/jps-builder/build.gradle
+++ b/jps-builder/build.gradle
@@ -8,6 +8,11 @@ compileTestJava {
compileJava {
dependsOn(":jps-shared:composedJar")
}
+java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }
// Ensuring the necessary tasks are executed before tests
test {
@@ -30,4 +35,4 @@ test {
dependencies {
implementation project(':jps-shared')
-}
\ No newline at end of file
+}
diff --git a/jps-builder/tests/org/elixir_lang/jps/BuildResult.java b/jps-builder/tests/org/elixir_lang/jps/BuildResult.java
index 1b92bfcb6..052e2d731 100644
--- a/jps-builder/tests/org/elixir_lang/jps/BuildResult.java
+++ b/jps-builder/tests/org/elixir_lang/jps/BuildResult.java
@@ -1,7 +1,5 @@
package org.elixir_lang.jps;
-import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.util.Function;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.incremental.MessageHandler;
import org.jetbrains.jps.incremental.messages.BuildMessage;
@@ -10,6 +8,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
/**
* Created by zyuyou on 15/7/17.
@@ -56,9 +55,16 @@ public boolean isSuccessful(){
}
public void assertSuccessful(){
- Function toStringFunction = StringUtil.createToStringFunction(BuildMessage.class);
- Assert.assertTrue("Build failed. \nErrors:\n" + StringUtil.join(myErrorMessages, toStringFunction, "\n") +
- "\nInfo messages:\n" + StringUtil.join(myInfoMessages, toStringFunction, "\n"), isSuccessful());
+ String errors = myErrorMessages.stream()
+ .map(BuildMessage::toString)
+ .collect(Collectors.joining("\n"));
+
+ String infos = myInfoMessages.stream()
+ .map(BuildMessage::toString)
+ .collect(Collectors.joining("\n"));
+
+ Assert.assertTrue("Build failed. \nErrors:\n" + errors +
+ "\nInfo messages:\n" + infos, isSuccessful());
}
@NotNull
diff --git a/jps-shared/build.gradle b/jps-shared/build.gradle
index a869b0950..0854ee58f 100644
--- a/jps-shared/build.gradle
+++ b/jps-shared/build.gradle
@@ -1,2 +1,6 @@
jar.archiveFileName = "jps-shared.jar"
-testClasses.enabled = false
\ No newline at end of file
+testClasses.enabled = false
+java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
diff --git a/resources/META-INF/changelog.html b/resources/META-INF/changelog.html
index 8f2f5b206..144f8221d 100644
--- a/resources/META-INF/changelog.html
+++ b/resources/META-INF/changelog.html
@@ -1,5 +1,21 @@
+
+v22.0.0-EAP
+
+
v21.0.0
-
diff --git a/resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml b/resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml
new file mode 100644
index 000000000..ca37e762d
--- /dev/null
+++ b/resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 83d3b7839..ce26952c1 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -1,16 +1,19 @@
-
+
org.elixir_lang
Elixir
Elle Imhoff
-
+
+ com.intellij.modules.platform
com.intellij.modules.lang
+ com.intellij.modules.xml
org.intellij.plugins.markdown
com.intellij.modules.java
+ org.intellij.intelliLang
-
+
+
+
+
@@ -86,6 +94,12 @@
+
+
+ implementationClass="org.elixir_lang.ElixirSyntaxHighlighterFactory"
+ language="Elixir"/>
@@ -269,7 +283,6 @@
-
-
+
+
+
+
+
+
+
+
+
@@ -278,9 +277,9 @@
-
-
-
+
+
+
diff --git a/resources/colorSchemes/ElixirDefault.xml b/resources/colorSchemes/ElixirDefault.xml
index 201b9bc5a..7df518c15 100644
--- a/resources/colorSchemes/ElixirDefault.xml
+++ b/resources/colorSchemes/ElixirDefault.xml
@@ -184,7 +184,6 @@
-
@@ -278,9 +277,9 @@
-
-
-
+
+
+
diff --git a/resources/injection/elixirInjections.xml b/resources/injection/elixirInjections.xml
new file mode 100644
index 000000000..9e934eb69
--- /dev/null
+++ b/resources/injection/elixirInjections.xml
@@ -0,0 +1,17 @@
+
+
+
+ Sigil: Regular Expression
+
+
+
+
+
+
+
+
+
+ Sigil: (Phoenix) EEX
+
+
+
diff --git a/screenshots/experimental/disable-injection-green-background.png b/screenshots/experimental/disable-injection-green-background.png
new file mode 100644
index 000000000..30b86bb33
Binary files /dev/null and b/screenshots/experimental/disable-injection-green-background.png differ
diff --git a/screenshots/experimental/elixir-experimental-settings-ui.png b/screenshots/experimental/elixir-experimental-settings-ui.png
new file mode 100644
index 000000000..83c05a847
Binary files /dev/null and b/screenshots/experimental/elixir-experimental-settings-ui.png differ
diff --git a/screenshots/experimental/h-sigil-html-before.png b/screenshots/experimental/h-sigil-html-before.png
new file mode 100644
index 000000000..5fa5d83e7
Binary files /dev/null and b/screenshots/experimental/h-sigil-html-before.png differ
diff --git a/screenshots/experimental/h-sigil-html.png b/screenshots/experimental/h-sigil-html.png
new file mode 100644
index 000000000..86677fb17
Binary files /dev/null and b/screenshots/experimental/h-sigil-html.png differ
diff --git a/settings.gradle b/settings.gradle
index 2267b9f98..65cdceb7e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,6 +1,6 @@
plugins {
- id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'intellij-elixir'
include 'jps-shared'
-include 'jps-builder'
\ No newline at end of file
+include 'jps-builder'
diff --git a/src/org/elixir_lang/ElixirSyntaxHighlighter.kt b/src/org/elixir_lang/ElixirSyntaxHighlighter.kt
index 40385a472..19489b973 100644
--- a/src/org/elixir_lang/ElixirSyntaxHighlighter.kt
+++ b/src/org/elixir_lang/ElixirSyntaxHighlighter.kt
@@ -222,8 +222,8 @@ class ElixirSyntaxHighlighter : SyntaxHighlighterBase() {
('A'..'Z').flatMap { letter ->
arrayOf("LOWER", "UPPER").map { capitalization ->
val name: Char = when (capitalization) {
- "LOWER" -> letter.toLowerCase()
- "UPPER" -> letter.toUpperCase()
+ "LOWER" -> letter.lowercaseChar()
+ "UPPER" -> letter.uppercaseChar()
else -> '?'
}
@@ -235,8 +235,8 @@ class ElixirSyntaxHighlighter : SyntaxHighlighterBase() {
val SIGIL_NAMES: kotlin.collections.List = ('A'..'Z').flatMap { letter ->
arrayOf("LOWER", "UPPER").map { capitalization ->
val name: Char = when (capitalization) {
- "LOWER" -> letter.toLowerCase()
- "UPPER" -> letter.toUpperCase()
+ "LOWER" -> letter.lowercaseChar()
+ "UPPER" -> letter.uppercaseChar()
else -> '?'
}
name
@@ -247,8 +247,8 @@ class ElixirSyntaxHighlighter : SyntaxHighlighterBase() {
val SIGIL_BY_NAME: Map = ('A'..'Z').flatMap { letter ->
arrayOf("LOWER", "UPPER").map { capitalization ->
val name: Char = when (capitalization) {
- "LOWER" -> letter.toLowerCase()
- "UPPER" -> letter.toUpperCase()
+ "LOWER" -> letter.lowercaseChar()
+ "UPPER" -> letter.uppercaseChar()
else -> '?'
}
name to TextAttributesKey.createTextAttributesKey("ELIXIR_SIGIL_${capitalization}_${letter}", SIGIL)
diff --git a/src/org/elixir_lang/UsageTypeProvider.kt b/src/org/elixir_lang/UsageTypeProvider.kt
index f0f92da1c..a04485976 100644
--- a/src/org/elixir_lang/UsageTypeProvider.kt
+++ b/src/org/elixir_lang/UsageTypeProvider.kt
@@ -15,7 +15,7 @@ import org.elixir_lang.psi.call.qualification.Qualified
import org.elixir_lang.reference.Callable
class UsageTypeProvider : com.intellij.usages.impl.rules.UsageTypeProviderEx {
- override fun getUsageType(element: PsiElement?, targets: Array): UsageType? =
+ override fun getUsageType(element: PsiElement, targets: Array): UsageType? =
when (element) {
is AtUnqualifiedNoParenthesesCall<*> -> MODULE_ATTRIBUTE_ACCUMULATE_OR_OVERRIDE
is AtOperation -> MODULE_ATTRIBUTE_READ
diff --git a/src/org/elixir_lang/action/DeleteAllSdksAction.kt b/src/org/elixir_lang/action/DeleteAllSdksAction.kt
new file mode 100644
index 000000000..ea5f361c8
--- /dev/null
+++ b/src/org/elixir_lang/action/DeleteAllSdksAction.kt
@@ -0,0 +1,145 @@
+package org.elixir_lang.action
+
+import com.intellij.notification.NotificationGroupManager
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.edtWriteAction
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.ProjectJdkTable
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar
+import com.intellij.openapi.ui.Messages
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import org.elixir_lang.notification.setup_sdk.Notifier
+import org.elixir_lang.sdk.ProcessOutput
+import org.elixir_lang.sdk.elixir.Type as ElixirSdkType
+import org.elixir_lang.sdk.erlang.Type as ErlangSdkType
+
+class DeleteAllSdksAction : AnAction() {
+ companion object {
+ private val LOG = Logger.getInstance(DeleteAllSdksAction::class.java)
+ }
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+
+ try {
+ removeAllElixirAndErlangSdks(project)
+ } catch (ex: Exception) {
+ Notifier.sdkRefreshError(project, "Error removing SDKs: ${ex.message ?: "Unknown error"}")
+ }
+ }
+
+ override fun update(e: AnActionEvent) {
+ super.update(e)
+ val project = e.project
+ e.presentation.isVisible = project != null
+ }
+
+ private fun removeAllElixirAndErlangSdks(project: Project) {
+ val projectJdkTable = ProjectJdkTable.getInstance()
+ val allSdks = projectJdkTable.allJdks
+
+ // Find all Elixir and Erlang SDKs
+ val elixirSdks = allSdks.filter { it.sdkType is ElixirSdkType }
+ val erlangSdks = allSdks.filter { it.sdkType is ErlangSdkType }
+ // Remove Erlang SDKs first since Elixir SDKs depend on them
+ val allTargetSdks = erlangSdks + elixirSdks
+
+ if (allTargetSdks.isEmpty()) {
+ Notifier.sdkRefreshWarning(project, "No Elixir or Erlang SDKs found in the IDE.")
+ return
+ }
+
+ // Show confirmation dialog
+ val sdkNames = allTargetSdks.joinToString("\n") { "• ${it.name}" }
+ val result = Messages.showYesNoDialog(
+ project,
+ "Are you sure you want to delete all Elixir and Erlang SDKs?\n\n" +
+ "This will remove the following SDKs from the IDE:\n$sdkNames\n\n" +
+ "This action cannot be undone.",
+ "Delete All Elixir/Erlang SDKs",
+ Messages.getQuestionIcon()
+ )
+
+ if (result != Messages.YES) {
+ return
+ }
+
+ // Remove SDKs and their associated libraries using modal progress with proper threading
+ val sdksRemoved = runWithModalProgressBlocking(project, "Removing All Elixir/Erlang SDKs") {
+ var removedCount = 0
+
+ edtWriteAction {
+ LOG.info("Starting removal of ${allTargetSdks.size} SDK(s) and associated libraries")
+
+ for (sdk in allTargetSdks) {
+ try {
+ LOG.debug("Removing SDK and library: ${sdk.name}")
+
+ // First remove associated library (like the SDK listeners do)
+ removeAssociatedLibrary(sdk)
+
+ // Then remove the SDK itself
+ projectJdkTable.removeJdk(sdk)
+ removedCount++
+
+ LOG.debug("Successfully removed SDK and library: ${sdk.name}")
+ } catch (ex: Exception) {
+ LOG.error("Failed to remove SDK: ${sdk.name}", ex)
+ // Continue with other SDKs if one fails
+ continue
+ }
+ }
+ LOG.info("Completed removal of $removedCount SDK(s) and associated libraries")
+ }
+
+ removedCount
+ }
+
+ // Show success notification
+ if (sdksRemoved > 0) {
+ NotificationGroupManager.getInstance()
+ .getNotificationGroup("Elixir")
+ .createNotification(
+ "Elixir SDKs Removed Successfully",
+ "Successfully removed $sdksRemoved SDK${if (sdksRemoved == 1) "" else "s"} (${elixirSdks.size} Elixir, ${erlangSdks.size} Erlang) from the IDE.",
+ NotificationType.INFORMATION
+ )
+ .notify(project)
+ } else {
+ Notifier.sdkRefreshWarning(
+ project,
+ "No Elixir SDKs were removed. They may have been removed already or there was an error."
+ )
+ }
+ }
+
+ override fun isDumbAware(): Boolean = true
+
+ override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
+
+ /**
+ * Removes the library associated with an SDK, following the same pattern as the SDK listeners
+ * in sdks/Configurable.kt
+ */
+ private fun removeAssociatedLibrary(sdk: Sdk) {
+ try {
+ val libraryTable = LibraryTablesRegistrar.getInstance().libraryTable
+ val library = libraryTable.getLibraryByName(sdk.name)
+
+ if (library != null) {
+ LOG.debug("Removing associated library: ${sdk.name}")
+ libraryTable.removeLibrary(library)
+ } else {
+ LOG.debug("No associated library found for SDK: ${sdk.name}")
+ }
+ } catch (ex: Exception) {
+ LOG.warn("Failed to remove associated library for SDK: ${sdk.name}", ex)
+ // Don't fail the whole operation if library removal fails
+ }
+ }
+}
diff --git a/src/org/elixir_lang/action/RefreshActiveElixirSdkAction.kt b/src/org/elixir_lang/action/RefreshActiveElixirSdkAction.kt
new file mode 100644
index 000000000..83f25d344
--- /dev/null
+++ b/src/org/elixir_lang/action/RefreshActiveElixirSdkAction.kt
@@ -0,0 +1,142 @@
+package org.elixir_lang.action
+
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.platform.ide.progress.ModalTaskOwner
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import org.elixir_lang.notification.setup_sdk.Notifier
+import org.elixir_lang.sdk.erlang_dependent.SdkAdditionalData
+import org.elixir_lang.sdk.elixir.Type as ElixirSdkType
+import org.elixir_lang.sdk.erlang.Type as ErlangSdkType
+
+class RefreshActiveElixirSdkAction : AnAction() {
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+
+ try {
+ refreshElixirSdkPaths(project)
+ } catch (ex: Exception) {
+ Notifier.sdkRefreshError(project, ex.message ?: "Unknown error")
+ }
+ }
+
+ private fun refreshElixirSdkPaths(project: Project) {
+ // Find active SDKs in the project
+ val activeElixirSdks = getActiveElixirSdks(project)
+ val activeErlangSdks = getActiveErlangSdks(project)
+
+ val totalElixirSdks = activeElixirSdks.size
+ val totalErlangSdks = activeErlangSdks.size
+ val totalActiveSdks = totalElixirSdks + totalErlangSdks
+
+ if (totalActiveSdks == 0) {
+ Notifier.sdkRefreshWarning(project, "No active Elixir or Erlang SDKs found in this project.")
+ return
+ }
+
+ var refreshedElixirCount = 0
+ var refreshedErlangCount = 0
+
+ runWithModalProgressBlocking(
+ ModalTaskOwner.project(project), "Refreshing Active SDK Paths"
+ ) {
+ val elixirSdkType = ElixirSdkType.instance
+
+ // Refresh active Elixir SDKs
+ for (elixirSdk in activeElixirSdks) {
+ try {
+ refreshSingleElixirSdk(elixirSdk, elixirSdkType)
+ refreshedElixirCount++
+ } catch (ex: Exception) {
+ // Continue with other SDKs if one fails
+ continue
+ }
+ }
+
+ // Refresh active Erlang SDKs
+ for (erlangSdk in activeErlangSdks) {
+ try {
+ refreshSingleErlangSdk(erlangSdk, erlangSdk.sdkType as ErlangSdkType)
+ refreshedErlangCount++
+ } catch (ex: Exception) {
+ // Continue with other SDKs if one fails
+ continue
+ }
+ }
+ }
+
+ // Show success notification with counts
+ Notifier.sdkRefreshSuccess(project, refreshedElixirCount, totalElixirSdks, refreshedErlangCount, totalErlangSdks)
+ }
+
+ private fun refreshSingleElixirSdk(sdk: Sdk, sdkType: ElixirSdkType) {
+ // setupSdkPaths handles its own write action and SDK modificator management
+ // This clears existing paths and reconfigures them using the existing logic
+ sdkType.setupSdkPaths(sdk)
+ }
+
+ private fun refreshSingleErlangSdk(sdk: Sdk, sdkType: ErlangSdkType) {
+ // setupSdkPaths handles its own write action and SDK modificator management
+ // This clears existing paths and reconfigures them using the existing logic
+ sdkType.setupSdkPaths(sdk)
+ }
+
+ override fun isDumbAware(): Boolean = true
+
+ // Utility functions to detect active Elixir SDKs
+ private fun getActiveElixirSdks(project: Project): Set {
+ val activeSdks = mutableSetOf()
+ val elixirSdkType = ElixirSdkType.instance
+
+ // Check project-level SDK
+ val projectSdk = ProjectRootManager.getInstance(project).projectSdk
+ if (projectSdk?.sdkType === elixirSdkType) {
+ activeSdks.add(projectSdk)
+ }
+
+ // Check module-level SDKs
+ ModuleManager.getInstance(project).modules.forEach { module ->
+ val moduleSdk = ModuleRootManager.getInstance(module).sdk
+ if (moduleSdk?.sdkType === elixirSdkType) {
+ activeSdks.add(moduleSdk)
+ }
+ }
+
+ return activeSdks
+ }
+
+ private fun getActiveErlangSdks(project: Project): Set {
+ val activeSdks = mutableSetOf()
+
+ // Check project-level SDK
+ val projectSdk = ProjectRootManager.getInstance(project).projectSdk
+ if (projectSdk?.sdkType is ErlangSdkType) {
+ activeSdks.add(projectSdk)
+ }
+
+ // Check module-level SDKs
+ ModuleManager.getInstance(project).modules.forEach { module ->
+ val moduleSdk = ModuleRootManager.getInstance(module).sdk
+ if (moduleSdk?.sdkType is ErlangSdkType) {
+ activeSdks.add(moduleSdk)
+ }
+ }
+
+ // Also include Erlang SDKs referenced by active Elixir SDKs
+ val activeElixirSdks = getActiveElixirSdks(project)
+ activeElixirSdks.forEach { elixirSdk ->
+ val additionalData = elixirSdk.sdkAdditionalData as? SdkAdditionalData
+ val erlangSdk = additionalData?.getErlangSdk()
+ if (erlangSdk != null) {
+ activeSdks.add(erlangSdk)
+ }
+ }
+
+ return activeSdks
+ }
+}
diff --git a/src/org/elixir_lang/action/RefreshAllElixirSdksAction.kt b/src/org/elixir_lang/action/RefreshAllElixirSdksAction.kt
new file mode 100644
index 000000000..ed06b3e82
--- /dev/null
+++ b/src/org/elixir_lang/action/RefreshAllElixirSdksAction.kt
@@ -0,0 +1,101 @@
+package org.elixir_lang.action
+
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.projectRoots.ProjectJdkTable
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.platform.ide.progress.ModalTaskOwner
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import org.elixir_lang.notification.setup_sdk.Notifier
+import org.elixir_lang.sdk.elixir.Type as ElixirSdkType
+import org.elixir_lang.sdk.erlang.Type as ErlangSdkType
+
+class RefreshAllElixirSdksAction : AnAction() {
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+
+ try {
+ refreshAllElixirSdkPaths(project)
+ } catch (ex: Exception) {
+ Notifier.sdkRefreshError(project, ex.message ?: "Unknown error")
+ }
+ }
+
+ private fun refreshAllElixirSdkPaths(project: com.intellij.openapi.project.Project) {
+ // Find all configured SDKs in the IDE
+ val allElixirSdks = getAllElixirSdks()
+ val allErlangSdks = getAllErlangSdks()
+
+ val totalElixirSdks = allElixirSdks.size
+ val totalErlangSdks = allErlangSdks.size
+ val totalSdks = totalElixirSdks + totalErlangSdks
+
+ if (totalSdks == 0) {
+ Notifier.sdkRefreshWarning(project, "No Elixir or Erlang SDKs are configured in the IDE.")
+ return
+ }
+
+ var refreshedElixirCount = 0
+ var refreshedErlangCount = 0
+
+ runWithModalProgressBlocking(
+ ModalTaskOwner.project(project), "Refreshing All SDK Paths"
+ ) {
+ val elixirSdkType = ElixirSdkType.instance
+
+ // Refresh all Elixir SDKs
+ for (elixirSdk in allElixirSdks) {
+ try {
+ refreshSingleElixirSdk(elixirSdk, elixirSdkType)
+ refreshedElixirCount++
+ } catch (ex: Exception) {
+ // Continue with other SDKs if one fails
+ continue
+ }
+ }
+
+ // Refresh all Erlang SDKs
+ for (erlangSdk in allErlangSdks) {
+ try {
+ refreshSingleErlangSdk(erlangSdk, erlangSdk.sdkType as ErlangSdkType)
+ refreshedErlangCount++
+ } catch (ex: Exception) {
+ // Continue with other SDKs if one fails
+ continue
+ }
+ }
+ }
+
+ // Show success notification with counts
+ Notifier.sdkRefreshSuccess(project, refreshedElixirCount, totalElixirSdks, refreshedErlangCount, totalErlangSdks)
+ }
+
+ private fun refreshSingleElixirSdk(sdk: Sdk, sdkType: ElixirSdkType) {
+ // setupSdkPaths handles its own write action and SDK modificator management
+ // This clears existing paths and reconfigures them using the existing logic
+ sdkType.setupSdkPaths(sdk)
+ }
+
+ private fun refreshSingleErlangSdk(sdk: Sdk, sdkType: ErlangSdkType) {
+ // setupSdkPaths handles its own write action and SDK modificator management
+ // This clears existing paths and reconfigures them using the existing logic
+ sdkType.setupSdkPaths(sdk)
+ }
+
+ override fun isDumbAware(): Boolean = true
+
+ // Utility functions to get all configured Elixir SDKs
+ private fun getAllElixirSdks(): List {
+ val elixirSdkType = ElixirSdkType.instance
+ return ProjectJdkTable.getInstance().allJdks.filter { sdk ->
+ sdk.sdkType === elixirSdkType
+ }
+ }
+
+ private fun getAllErlangSdks(): List {
+ return ProjectJdkTable.getInstance().allJdks.filter { sdk ->
+ sdk.sdkType is ErlangSdkType
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/elixir_lang/beam/StubBuilder.kt b/src/org/elixir_lang/beam/StubBuilder.kt
index cc7bb08a2..04d6ad709 100644
--- a/src/org/elixir_lang/beam/StubBuilder.kt
+++ b/src/org/elixir_lang/beam/StubBuilder.kt
@@ -21,7 +21,7 @@ class StubBuilder : BinaryFileStubBuilder {
return if (stub != null) {
stub
} else {
- LOGGER.info("No stub built for file $fileContent")
+// LOGGER.debug("No stub built for file $fileContent")
null
}
}
diff --git a/src/org/elixir_lang/beam/assembly/Controls.kt b/src/org/elixir_lang/beam/assembly/Controls.kt
index 2c3f97961..1c6d7f855 100644
--- a/src/org/elixir_lang/beam/assembly/Controls.kt
+++ b/src/org/elixir_lang/beam/assembly/Controls.kt
@@ -1,7 +1,9 @@
package org.elixir_lang.beam.assembly
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.editor.Document
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFileFactory
@@ -178,8 +180,10 @@ class Controls(val cache: Cache, val project: Project): JBScrollPane() {
private fun computeDocumentText() = cache.code?.assembly(cache, assemblyOptions) ?: DEFAULT_TEXT
private fun setDocumentText() {
- ApplicationManager.getApplication().runWriteAction {
- document.setText(computeDocumentText())
+ runBlockingCancellable {
+ edtWriteAction {
+ document.setText(computeDocumentText())
+ }
}
}
}
diff --git a/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/definitions/definition/clause/Panel.kt b/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/definitions/definition/clause/Panel.kt
index 6111a58ec..23ecd0dcf 100644
--- a/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/definitions/definition/clause/Panel.kt
+++ b/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/definitions/definition/clause/Panel.kt
@@ -3,7 +3,9 @@
package org.elixir_lang.beam.chunk.debug_info.v1.elixir_erl.v1.definitions.definition.clause
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.editor.EditorFactory
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFileFactory
@@ -106,8 +108,10 @@ class Panel(private val definitionsTree: Tree, project: Project): JPanel(GridLay
else -> DEFAULT_TEXT
}
- ApplicationManager.getApplication().runWriteAction {
- document.setText(text)
+ runBlockingCancellable {
+ edtWriteAction {
+ document.setText(text)
+ }
}
}
diff --git a/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/type_specifications/Panel.kt b/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/type_specifications/Panel.kt
index 4c6b42c42..4753c6c0f 100644
--- a/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/type_specifications/Panel.kt
+++ b/src/org/elixir_lang/beam/chunk/debug_info/v1/elixir_erl/v1/type_specifications/Panel.kt
@@ -3,7 +3,9 @@
package org.elixir_lang.beam.chunk.debug_info.v1.elixir_erl.v1.type_specifications
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.editor.EditorFactory
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFileFactory
@@ -75,8 +77,10 @@ class Panel(private val typeSpecificationTree: Tree, project: Project) : JPanel(
else -> DEFAULT_TEXT
}
- ApplicationManager.getApplication().runWriteAction {
- document.setText(text)
+ runBlockingCancellable {
+ edtWriteAction {
+ document.setText(text)
+ }
}
}
diff --git a/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Char.kt b/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Char.kt
index ac0f947c6..d8e281d01 100644
--- a/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Char.kt
+++ b/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Char.kt
@@ -11,20 +11,18 @@ object Char {
ifTag(term, TAG) { toMacroStringDeclaredScope(it) }
fun toMacroStringDeclaredScope(term: OtpErlangTuple): MacroStringDeclaredScope =
- toString(term).let { MacroStringDeclaredScope(it, doBlock = false, Scope.EMPTY) }
+ MacroStringDeclaredScope(toString(term), doBlock = false, Scope.EMPTY)
private const val TAG = "char"
private fun codePointToString(term: OtpErlangLong): String {
val macroStringBuilder = StringBuilder().append('?')
- val codePoint = term.intValue()
-
- when (codePoint) {
- '\\'.toInt() -> macroStringBuilder.append("\\\\")
- '\n'.toInt() -> macroStringBuilder.append("\\n")
- '\r'.toInt() -> macroStringBuilder.append("\\r")
- ' '.toInt() -> macroStringBuilder.append("\\s")
- '\t'.toInt() -> macroStringBuilder.append("\\t")
+ when (val codePoint = term.intValue()) {
+ '\\'.code -> macroStringBuilder.append("\\\\")
+ '\n'.code -> macroStringBuilder.append("\\n")
+ '\r'.code -> macroStringBuilder.append("\\r")
+ ' '.code -> macroStringBuilder.append("\\s")
+ '\t'.code -> macroStringBuilder.append("\\t")
else -> macroStringBuilder.appendCodePoint(codePoint)
}
diff --git a/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Panel.kt b/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Panel.kt
index fbedb594d..49512d60f 100644
--- a/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Panel.kt
+++ b/src/org/elixir_lang/beam/chunk/debug_info/v1/erl_abstract_code/abstract_code_compiler_options/abstract_code/Panel.kt
@@ -2,8 +2,9 @@
package org.elixir_lang.beam.chunk.debug_info.v1.erl_abstract_code.abstract_code_compiler_options.abstract_code
-import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.editor.EditorFactory
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFileFactory
@@ -37,7 +38,6 @@ class Panel(private val formsTree: Tree, project: Project): JPanel(GridLayout(1,
private val attributeModuleByAttribute = createWeakValueMap()
- private var attributes: WeakReference = WeakReference(null)
private var attributesModule: WeakReference = WeakReference(null)
private var clauseByClause = createWeakValueMap()
@@ -47,7 +47,6 @@ class Panel(private val formsTree: Tree, project: Project): JPanel(GridLayout(1,
private val functionModuleByFunction = createWeakValueMap()
- private var functions: WeakReference = WeakReference(null)
private var functionsModule: WeakReference = WeakReference(null)
private var module: WeakReference = WeakReference(null)
@@ -204,8 +203,10 @@ class Panel(private val formsTree: Tree, project: Project): JPanel(GridLayout(1,
else -> DEFAULT_TEXT
}
- ApplicationManager.getApplication().runWriteAction {
- document.setText(text)
+ runBlockingCancellable {
+ edtWriteAction {
+ document.setText(text)
+ }
}
}
diff --git a/src/org/elixir_lang/beam/chunk/elixir_documentation/CallbackDoc.kt b/src/org/elixir_lang/beam/chunk/elixir_documentation/CallbackDoc.kt
index ea893ed20..28f0c9faa 100644
--- a/src/org/elixir_lang/beam/chunk/elixir_documentation/CallbackDoc.kt
+++ b/src/org/elixir_lang/beam/chunk/elixir_documentation/CallbackDoc.kt
@@ -14,19 +14,21 @@ data class CallbackDoc(val nameArity: NameArity, val line: Int, val kind: Kind,
CALLBACK,
MACROCALLBACK;
- val attributeName: String by lazy { name.toLowerCase() }
+ val attributeName: String by lazy { name.lowercase() }
val attribute: String by lazy { "@$attributeName" }
companion object {
private val KIND_BY_ATTRIBUTE_NAME =
- Kind.values().associateBy { it.attributeName }
+ Kind.values().associateBy { it.attributeName }
fun from(term: OtpErlangObject): Kind? =
when (term) {
is OtpErlangAtom ->
- from(term)
+ from(term)
+
else -> {
- logger.error("""
+ logger.error(
+ """
Kind `${term.javaClass}` is not an `OtpErlangAtom`
## kind
@@ -34,7 +36,8 @@ data class CallbackDoc(val nameArity: NameArity, val line: Int, val kind: Kind,
```elixir
${inspect(term)}
```
- """)
+ """
+ )
null
}
@@ -53,7 +56,8 @@ data class CallbackDoc(val nameArity: NameArity, val line: Int, val kind: Kind,
when (term) {
is OtpErlangTuple -> from(term)
else -> {
- logger.error("""
+ logger.error(
+ """
:callback_docs element is not a tuple
## element
@@ -61,7 +65,8 @@ data class CallbackDoc(val nameArity: NameArity, val line: Int, val kind: Kind,
```elixir
${inspect(term)}
```
- """.trimIndent())
+ """.trimIndent()
+ )
null
}
@@ -82,19 +87,20 @@ data class CallbackDoc(val nameArity: NameArity, val line: Int, val kind: Kind,
null
}
} else {
- logger.error("""
+ logger.error(
+ """
:callback_docs element tuple arity ($arity) is not 4
```elixir
${inspect(tuple)}
```
- """.trimIndent())
+ """.trimIndent()
+ )
null
}
}
-
}
}
diff --git a/src/org/elixir_lang/beam/chunk/elixir_documentation/Doc.kt b/src/org/elixir_lang/beam/chunk/elixir_documentation/Doc.kt
index 2c8eed08c..e238f2c2b 100644
--- a/src/org/elixir_lang/beam/chunk/elixir_documentation/Doc.kt
+++ b/src/org/elixir_lang/beam/chunk/elixir_documentation/Doc.kt
@@ -10,13 +10,15 @@ import org.elixir_lang.NameArity
import org.elixir_lang.beam.chunk.ElixirDocumentation.Companion.doc
import org.elixir_lang.beam.term.inspect
import org.elixir_lang.beam.term.line
+import java.util.Locale
+import java.util.Locale.getDefault
data class Doc(val nameArity: NameArity, val line: Int, val kind: Kind, val arguments: List, val doc: Any?) {
enum class Kind {
DEF,
DEFMACRO;
- val macro: String by lazy { name.toLowerCase() }
+ val macro: String by lazy { name.lowercase() }
companion object {
private val KIND_BY_MACRO = Kind.values().associateBy { it.macro }
diff --git a/src/org/elixir_lang/beam/chunk/elixir_documentation/Panel.kt b/src/org/elixir_lang/beam/chunk/elixir_documentation/Panel.kt
index e02478516..8c3850ad2 100644
--- a/src/org/elixir_lang/beam/chunk/elixir_documentation/Panel.kt
+++ b/src/org/elixir_lang/beam/chunk/elixir_documentation/Panel.kt
@@ -1,7 +1,9 @@
package org.elixir_lang.beam.chunk.elixir_documentation
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.editor.EditorFactory
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFileFactory
@@ -51,8 +53,10 @@ class Panel(private val elixirDocumentationTree: Tree, project: Project, private
else -> DEFAULT_TEXT
}
- ApplicationManager.getApplication().runWriteAction {
- document.setText(text)
+ runBlockingCancellable {
+ edtWriteAction {
+ document.setText(text)
+ }
}
}
diff --git a/src/org/elixir_lang/beam/chunk/elixir_documentation/TypeDoc.kt b/src/org/elixir_lang/beam/chunk/elixir_documentation/TypeDoc.kt
index 5ed1cc54f..94f6edfd5 100644
--- a/src/org/elixir_lang/beam/chunk/elixir_documentation/TypeDoc.kt
+++ b/src/org/elixir_lang/beam/chunk/elixir_documentation/TypeDoc.kt
@@ -15,7 +15,7 @@ class TypeDoc(val nameArity: NameArity, val line: Int, val kind: Kind, val doc:
TYPE,
TYPEP;
- val attributeName: String by lazy { name.toLowerCase() }
+ val attributeName: String by lazy { name.lowercase() }
val attribute: String by lazy { "@$attributeName" }
companion object {
diff --git a/src/org/elixir_lang/beam/term/Inspect.kt b/src/org/elixir_lang/beam/term/Inspect.kt
index 036016654..e3ff424e9 100644
--- a/src/org/elixir_lang/beam/term/Inspect.kt
+++ b/src/org/elixir_lang/beam/term/Inspect.kt
@@ -27,8 +27,8 @@ fun String.elixirEscape(): String {
private fun Int.elixirEscape(): IntStream =
when (this) {
- '\n'.toInt() -> "\\n".codePoints()
- '\r'.toInt() -> "\\r".codePoints()
+ '\n'.code -> "\\n".codePoints()
+ '\r'.code -> "\\r".codePoints()
27 -> "\\e".codePoints()
else -> IntStream.of(this)
}
diff --git a/src/org/elixir_lang/code_insight/line_marker_provider/CallDefinition.kt b/src/org/elixir_lang/code_insight/line_marker_provider/CallDefinition.kt
index 7698fe154..a1466d77f 100644
--- a/src/org/elixir_lang/code_insight/line_marker_provider/CallDefinition.kt
+++ b/src/org/elixir_lang/code_insight/line_marker_provider/CallDefinition.kt
@@ -52,6 +52,22 @@ class CallDefinition : LineMarkerProvider {
return callDefinitionSeparator(leafPsiElement)
}
+ private fun callDefinitionSeparator(call: Call): LineMarkerInfo<*> {
+ // Find the leaf element (identifier token) within the Call
+ val leafPsiElement = call
+ .functionNameElement()
+ ?.node
+ ?.findChildByType(ElixirTypes.IDENTIFIER_TOKEN) as LeafPsiElement?
+ ?: call
+ .node
+ .findChildByType(ElixirTypes.IDENTIFIER_TOKEN) as LeafPsiElement?
+ ?: error(
+ "Call (${call.text}) does not have an IDENTIFIER_TOKEN"
+ )
+
+ return callDefinitionSeparator(leafPsiElement)
+ }
+
private fun callDefinitionSeparator(psiElement: PsiElement): LineMarkerInfo<*> =
LineMarkerInfo(
psiElement,
diff --git a/src/org/elixir_lang/code_insight/line_marker_provider/Implementation.kt b/src/org/elixir_lang/code_insight/line_marker_provider/Implementation.kt
index 6dd3a1e80..58f1bb332 100644
--- a/src/org/elixir_lang/code_insight/line_marker_provider/Implementation.kt
+++ b/src/org/elixir_lang/code_insight/line_marker_provider/Implementation.kt
@@ -13,7 +13,9 @@ import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.NotNullLazyValue
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
+import com.intellij.psi.impl.source.tree.LeafPsiElement
import org.elixir_lang.Icons
+import org.elixir_lang.psi.ElixirTypes
import org.elixir_lang.beam.psi.impl.ModuleImpl
import org.elixir_lang.psi.CallDefinitionClause
import org.elixir_lang.psi.CallDefinitionClause.enclosingModularMacroCall
@@ -31,6 +33,17 @@ class Implementation : LineMarkerProvider {
else -> null
}
+ private fun getLeafElementForCall(call: Call): LeafPsiElement? {
+ // Find the leaf element (identifier token) within the Call for line marker registration
+ return call
+ .functionNameElement()
+ ?.node
+ ?.findChildByType(ElixirTypes.IDENTIFIER_TOKEN) as LeafPsiElement?
+ ?: call
+ .node
+ .findChildByType(ElixirTypes.IDENTIFIER_TOKEN) as LeafPsiElement?
+ }
+
private fun getLineMarkerInfo(call: Call): LineMarkerInfo<*>? =
if (Implementation.`is`(call)) {
val targets: NotNullLazyValue> = NotNullLazyValue.createValue {
@@ -43,9 +56,11 @@ class Implementation : LineMarkerProvider {
protocols
}
- ProtocolsGutterIconBuilder()
- .setTargets(targets)
- .createLineMarkerInfo(call)
+ getLeafElementForCall(call)?.let { leafElement ->
+ ProtocolsGutterIconBuilder()
+ .setTargets(targets)
+ .createLineMarkerInfo(leafElement)
+ }
} else if (CallDefinitionClause.`is`(call)) {
enclosingModularMacroCall(call)?.let { modularCall ->
CallDefinitionClause.nameArityInterval(call, ResolveState.initial())?.let { implNameArityInterval ->
@@ -92,9 +107,11 @@ class Implementation : LineMarkerProvider {
}
- ProtocolsGutterIconBuilder()
- .setTargets(targets)
- .createLineMarkerInfo(call)
+ getLeafElementForCall(call)?.let { leafElement ->
+ ProtocolsGutterIconBuilder()
+ .setTargets(targets)
+ .createLineMarkerInfo(leafElement)
+ }
} else {
null
}
diff --git a/src/org/elixir_lang/code_insight/line_marker_provider/Protocol.kt b/src/org/elixir_lang/code_insight/line_marker_provider/Protocol.kt
index a4c945607..3694dd637 100644
--- a/src/org/elixir_lang/code_insight/line_marker_provider/Protocol.kt
+++ b/src/org/elixir_lang/code_insight/line_marker_provider/Protocol.kt
@@ -14,7 +14,9 @@ import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.NotNullLazyValue
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
+import com.intellij.psi.impl.source.tree.LeafPsiElement
import org.elixir_lang.Icons
+import org.elixir_lang.psi.ElixirTypes
import org.elixir_lang.beam.psi.impl.ModuleImpl
import org.elixir_lang.psi.CallDefinitionClause
import org.elixir_lang.psi.CallDefinitionClause.enclosingModularMacroCall
@@ -32,6 +34,17 @@ class Protocol : LineMarkerProvider {
else -> null
}
+ private fun getLeafElementForCall(call: Call): LeafPsiElement? {
+ // Find the leaf element (identifier token) within the Call for line marker registration
+ return call
+ .functionNameElement()
+ ?.node
+ ?.findChildByType(ElixirTypes.IDENTIFIER_TOKEN) as LeafPsiElement?
+ ?: call
+ .node
+ .findChildByType(ElixirTypes.IDENTIFIER_TOKEN) as LeafPsiElement?
+ }
+
private fun getLineMarkerInfo(call: Call): LineMarkerInfo<*>? =
if (Protocol.`is`(call)) {
val targets: NotNullLazyValue> = NotNullLazyValue.createValue {
@@ -44,9 +57,11 @@ class Protocol : LineMarkerProvider {
implementations
}
- ImplsGutterIconBuilder()
- .setTargets(targets)
- .createLineMarkerInfo(call)
+ getLeafElementForCall(call)?.let { leafElement ->
+ ImplsGutterIconBuilder()
+ .setTargets(targets)
+ .createLineMarkerInfo(leafElement)
+ }
} else if (CallDefinitionClause.`is`(call)) {
enclosingModularMacroCall(call)?.let { modularCall ->
CallDefinitionClause.nameArityInterval(call, ResolveState.initial())?.let { protocolNameArityInterval ->
@@ -93,9 +108,11 @@ class Protocol : LineMarkerProvider {
}
- ImplsGutterIconBuilder()
- .setTargets(targets)
- .createLineMarkerInfo(call)
+ getLeafElementForCall(call)?.let { leafElement ->
+ ImplsGutterIconBuilder()
+ .setTargets(targets)
+ .createLineMarkerInfo(leafElement)
+ }
} else {
null
}
diff --git a/src/org/elixir_lang/configuration/ElixirCompilerOptionsConfigurable.java b/src/org/elixir_lang/configuration/ElixirCompilerOptionsConfigurable.java
index 39f24f584..a92812c10 100644
--- a/src/org/elixir_lang/configuration/ElixirCompilerOptionsConfigurable.java
+++ b/src/org/elixir_lang/configuration/ElixirCompilerOptionsConfigurable.java
@@ -20,22 +20,23 @@ public class ElixirCompilerOptionsConfigurable extends CompilerConfigurable {
private JCheckBox myWarningsAsErrorsCheckBox;
private final ElixirCompilerSettings mySettings;
+ private boolean myUiInitialized = false;
public ElixirCompilerOptionsConfigurable(Project project) {
super(project);
mySettings = ElixirCompilerSettings.getInstance(project);
-
- setupUiListeners();
}
private void setupUiListeners(){
- myRootPanel.addAncestorListener(new AncestorAdapter(){
- @Override
- public void ancestorAdded(AncestorEvent event) {
- reset();
- }
- });
+ if (myRootPanel != null) {
+ myRootPanel.addAncestorListener(new AncestorAdapter(){
+ @Override
+ public void ancestorAdded(AncestorEvent event) {
+ reset();
+ }
+ });
+ }
}
@NotNull
@@ -51,6 +52,10 @@ public String getDisplayName() {
@Override
public JComponent createComponent() {
+ if (!myUiInitialized) {
+ setupUiListeners();
+ myUiInitialized = true;
+ }
return myRootPanel;
}
diff --git a/src/org/elixir_lang/debugger/stack_frame/value/Presentation.kt b/src/org/elixir_lang/debugger/stack_frame/value/Presentation.kt
index b758e475f..4f62b4d32 100644
--- a/src/org/elixir_lang/debugger/stack_frame/value/Presentation.kt
+++ b/src/org/elixir_lang/debugger/stack_frame/value/Presentation.kt
@@ -164,8 +164,8 @@ class Presentation(private val myValue: Any) : XValuePresentation() {
s.toString().all { isPrintable(it) }
private fun isPrintable(c: Char): Boolean {
- return (c.toInt() in 32..126
- || c == '\n' || c == '\r' || c == '\t' || c.toInt() == 11 || c == '\b' || c == '\u000c' || c.toInt() == 27 || c.toInt() == 7) /* bell */
+ return (c.code in 32..126
+ || c == '\n' || c == '\r' || c == '\t' || c.code == 11 || c == '\b' || c == '\u000c' || c.code == 27 || c.code == 7) /* bell */
}
private fun renderErlangString(str: OtpErlangString, renderer: XValueTextRenderer) {
diff --git a/src/org/elixir_lang/distillery/Configuration.kt b/src/org/elixir_lang/distillery/Configuration.kt
index 09640771d..d80ae25f1 100644
--- a/src/org/elixir_lang/distillery/Configuration.kt
+++ b/src/org/elixir_lang/distillery/Configuration.kt
@@ -108,7 +108,7 @@ class Configuration(name: String, project: Project, configurationFactory: Config
}
var codeLoadingMode: CodeLoadingMode?
- get() = _envs[CODE_LOADING_MODE]?.let { CodeLoadingMode.valueOf(it.toUpperCase()) }
+ get() = _envs[CODE_LOADING_MODE]?.let { CodeLoadingMode.valueOf(it.uppercase()) }
set(codeLoadingMode) {
if (codeLoadingMode == null) {
_envs.remove(CODE_LOADING_MODE)
diff --git a/src/org/elixir_lang/distillery/configuration/editor/CodeLoadingMode.kt b/src/org/elixir_lang/distillery/configuration/editor/CodeLoadingMode.kt
index 1def0e30a..03ea7b2df 100644
--- a/src/org/elixir_lang/distillery/configuration/editor/CodeLoadingMode.kt
+++ b/src/org/elixir_lang/distillery/configuration/editor/CodeLoadingMode.kt
@@ -2,9 +2,9 @@ package org.elixir_lang.distillery.configuration.editor
enum class CodeLoadingMode {
EMBEDDED {
- override fun toString(): String = super.toString().toLowerCase()
+ override fun toString(): String = super.toString().lowercase()
},
INTERACTIVE {
- override fun toString(): String = super.toString().toLowerCase()
+ override fun toString(): String = super.toString().lowercase()
}
}
diff --git a/src/org/elixir_lang/errorreport/Logger.kt b/src/org/elixir_lang/errorreport/Logger.kt
index 650b50cc6..23859fb18 100644
--- a/src/org/elixir_lang/errorreport/Logger.kt
+++ b/src/org/elixir_lang/errorreport/Logger.kt
@@ -1,7 +1,7 @@
package org.elixir_lang.errorreport
import com.ericsson.otp.erlang.OtpErlangObject
-import com.intellij.diagnostic.AttachmentFactory
+import com.intellij.diagnostic.CoreAttachmentFactory
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
@@ -47,7 +47,7 @@ object Logger {
val message = message(containingFile, element)
val virtualFile = containingFile.virtualFile
if (virtualFile != null) {
- val attachment = AttachmentFactory.createAttachment(virtualFile)
+ val attachment = CoreAttachmentFactory.createAttachment(virtualFile)
logger.error(message, throwable, attachment)
} else {
logger.error(message, throwable)
diff --git a/src/org/elixir_lang/exunit/TestLineMarkerProvider.kt b/src/org/elixir_lang/exunit/TestLineMarkerProvider.kt
index 2744d3410..73ebe0f6b 100644
--- a/src/org/elixir_lang/exunit/TestLineMarkerProvider.kt
+++ b/src/org/elixir_lang/exunit/TestLineMarkerProvider.kt
@@ -59,7 +59,7 @@ class ExUnitLineMarkerProvider : RunLineMarkerContributor() {
if (SystemUtils.IS_OS_WINDOWS) {
// In Elixir, the drive letter is lowercase for some reason.
val location = "file://".length
- url = url.replaceRange(location, location + 1, url[location].toLowerCase().toString())
+ url = url.replaceRange(location, location + 1, url[location].lowercaseChar().toString())
}
val state = TestStateStorage.getInstance(element.project)?.getState(url)
return withExecutorActions(getTestStateIcon(state, isClass))
diff --git a/src/org/elixir_lang/facet/Configurable.kt b/src/org/elixir_lang/facet/Configurable.kt
index de0fbb47f..62fbc91aa 100644
--- a/src/org/elixir_lang/facet/Configurable.kt
+++ b/src/org/elixir_lang/facet/Configurable.kt
@@ -3,9 +3,12 @@ package org.elixir_lang.facet
import com.intellij.openapi.module.Module
import com.intellij.openapi.options.UnnamedConfigurable
import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.ui.components.JBLabel
import org.elixir_lang.facet.sdk.ComboBox
+import java.awt.FlowLayout
import javax.swing.JComponent
import javax.swing.JPanel
+
/**
* Either project or module
*/
@@ -17,11 +20,67 @@ abstract class Configurable(val module: Module) : UnnamedConfigurable {
var sdk: Sdk? = null
private lateinit var rootPanel: JPanel
private lateinit var sdkComboBox: ComboBox
+// private lateinit var deleteButton: JButton
// https://github.com/JetBrains/intellij-community/blob/84601d73c4ae4cc3615bcd73304e5b32e8ef8686/python/python-community-configure/src/com/jetbrains/python/configuration/PythonSdkDetailsDialog.java#L119-L159
- override fun createComponent(): JComponent = rootPanel
+ override fun createComponent(): JComponent {
+ if (!::rootPanel.isInitialized) {
+ sdkComboBox = ComboBox()
+// deleteButton = JButton("Delete SDK", AllIcons.General.Remove).apply {
+// toolTipText = "Delete the selected SDK configuration"
+// addActionListener {
+// deleteSelectedSdk()
+// }
+// }
+
+ rootPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 0)).apply {
+ add(JBLabel("Elixir SDK:"))
+ add(sdkComboBox)
+// add(deleteButton)
+ }
+
+ // Update delete button state when selection changes
+// sdkComboBox.addActionListener {
+// updateDeleteButtonState()
+// }
+// updateDeleteButtonState()
+ }
+ return rootPanel
+ }
+
+// private fun updateDeleteButtonState() {
+// if (::deleteButton.isInitialized) {
+// deleteButton.isEnabled = sdkComboBox.selectedItem != null
+// }
+// }
+//
+// private fun deleteSelectedSdk() {
+// val selectedSdk = sdkComboBox.selectedItem as? Sdk ?: return
+//
+// val result = Messages.showYesNoDialog(
+// rootPanel,
+// "Are you sure you want to delete the SDK '${selectedSdk.name}'?\n\nThis will remove it from all projects using this SDK.",
+// "Delete SDK",
+// Messages.getQuestionIcon()
+// )
+//
+// if (result == Messages.YES) {
+// // Remove from the project model
+// projectSdksModel.removeSdk(selectedSdk)
+//
+// // Select null (no SDK) in the combo box
+// sdkComboBox.selectedItem = null
+//
+// // Update button state
+// updateDeleteButtonState()
+// }
+// }
override fun isModified(): Boolean {
+ if (!::sdkComboBox.isInitialized) {
+ return false
+ }
+
val existingInitialSdk = initSdk()?.let {
projectSdksModel.findSdk(it.name)
}
@@ -30,10 +89,15 @@ abstract class Configurable(val module: Module) : UnnamedConfigurable {
}
override fun apply() {
- applySdk(sdkComboBox.selectedItem as Sdk?)
+ if (::sdkComboBox.isInitialized) {
+ applySdk(sdkComboBox.selectedItem as Sdk?)
+ }
}
override fun reset() {
- sdkComboBox.selectedItem = initSdk()?.let { projectSdksModel.findSdk(it.name) }
+ if (::sdkComboBox.isInitialized) {
+ sdkComboBox.selectedItem = initSdk()?.let { projectSdksModel.findSdk(it.name) }
+// updateDeleteButtonState()
+ }
}
}
diff --git a/src/org/elixir_lang/facet/sdk/Editor.kt b/src/org/elixir_lang/facet/sdk/Editor.kt
index 03825bf05..ea9edb0cf 100644
--- a/src/org/elixir_lang/facet/sdk/Editor.kt
+++ b/src/org/elixir_lang/facet/sdk/Editor.kt
@@ -198,7 +198,7 @@ class Editor(private val sdkModel: SdkModel, private val history: History, priva
for (type in sdkPathEditorByOrderRootType.keys) {
sdkPathEditorByOrderRootType[type]?.reset(sdkModificator)
}
- sdkModificator.commitChanges()
+ ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() }
setHomePathValue(FileUtil.toSystemDependentName(sdk.homePath ?: ""))
_versionString = null
homeFieldLabel.text = homeFieldLabelValue
diff --git a/src/org/elixir_lang/facet/sdks/Configurable.kt b/src/org/elixir_lang/facet/sdks/Configurable.kt
index 05855ad39..1194d78b7 100644
--- a/src/org/elixir_lang/facet/sdks/Configurable.kt
+++ b/src/org/elixir_lang/facet/sdks/Configurable.kt
@@ -1,6 +1,7 @@
package org.elixir_lang.facet.sdks
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.options.ex.ConfigurableCardPanel
import com.intellij.openapi.projectRoots.Sdk
@@ -24,6 +25,8 @@ import org.elixir_lang.facet.sdk.Editor
import javax.swing.JComponent
import javax.swing.JPanel
+private val LOG = logger()
+
fun Library.ModifiableModel.addRoots(sdk: Sdk) =
sdk
.rootProvider
@@ -111,6 +114,7 @@ abstract class Configurable: SearchableConfigurable, com.intellij.openapi.option
private fun addListeners() {
val listener = object : SdkModel.Listener {
override fun beforeSdkRemove(sdk: Sdk) {
+ LOG.debug("beforeSdkRemove(sdk.name='${sdk.name}')")
LibraryTablesRegistrar.getInstance().libraryTable.let { libraryTable ->
libraryTable.getLibraryByName(sdk.name)?.let { library ->
ApplicationManager.getApplication().runWriteAction {
@@ -121,6 +125,7 @@ abstract class Configurable: SearchableConfigurable, com.intellij.openapi.option
}
override fun sdkAdded(sdk: Sdk) {
+ LOG.debug("sdkAdded(sdk.name='${sdk.name}')")
LibraryTablesRegistrar.getInstance().libraryTable.let { libraryTable ->
ApplicationManager.getApplication().runWriteAction {
libraryTable.createLibrary(sdk.name).modifiableModel.apply {
@@ -134,6 +139,7 @@ abstract class Configurable: SearchableConfigurable, com.intellij.openapi.option
}
override fun sdkChanged(sdk: Sdk, previousName: String) {
+ LOG.debug("sdkChanged(sdk.name='${sdk.name}', previousName='$previousName')")
if (sdk.name != previousName) {
LibraryTablesRegistrar.getInstance().libraryTable.getLibraryByName(previousName)?.let { library ->
ApplicationManager.getApplication().runWriteAction {
@@ -159,6 +165,7 @@ abstract class Configurable: SearchableConfigurable, com.intellij.openapi.option
}
override fun sdkHomeSelected(sdk: Sdk, newSdkHome: String) {
+ LOG.debug("sdkHomeSelected(sdk.name='${sdk.name}', newSdkHome='$newSdkHome')")
}
}
projectSdksModel.addListener(listener)
diff --git a/src/org/elixir_lang/find_usages/handler/Call.kt b/src/org/elixir_lang/find_usages/handler/Call.kt
index 5a5ed77de..8b4a69ccd 100644
--- a/src/org/elixir_lang/find_usages/handler/Call.kt
+++ b/src/org/elixir_lang/find_usages/handler/Call.kt
@@ -108,3 +108,4 @@ private fun Iterable.withEnclosingModularMacroCall():
private fun Iterable.toSecondaryElements(): List =
this.flatMap { it.toSecondaryElements() }
+
diff --git a/src/org/elixir_lang/formatter/MixFormatExternalFormatProcessor.kt b/src/org/elixir_lang/formatter/MixFormatExternalFormatProcessor.kt
index c8d1842bb..5fc508d57 100644
--- a/src/org/elixir_lang/formatter/MixFormatExternalFormatProcessor.kt
+++ b/src/org/elixir_lang/formatter/MixFormatExternalFormatProcessor.kt
@@ -5,9 +5,11 @@ import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.TextRange
@@ -46,10 +48,10 @@ class MixFormatExternalFormatProcessor : ExternalFormatProcessor {
workingDirectory(source)?.let { workingDirectory ->
mostSpecificSdk(source)?.takeIf(::elixirSdkHasErlangSdk)?.let { sdk ->
format(workingDirectory, sdk, document.text)?.let { formattedText ->
- application.invokeLater {
+ runBlockingCancellable {
if (source.isValid) {
- CommandProcessor.getInstance().runUndoTransparentAction {
- application.runWriteAction {
+ edtWriteAction {
+ CommandProcessor.getInstance().runUndoTransparentAction {
document.setText(formattedText)
}
}
diff --git a/src/org/elixir_lang/injection/ElixirSigilInjectionSupport.kt b/src/org/elixir_lang/injection/ElixirSigilInjectionSupport.kt
new file mode 100644
index 000000000..4799ec14e
--- /dev/null
+++ b/src/org/elixir_lang/injection/ElixirSigilInjectionSupport.kt
@@ -0,0 +1,27 @@
+package org.elixir_lang.injection
+
+import com.intellij.psi.PsiLanguageInjectionHost
+import org.intellij.plugins.intelliLang.inject.AbstractLanguageInjectionSupport
+import org.jetbrains.annotations.NonNls
+
+internal class ElixirSigilInjectionSupport : AbstractLanguageInjectionSupport() {
+ override fun getId(): String {
+ return ELIXIR_SUPPORT_ID
+ }
+
+ override fun getPatternClasses(): Array> {
+ return arrayOf(ElixirSigilPatterns::class.java)
+ }
+
+ override fun isApplicableTo(host: PsiLanguageInjectionHost): Boolean {
+ return true
+ }
+
+ override fun useDefaultInjector(host: PsiLanguageInjectionHost): Boolean {
+ return true
+ }
+
+ companion object {
+ const val ELIXIR_SUPPORT_ID: @NonNls String = "elixir"
+ }
+}
diff --git a/src/org/elixir_lang/injection/ElixirSigilInjector.kt b/src/org/elixir_lang/injection/ElixirSigilInjector.kt
new file mode 100644
index 000000000..ee5cb8464
--- /dev/null
+++ b/src/org/elixir_lang/injection/ElixirSigilInjector.kt
@@ -0,0 +1,86 @@
+package org.elixir_lang.injection
+
+import com.intellij.lang.Language
+import com.intellij.lang.html.HTMLLanguage
+import com.intellij.lang.injection.MultiHostInjector
+import com.intellij.lang.injection.MultiHostRegistrar
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.psi.PsiElement
+import org.elixir_lang.psi.SigilHeredoc
+import org.elixir_lang.psi.SigilLine
+import org.intellij.lang.regexp.RegExpLanguage
+import org.elixir_lang.eex.Language as EexLanguage
+
+private val LOG = logger()
+
+internal class ElixirSigilInjector : MultiHostInjector {
+ override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) {
+ when (context) {
+ is SigilLine -> handleSigilLine(registrar, context)
+ is SigilHeredoc -> handleSigilHeredoc(registrar, context)
+ else -> return
+ }
+ }
+
+ private fun handleSigilLine(registrar: MultiHostRegistrar, sigilLine: SigilLine) {
+ if (!sigilLine.isValidHost) return
+ val lang = languageForSigil(sigilLine.sigilName()) ?: return
+
+ sigilLine.body?.let { lineBody ->
+ registrar
+ .startInjecting(lang)
+ .addPlace(null, null, sigilLine, lineBody.textRangeInParent)
+ .doneInjecting()
+ }
+ }
+
+ private fun handleSigilHeredoc(registrar: MultiHostRegistrar, sigilHeredoc: SigilHeredoc) {
+ if (!sigilHeredoc.isValidHost || sigilHeredoc.heredocLineList.isEmpty() || !sigilHeredoc.isValid) {
+ if (LOG.isDebugEnabled) {
+ LOG.debug(
+ "handleSigilHeredoc: returning early: " +
+ "isValidHost=${sigilHeredoc.isValidHost}, " +
+ "heredocLineList.isEmpty=${sigilHeredoc.heredocLineList.isEmpty()}, " +
+ "isValid=${sigilHeredoc.isValid}"
+ )
+ }
+ return
+ }
+
+ val lang = languageForSigil(sigilHeredoc.sigilName()) ?: return
+ registrar.startInjecting(lang)
+ if (LOG.isDebugEnabled) {
+ LOG.debug("handleSigilHeredoc: injecting ${lang.displayName} into ${sigilHeredoc.heredocLineList.size} lines")
+ }
+ for (item in sigilHeredoc.heredocLineList) {
+ if (item.isValid) {
+ if (LOG.isDebugEnabled) {
+ LOG.debug("handleSigilHeredoc: injecting into heredocLine: ${item.text}")
+ }
+ registrar.addPlace(null, null, sigilHeredoc, item.textRangeInParent)
+ } else if (LOG.isDebugEnabled) {
+ LOG.debug("handleSigilHeredoc: skipping invalid heredocLine")
+ }
+ }
+ if (LOG.isDebugEnabled) {
+ LOG.debug("handleSigilHeredoc: done injecting into ${sigilHeredoc.heredocLineList.size} lines")
+ }
+
+ registrar.doneInjecting()
+ }
+
+ override fun elementsToInjectIn() = listOf(SigilHeredoc::class.java, SigilLine::class.java)
+
+ private fun languageForSigil(sigilName: Char): Language? {
+ if (LOG.isDebugEnabled) {
+ LOG.debug("languageForSigil: sigilName='$sigilName'")
+ }
+
+ return when (sigilName) {
+ 'H' -> HTMLLanguage.INSTANCE
+ 'L' -> EexLanguage.INSTANCE
+ 'r' -> RegExpLanguage.INSTANCE
+ else -> null
+ }
+ }
+}
diff --git a/src/org/elixir_lang/injection/ElixirSigilPatterns.java b/src/org/elixir_lang/injection/ElixirSigilPatterns.java
new file mode 100644
index 000000000..00c659cce
--- /dev/null
+++ b/src/org/elixir_lang/injection/ElixirSigilPatterns.java
@@ -0,0 +1,38 @@
+package org.elixir_lang.injection;
+
+import com.intellij.patterns.ElementPattern;
+import com.intellij.patterns.PatternCondition;
+import com.intellij.patterns.PlatformPatterns;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ProcessingContext;
+import org.elixir_lang.psi.Sigil;
+import org.jetbrains.annotations.NotNull;
+
+public class ElixirSigilPatterns extends PlatformPatterns {
+ public static ElementPattern> sigil() {
+ return psiElement().inside(psiElement(Sigil.class));
+ }
+
+ @SuppressWarnings("unused")
+ public static ElementPattern> sigilWithName(String name) {
+ return and(sigil(), psiElement().with(new ElixirSigilPatterns.SigilWithName(name)));
+ }
+
+ public static class SigilWithName extends @NotNull PatternCondition {
+ Character expectedSigil;
+
+ public SigilWithName(String name) {
+ super(name);
+ expectedSigil = name.charAt(0);
+ }
+
+ @Override
+ public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext processingContext) {
+ if (psiElement instanceof Sigil) {
+ return ((Sigil) psiElement).sigilName() == expectedSigil;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt b/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
index 66235b070..b96c446f9 100644
--- a/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
+++ b/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
@@ -6,11 +6,19 @@ import org.elixir_lang.injection.markdown.Injector
import org.elixir_lang.psi.AtUnqualifiedNoParenthesesCall
import org.elixir_lang.psi.ElixirNoParenthesesKeywords
import org.elixir_lang.psi.Parent
+import org.elixir_lang.psi.Sigil
+import org.elixir_lang.settings.ElixirExperimentalSettings
object PsiLanguageInjectionHost {
@JvmStatic
- fun isValidHost(psiElement: PsiElement): Boolean =
- when (val greatGrandParent = psiElement.parent?.parent?.parent) {
+ fun isValidHost(psiElement: PsiElement): Boolean {
+ // If the element is a Sigil, then it is definitely a valid host
+ // @todo make this more precise, to ~H etc
+ if (ElixirExperimentalSettings.instance.state.enableHtmlInjection && psiElement as? Sigil != null) {
+ return true
+ }
+
+ return when (val greatGrandParent = psiElement.parent?.parent?.parent) {
is AtUnqualifiedNoParenthesesCall<*> -> Injector.isValidHost(greatGrandParent)
is ElixirNoParenthesesKeywords -> {
greatGrandParent
@@ -22,6 +30,7 @@ object PsiLanguageInjectionHost {
}
else -> false
}
+ }
@JvmStatic
fun createLiteralTextEscaper(parent: Parent): LiteralTextEscaper =
diff --git a/src/org/elixir_lang/mix/PackageManager.kt b/src/org/elixir_lang/mix/PackageManager.kt
index 9d78b826a..20d2c2a56 100644
--- a/src/org/elixir_lang/mix/PackageManager.kt
+++ b/src/org/elixir_lang/mix/PackageManager.kt
@@ -2,7 +2,11 @@ package org.elixir_lang.mix
import org.elixir_lang.package_manager.DepGatherer
-object PackageManager : org.elixir_lang.PackageManager {
- override val fileName: String = "mix.exs"
+class PackageManager : org.elixir_lang.PackageManager {
+ override val fileName: String = FILE_NAME
override fun depGatherer(): DepGatherer = org.elixir_lang.mix.DepGatherer()
+
+ companion object {
+ const val FILE_NAME = "mix.exs"
+ }
}
diff --git a/src/org/elixir_lang/mix/Project.kt b/src/org/elixir_lang/mix/Project.kt
index d23d9ce3b..b60530295 100644
--- a/src/org/elixir_lang/mix/Project.kt
+++ b/src/org/elixir_lang/mix/Project.kt
@@ -1,12 +1,13 @@
package org.elixir_lang.mix
-import com.intellij.openapi.application.runWriteAction
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.ModifiableModuleModel
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.ModifiableRootModel
@@ -83,12 +84,14 @@ object Project {
val createdRootModels = otpApps.mapNotNull { createModuleForOtpApp(it, moduleModel, rootModelModifier) }
if (createdRootModels.isNotEmpty()) {
- runWriteAction {
- for (rootModel in createdRootModels) {
- rootModel.commit()
- }
+ runBlockingCancellable {
+ edtWriteAction {
+ for (rootModel in createdRootModels) {
+ rootModel.commit()
+ }
- moduleModel.commit()
+ moduleModel.commit()
+ }
}
ProgressManager.getInstance()
diff --git a/src/org/elixir_lang/mix/Watcher.kt b/src/org/elixir_lang/mix/Watcher.kt
index 5bbdeedb0..d4c41f062 100644
--- a/src/org/elixir_lang/mix/Watcher.kt
+++ b/src/org/elixir_lang/mix/Watcher.kt
@@ -33,7 +33,7 @@ class Watcher(private val project: Project) : BulkFileListener {
}
private fun contentsChanged(event: VFileContentChangeEvent) {
- if (event.file.name == PackageManager.fileName) {
+ if (event.file.name == PackageManager.FILE_NAME) {
ModuleUtil.findModuleForFile(event.file, project)?.let { module ->
val eventFileParent = event.file.parent
val shouldSync =
diff --git a/src/org/elixir_lang/mix/project/DirectoryConfigurator.kt b/src/org/elixir_lang/mix/project/DirectoryConfigurator.kt
index 147900ba3..20017901c 100644
--- a/src/org/elixir_lang/mix/project/DirectoryConfigurator.kt
+++ b/src/org/elixir_lang/mix/project/DirectoryConfigurator.kt
@@ -16,9 +16,6 @@ import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ex.ProjectManagerEx
import com.intellij.openapi.roots.ModuleRootModificationUtil
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable
-import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
@@ -26,7 +23,6 @@ import com.intellij.platform.PlatformProjectOpenProcessor.Companion.runDirectory
import com.intellij.projectImport.ProjectAttachProcessor
import com.intellij.util.PlatformUtils
import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.elixir_lang.DepsWatcher
@@ -36,7 +32,6 @@ import org.elixir_lang.mix.Watcher
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
-import org.elixir_lang.sdk.elixir.Type
/**
* Used in Small IDEs like Rubymine that don't support [OpenProcessor].
@@ -173,7 +168,8 @@ class DirectoryConfigurator : com.intellij.platform.DirectoryProjectConfigurator
)
?.let { project ->
LOG.debug("runDirectoryProjectConfigurators for project: $project at $path")
- runDirectoryProjectConfigurators(path, project, false)
+ // @todo changed in 2025.3, to add createModule.
+ runDirectoryProjectConfigurators(path, project, newProject = false, createModule = false)
LOG.debug("runDirectoryProjectConfigurators complete for project: $project at $path")
LOG.debug("Saving settings for project: $project at $path")
diff --git a/src/org/elixir_lang/mix/project/OpenProcessor.kt b/src/org/elixir_lang/mix/project/OpenProcessor.kt
index 473d22055..ff9771710 100644
--- a/src/org/elixir_lang/mix/project/OpenProcessor.kt
+++ b/src/org/elixir_lang/mix/project/OpenProcessor.kt
@@ -1,23 +1,83 @@
package org.elixir_lang.mix.project
+import com.intellij.ide.IdeBundle
+import com.intellij.ide.impl.OpenProjectTask
+import com.intellij.ide.impl.ProjectUtil
import com.intellij.ide.util.projectWizard.WizardContext
+import com.intellij.openapi.components.StorageScheme
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.application.writeAction
+import com.intellij.openapi.project.ex.ProjectManagerEx
+import com.intellij.openapi.roots.ui.configuration.ModulesProvider
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.projectImport.ProjectImportBuilder
-import com.intellij.projectImport.ProjectOpenProcessorBase
+import com.intellij.projectImport.ProjectOpenProcessor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import org.elixir_lang.mix.project._import.Builder
+import java.nio.file.Files
/**
* Created by zyuyou on 15/7/1.
*/
-class OpenProcessor : ProjectOpenProcessorBase() {
- override val supportedExtensions = arrayOf("mix.exs")
+class OpenProcessor : ProjectOpenProcessor() {
+ private val supportedExtensions = arrayOf("mix.exs")
- override fun doGetBuilder(): Builder = ProjectImportBuilder.EXTENSIONS_POINT_NAME.findExtensionOrFail(Builder::class.java)
+ override val name: String
+ get() = "Mix"
- override fun doQuickImport(file: VirtualFile, wizardContext: WizardContext): Boolean {
- val projectRoot = file.parent
+ private val builder: Builder
+ get() = ProjectImportBuilder.EXTENSIONS_POINT_NAME.findExtensionOrFail(Builder::class.java)
+
+ override fun canOpenProject(file: VirtualFile): Boolean {
+ return if (file.isDirectory) {
+ file.children?.any { it.name in supportedExtensions } ?: false
+ } else {
+ file.name in supportedExtensions
+ }
+ }
+
+ override suspend fun openProjectAsync(
+ virtualFile: VirtualFile,
+ projectToClose: Project?,
+ forceOpenInNewFrame: Boolean
+ ): Project? {
+ val wizardContext = WizardContext(null, null)
+
+ var resolvedFile = virtualFile
+ if (virtualFile.isDirectory) {
+ resolvedFile = virtualFile.children?.firstOrNull { it.name in supportedExtensions } ?: return null
+ }
+
+ val projectRoot = resolvedFile.parent
wizardContext.projectName = projectRoot.name
+ wizardContext.setProjectFileDirectory(projectRoot.toNioPath(), false)
+
+ // Import Mix project configuration
builder.setProjectRoot(projectRoot)
- return true
+
+ val projectPath = wizardContext.projectDirectory
+ val options = OpenProjectTask {
+ this.projectToClose = projectToClose
+ this.forceOpenInNewFrame = forceOpenInNewFrame
+ this.projectName = wizardContext.projectName
+ this.isNewProject = true
+ this.beforeOpen = { project ->
+ // Configure the project - Builder now uses coroutine-aware write actions
+ builder.commit(project, null, ModulesProvider.EMPTY_MODULES_PROVIDER, null)
+ true
+ }
+ }
+
+ // Open the project without modal progress (to avoid EDT issues)
+ val project = withContext(Dispatchers.IO) {
+ ProjectManagerEx.getInstanceEx().openProjectAsync(projectPath, options)
+ }
+
+ if (project != null) {
+ ProjectUtil.updateLastProjectLocation(projectPath)
+ }
+
+ return project
}
}
diff --git a/src/org/elixir_lang/mix/project/_import/Builder.kt b/src/org/elixir_lang/mix/project/_import/Builder.kt
index 1d98c2543..fe576427b 100644
--- a/src/org/elixir_lang/mix/project/_import/Builder.kt
+++ b/src/org/elixir_lang/mix/project/_import/Builder.kt
@@ -3,14 +3,15 @@ package org.elixir_lang.mix.project._import
import com.intellij.compiler.CompilerWorkspaceConfiguration
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ex.ApplicationInfoEx
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.ModifiableModuleModel
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.options.ConfigurationException
-import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
-import com.intellij.openapi.progress.Task
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
@@ -23,6 +24,7 @@ import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl
import com.intellij.packaging.artifacts.ModifiableArtifactModel
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.projectImport.ProjectImportBuilder
import org.elixir_lang.configuration.ElixirCompilerSettings
import org.elixir_lang.mix.Icons
@@ -103,7 +105,9 @@ class Builder : ProjectImportBuilder() {
modulesProvider: ModulesProvider,
artifactModel: ModifiableArtifactModel?
): List {
- fixProjectSdk(project)
+ runBlockingCancellable {
+ fixProjectSdk(project)
+ }
val createModules = createModulesForOtpApps(
project,
mySelectedOtpApps,
@@ -158,12 +162,10 @@ class Builder : ProjectImportBuilder() {
projectRoot.refreshAndFindChild("deps")
}
- ProgressManager.getInstance()
- .run(object : Task.Modal(ProjectImportBuilder.getCurrentProject(), "Scanning Mix Projects", true) {
- override fun run(indicator: ProgressIndicator) {
- myFoundOtpApps = org.elixir_lang.mix.Project.findOtpApps(projectRoot, indicator)
- }
- })
+ // Use EmptyProgressIndicator for simple progress tracking without UI
+ // Not sure if this is right, but :shrug:.
+ val progressIndicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
+ myFoundOtpApps = org.elixir_lang.mix.Project.findOtpApps(projectRoot, progressIndicator)
mySelectedOtpApps = myFoundOtpApps
@@ -177,14 +179,14 @@ class Builder : ProjectImportBuilder() {
companion object {
private val LOG = Logger.getInstance(Builder::class.java)
- private fun fixProjectSdk(project: Project): Sdk? {
+ private suspend fun fixProjectSdk(project: Project): Sdk? {
val projectRootMgr = ProjectRootManagerEx.getInstanceEx(project)
val selectedSdk = projectRootMgr.projectSdk
val fixedProjectSdk: Sdk?
if (selectedSdk == null || selectedSdk.sdkType !== Type.instance) {
fixedProjectSdk = ProjectJdkTable.getInstance().findMostRecentSdkOfType(Type.instance)
- ApplicationManager.getApplication().runWriteAction { projectRootMgr.projectSdk = fixedProjectSdk }
+ edtWriteAction { projectRootMgr.projectSdk = fixedProjectSdk }
} else {
fixedProjectSdk = selectedSdk
}
@@ -196,8 +198,9 @@ class Builder : ProjectImportBuilder() {
private fun deleteIdeaModuleFiles(otpApps: List) {
val ex = arrayOfNulls(1)
- ApplicationManager.getApplication().runWriteAction(object : Runnable {
- override fun run() {
+ // Use runBlockingCancellable for coroutine-aware write action
+ runBlockingCancellable {
+ edtWriteAction {
for (importedOtpApp in otpApps) {
val ideaModuleFile = importedOtpApp.ideaModuleFile
if (ideaModuleFile != null) {
@@ -207,11 +210,10 @@ class Builder : ProjectImportBuilder() {
} catch (e: IOException) {
ex[0] = e
}
-
}
}
}
- })
+ }
ex[0]?.let { ioException ->
throw ioException
diff --git a/src/org/elixir_lang/mix/project/_import/step/Root.java b/src/org/elixir_lang/mix/project/_import/step/Root.java
index 7b6691ea7..8ca1da0f9 100644
--- a/src/org/elixir_lang/mix/project/_import/step/Root.java
+++ b/src/org/elixir_lang/mix/project/_import/step/Root.java
@@ -47,10 +47,10 @@ public Root(WizardContext context) {
String projectFileDirectory = context.getProjectFileDirectory();
//noinspection DialogTitleCapitalization
myProjectRootComponent.addBrowseFolderListener(
- "Select mix.exs of a mix project to import",
- "",
null,
FileChooserDescriptorFactory.createSingleFolderDescriptor()
+ .withTitle("Select mix.exs of a mix project to import")
+ .withDescription("")
);
myProjectRootComponent.setText(projectFileDirectory); // provide project path
diff --git a/src/org/elixir_lang/notification/setup_sdk/Notifier.kt b/src/org/elixir_lang/notification/setup_sdk/Notifier.kt
index 7ef25213e..d7311644d 100644
--- a/src/org/elixir_lang/notification/setup_sdk/Notifier.kt
+++ b/src/org/elixir_lang/notification/setup_sdk/Notifier.kt
@@ -4,6 +4,7 @@ import com.intellij.execution.ExecutionException
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.module.Module
+import com.intellij.openapi.project.Project
object Notifier {
fun mixSettings(module: Module, executionException: ExecutionException) {
@@ -39,4 +40,71 @@ object Notifier {
.addAction(Action(project, module))
.notify(project);
}
+
+ // SDK Refresh notification methods
+ fun sdkRefreshSuccess(
+ project: Project,
+ refreshedElixirCount: Int,
+ totalElixirCount: Int,
+ refreshedErlangCount: Int,
+ totalErlangCount: Int
+ ) {
+ val parts = mutableListOf()
+
+ if (totalElixirCount > 0) {
+ if (refreshedElixirCount == totalElixirCount) {
+ parts.add("$refreshedElixirCount Elixir SDK${if (refreshedElixirCount != 1) "s" else ""}")
+ } else {
+ parts.add("$refreshedElixirCount of $totalElixirCount Elixir SDK${if (totalElixirCount != 1) "s" else ""}")
+ }
+ }
+
+ if (totalErlangCount > 0) {
+ if (refreshedErlangCount == totalErlangCount) {
+ parts.add("$refreshedErlangCount Erlang SDK${if (refreshedErlangCount != 1) "s" else ""}")
+ } else {
+ parts.add("$refreshedErlangCount of $totalErlangCount Erlang SDK${if (totalErlangCount != 1) "s" else ""}")
+ }
+ }
+
+ val message = "Successfully refreshed " + when (parts.size) {
+ 1 -> parts[0]
+ 2 -> "${parts[0]} and ${parts[1]}"
+ else -> "SDKs"
+ } + "."
+
+ NotificationGroupManager
+ .getInstance()
+ .getNotificationGroup("Elixir")
+ .createNotification(
+ "Elixir SDK Paths Refreshed",
+ message,
+ NotificationType.INFORMATION
+ )
+ .notify(project)
+ }
+
+ fun sdkRefreshWarning(project: Project, message: String) {
+ NotificationGroupManager
+ .getInstance()
+ .getNotificationGroup("Elixir")
+ .createNotification(
+ "Elixir SDK Refresh",
+ message,
+ NotificationType.WARNING
+ )
+ .notify(project)
+ }
+
+ fun sdkRefreshError(project: Project, errorMessage: String) {
+ NotificationGroupManager
+ .getInstance()
+ .getNotificationGroup("Elixir")
+ .createNotification(
+ "Elixir SDK Refresh Failed",
+ "Failed to refresh SDK paths: $errorMessage",
+ NotificationType.ERROR
+ )
+ .notify(project)
+ }
}
diff --git a/src/org/elixir_lang/psi/ModuleWalker.kt b/src/org/elixir_lang/psi/ModuleWalker.kt
index 31c5ac556..6f38843ca 100644
--- a/src/org/elixir_lang/psi/ModuleWalker.kt
+++ b/src/org/elixir_lang/psi/ModuleWalker.kt
@@ -15,7 +15,12 @@ open class ModuleWalker(val name: String, vararg nameArityRangeWalkers: NameArit
walkerWithName(call)?.hasArity(call) ?: false
protected fun resolvesTo(call: Call, state: ResolveState) =
- resolvesToModularName(call, state, name)
+ if (state.hasBeenVisited(call)) {
+ false
+ } else {
+ val updatedState = state.putVisitedElement(call)
+ resolvesToModularName(call, updatedState, name)
+ }
open fun walkChild(call: Call, state: ResolveState, keepProcessing: (element: PsiElement, state: ResolveState) -> Boolean): Boolean =
walkerWithName(call)?.let { nameArityRangeWalkerByName ->
diff --git a/src/org/elixir_lang/sdk/elixir/Type.kt b/src/org/elixir_lang/sdk/elixir/Type.kt
index da14369f5..4df5c8915 100644
--- a/src/org/elixir_lang/sdk/elixir/Type.kt
+++ b/src/org/elixir_lang/sdk/elixir/Type.kt
@@ -2,19 +2,19 @@ package org.elixir_lang.sdk.elixir
import com.intellij.facet.FacetManager
import com.intellij.openapi.application.ApplicationManager
-import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
+import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectBundle
import com.intellij.openapi.projectRoots.*
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.OrderRootType
-import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.InvalidDataException
import com.intellij.openapi.util.SystemInfo
@@ -24,7 +24,6 @@ import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiElement
-import com.intellij.serviceContainer.AlreadyDisposedException
import com.intellij.util.system.CpuArch
import gnu.trove.THashSet
import org.apache.commons.io.FilenameUtils
@@ -257,6 +256,68 @@ ELIXIR_SDK_HOME
private const val WINDOWS_32BIT_DEFAULT_HOME_PATH = "C:\\Program Files\\Elixir"
private const val WINDOWS_64BIT_DEFAULT_HOME_PATH = "C:\\Program Files (x86)\\Elixir"
+ init {
+ setupSdkTableListener()
+ }
+
+ private fun setupSdkTableListener() {
+ val messageBus = ApplicationManager.getApplication().messageBus
+ messageBus.connect().subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, object : ProjectJdkTable.Listener {
+ override fun jdkRemoved(jdk: Sdk) {
+ // When an Erlang SDK is removed, clean up any Elixir SDKs that reference it
+ if (org.elixir_lang.sdk.erlang_dependent.Type.staticIsValidDependency(jdk)) {
+ cleanupOrphanedElixirSdkReferences(jdk)
+ }
+ }
+
+ override fun jdkNameChanged(jdk: Sdk, previousName: String) {
+ // When an Erlang SDK is renamed, update any Elixir SDKs that reference the old name
+ if (org.elixir_lang.sdk.erlang_dependent.Type.staticIsValidDependency(jdk)) {
+ updateElixirSdkReferencesAfterRename(jdk, previousName)
+ }
+ }
+ })
+ }
+
+ private fun cleanupOrphanedElixirSdkReferences(deletedErlangSdk: Sdk) {
+ val projectJdkTable = ProjectJdkTable.getInstance()
+ val elixirSdks = projectJdkTable.allJdks.filter { it.sdkType is Type }
+
+ for (elixirSdk in elixirSdks) {
+ val additionalData = elixirSdk.sdkAdditionalData as? SdkAdditionalData
+ if (additionalData != null) {
+ val currentErlangSdk = additionalData.getErlangSdk()
+ if (currentErlangSdk?.name == deletedErlangSdk.name) {
+ LOG.info("Clearing orphaned Erlang SDK reference '${deletedErlangSdk.name}' from Elixir SDK '${elixirSdk.name}'")
+ additionalData.setErlangSdk(null)
+
+ // Try to auto-assign a new Erlang SDK if available
+ val newErlangSdk = additionalData.getErlangSdk() // Will auto-discover
+ if (newErlangSdk != null) {
+ LOG.info("Auto-assigned new Erlang SDK '${newErlangSdk.name}' to Elixir SDK '${elixirSdk.name}'")
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateElixirSdkReferencesAfterRename(renamedErlangSdk: Sdk, previousName: String) {
+ LOG.debug("Updating Elixir SDK references from '$previousName' to '${renamedErlangSdk.name}'")
+ val projectJdkTable = ProjectJdkTable.getInstance()
+ val elixirSdks = projectJdkTable.allJdks.filter { it.sdkType is Type }
+
+ for (elixirSdk in elixirSdks) {
+ val additionalData = elixirSdk.sdkAdditionalData as? SdkAdditionalData
+ if (additionalData != null) {
+ val currentErlangSdk = additionalData.getErlangSdk()
+ if (currentErlangSdk?.name == previousName) {
+ LOG.info("Updating Erlang SDK reference from '${previousName}' to '${renamedErlangSdk.name}' in Elixir SDK '${elixirSdk.name}'")
+ additionalData.setErlangSdk(renamedErlangSdk)
+ }
+ }
+ }
+ }
+
private fun releaseVersion(sdkModificator: SdkModificator): String? =
sdkModificator.versionString?.let { Release.fromString(it) }?.version()
@@ -326,12 +387,23 @@ ELIXIR_SDK_HOME
private fun configureSdkPaths(sdk: Sdk) {
val sdkModificator = sdk.sdkModificator
- org.elixir_lang.sdk.Type
- .addCodePaths(sdkModificator)
+
+ // Configure base paths
+ org.elixir_lang.sdk.Type.addCodePaths(sdkModificator)
addDocumentationPaths(sdkModificator)
addSourcePaths(sdkModificator)
+
+ // Configure internal Erlang SDK - this will now create and fully setup the Erlang SDK synchronously
configureInternalErlangSdk(sdk, sdkModificator)
- ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() }
+
+ // Use coroutine-based approach for final commit for IntelliJ 2025.2+ compatibility
+ runBlockingCancellable {
+ edtWriteAction {
+ LOG.debug("Committing SDK changes for ${sdk.name}")
+ sdkModificator.commitChanges()
+ LOG.debug("Committed SDK changes for ${sdk.name}")
+ }
+ }
}
private fun configureInternalErlangSdk(
@@ -471,16 +543,23 @@ ELIXIR_SDK_HOME
): Sdk? {
val sdkName = erlangSdkType.suggestSdkName("Default " + erlangSdkType.name, homePath)
val projectJdkImpl = ProjectJdkImpl(sdkName, erlangSdkType)
- projectJdkImpl.homePath = homePath
- erlangSdkType.setupSdkPaths(projectJdkImpl)
+ var modificator = projectJdkImpl.sdkModificator
+ modificator.homePath = homePath
return if (projectJdkImpl.versionString != null) {
- ApplicationManager.getApplication().invokeAndWait(
- {
- ApplicationManager.getApplication().runWriteAction { projectJdkTable.addJdk(projectJdkImpl) }
- },
- ModalityState.NON_MODAL,
- )
+ // First commit the basic SDK setup
+ modificator.commitChanges()
+
+ // Add to SDK table and setup paths using coroutine-based approach
+ runBlockingCancellable {
+ edtWriteAction {
+ projectJdkTable.addJdk(projectJdkImpl)
+
+ // Setup SDK paths - this will work properly within the write action
+ erlangSdkType.setupSdkPaths(projectJdkImpl)
+ }
+ }
+
projectJdkImpl
} else {
null
@@ -565,18 +644,14 @@ ELIXIR_SDK_HOME
val project = psiElement.project
return if (!project.isDisposed) {
- if (ProjectFileIndex.SERVICE.getInstance(project) != null) {
+ run {
// Use a background thread to perform the ReadAction
val module =
ApplicationManager
.getApplication()
.executeOnPooledThread {
- try {
- ReadAction.compute {
- ModuleUtilCore.findModuleForPsiElement(psiElement)
- }
- } catch (_: AlreadyDisposedException) {
- null
+ ReadAction.compute {
+ ModuleUtilCore.findModuleForPsiElement(psiElement)
}
}.get() // Wait for the result
@@ -585,8 +660,6 @@ ELIXIR_SDK_HOME
} else {
mostSpecificSdk(project)
}
- } else {
- mostSpecificSdk(project)
}
} else {
null
diff --git a/src/org/elixir_lang/sdk/erlang/Type.kt b/src/org/elixir_lang/sdk/erlang/Type.kt
index f056ee33e..a0443ee7a 100644
--- a/src/org/elixir_lang/sdk/erlang/Type.kt
+++ b/src/org/elixir_lang/sdk/erlang/Type.kt
@@ -2,7 +2,10 @@ package org.elixir_lang.sdk.erlang
import com.intellij.execution.ExecutionException
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.edtWriteAction
+import com.intellij.openapi.application.writeAction
import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkModel
import com.intellij.openapi.projectRoots.SdkModificator
@@ -117,7 +120,21 @@ class Type : SdkType("Erlang SDK for Elixir SDK") {
val sdkModificator = sdk.sdkModificator
org.elixir_lang.sdk.Type
.addCodePaths(sdkModificator)
- ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() }
+
+ // Check if we're already in a write action (called from Elixir SDK setup)
+ if (ApplicationManager.getApplication().isWriteAccessAllowed) {
+ // We're already in a write action, commit directly
+ sdkModificator.commitChanges()
+ } else {
+ // Use coroutine-based approach for IntelliJ 2025.2+ compatibility when called independently
+ runBlockingCancellable {
+ edtWriteAction {
+ LOGGER.debug("Committing SDK changes for ${sdk.name}")
+ sdkModificator.commitChanges()
+ LOGGER.debug("Committed SDK changes for ${sdk.name}")
+ }
+ }
+ }
}
override fun suggestHomePath(): String? = suggestHomePaths().firstOrNull()
diff --git a/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt b/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt
index 4262bf903..8064ad0f5 100644
--- a/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt
+++ b/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt
@@ -188,7 +188,9 @@ class AdditionalDataConfigurable(
elixirSdk!!,
)
sdkModificator.sdkAdditionalData = sdkAdditionData
- ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() }
+ ApplicationManager.getApplication().runWriteAction {
+ sdkModificator.commitChanges()
+ }
this.sdkModificator.commitChanges()
}
diff --git a/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt b/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt
index 90b4354ff..ba1bc6b39 100644
--- a/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt
+++ b/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt
@@ -36,8 +36,20 @@ class SdkAdditionalData :
*/
@Throws(ConfigurationException::class)
override fun checkValid(sdkModel: SdkModel) {
- if (getErlangSdk() == null) {
- throw ConfigurationException("Please configure the Erlang ERLANG_SDK_NAME")
+ val erlangSdk = getErlangSdk()
+ if (erlangSdk == null) {
+ val availableErlangSdks = ProjectJdkTable.getInstance().allJdks.filter {
+ Type.staticIsValidDependency(it)
+ }
+
+ val message = if (availableErlangSdks.isEmpty()) {
+ "No Erlang SDK found. Please configure an Erlang SDK first, then configure this Elixir SDK."
+ } else {
+ val availableNames = availableErlangSdks.joinToString(", ") { it.name }
+ "No valid Erlang SDK configured for this Elixir SDK. Available Erlang SDKs: $availableNames"
+ }
+
+ throw ConfigurationException(message)
}
}
@@ -54,7 +66,13 @@ class SdkAdditionalData :
val sdk = getErlangSdk()
if (sdk != null) {
- element.setAttribute(ERLANG_SDK_NAME, sdk.name)
+ // Double-check that the SDK still exists in the table before writing
+ val jdkTable = ProjectJdkTable.getInstance()
+ if (jdkTable.findJdk(sdk.name) != null) {
+ element.setAttribute(ERLANG_SDK_NAME, sdk.name)
+ }
+ // If SDK doesn't exist in table, don't write the attribute
+ // This prevents persisting references to deleted SDKs
}
}
@@ -64,8 +82,15 @@ class SdkAdditionalData :
if (erlangSdk == null) {
if (erlangSdkName != null) {
erlangSdk = jdkTable.findJdk(erlangSdkName!!)
- erlangSdkName = null
+ if (erlangSdk == null) {
+ // Clear orphaned reference - SDK was deleted
+ erlangSdkName = null
+ } else {
+ // Found the SDK, clear the name since we have the reference
+ erlangSdkName = null
+ }
} else {
+ // Auto-discover if no specific SDK was configured
for (jdk in jdkTable.allJdks) {
if (Type.staticIsValidDependency(jdk)) {
erlangSdk = jdk
@@ -73,6 +98,12 @@ class SdkAdditionalData :
}
}
}
+ } else {
+ // Validate that the cached SDK still exists in the table
+ if (jdkTable.findJdk(erlangSdk!!.name) == null) {
+ // SDK was deleted, clear the reference
+ erlangSdk = null
+ }
}
return erlangSdk
diff --git a/src/org/elixir_lang/settings/ElixirExperimentalSettings.kt b/src/org/elixir_lang/settings/ElixirExperimentalSettings.kt
new file mode 100644
index 000000000..ba708c826
--- /dev/null
+++ b/src/org/elixir_lang/settings/ElixirExperimentalSettings.kt
@@ -0,0 +1,52 @@
+package org.elixir_lang.settings
+
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.State
+import com.intellij.openapi.components.Storage
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.util.messages.Topic
+
+interface ElixirExperimentalSettingsListener {
+ fun settingsChanged(oldState: ElixirExperimentalSettings.State, newState: ElixirExperimentalSettings.State)
+}
+
+private val LOG = logger()
+
+@State(
+ name = "org.elixir_lang.settings.ElixirExperimentalSettings",
+ storages = [Storage("elixir.experimental.xml")]
+)
+@Service
+class ElixirExperimentalSettings : PersistentStateComponent {
+ data class State(
+ var enableHtmlInjection: Boolean = false,
+// var enableDeleteSdkSmallIDE: Boolean = false,
+ var enableStatusBarWidget : Boolean = false,
+ var enableLiteralSigilInjection: Boolean = false
+ )
+
+ private var elixirSettingsState = State()
+
+ override fun getState(): State = elixirSettingsState
+
+ override fun loadState(state: State) {
+ LOG.debug("Loading ElixirExperimentalSettings state: $state")
+ elixirSettingsState = state
+ }
+
+ companion object {
+ @JvmField
+ val SETTINGS_CHANGED_TOPIC: Topic = Topic.create(
+ "ElixirExperimentalSettings.settingsChanged",
+ ElixirExperimentalSettingsListener::class.java
+ )
+
+ val instance: ElixirExperimentalSettings
+ get() = com.intellij.openapi.application.ApplicationManager
+ .getApplication()
+ .getService(ElixirExperimentalSettings::class.java)
+ }
+}
+
+
diff --git a/src/org/elixir_lang/settings/ElixirExperimentalSettingsConfigurable.kt b/src/org/elixir_lang/settings/ElixirExperimentalSettingsConfigurable.kt
new file mode 100644
index 000000000..2b3fa50c4
--- /dev/null
+++ b/src/org/elixir_lang/settings/ElixirExperimentalSettingsConfigurable.kt
@@ -0,0 +1,109 @@
+package org.elixir_lang.settings
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.options.Configurable
+import com.intellij.openapi.ui.DialogPanel
+import com.intellij.ui.dsl.builder.bindSelected
+import com.intellij.ui.dsl.builder.panel
+import javax.swing.JComponent
+
+private val LOG = logger()
+
+/**
+ * Configurable for Elixir experimental features.
+ * Currently supports enabling HTML language injection for ~H sigils used in Phoenix LiveView templates.
+ */
+internal class ElixirExperimentalSettingsConfigurable : Configurable, Configurable.Beta {
+ private var settingsPanel: DialogPanel? = null
+ private val settings = ElixirExperimentalSettings.instance
+
+ override fun createComponent(): JComponent {
+ settingsPanel = panel {
+ group("HTML Injection in ~H Sigils code blocks") {
+ row {
+ checkBox("Enable ~H Sigil HTML language injection")
+ .comment("Provides HTML syntax highlighting and code completion within ~H Sigils code blocks, when working with Phoenix Live View HEEx templates, otherwise you'll see this as a string.
Documentation for ~H Sigil HTML Injection Support.")
+ .bindSelected(
+ getter = { settings.state.enableHtmlInjection },
+ setter = { settings.state.enableHtmlInjection = it }
+ )
+ }
+ }
+ group("Literal Sigil Injection") {
+ row {
+ checkBox("Enable literal sigil injection (~S, ~W, etc.)")
+ .comment("Enables literal sigil injection, which allows the Elixir sigil ~S to work. WARNING: THIS IS CURRENTLY BUGGY AND CAUSING ISSUES WITH ~s")
+ .bindSelected(
+ getter = { settings.state.enableLiteralSigilInjection },
+ setter = { settings.state.enableLiteralSigilInjection = it }
+ )
+ }
+ }
+ group("Status Bar Widget") {
+ row {
+ // @todo check enableDeleteSdkSmallIDE
+ checkBox("Enable Status Bar Widget showing if the Elixir SDK is correctly configured") // TODO: localize
+ .comment("Adds a widget to the status bar that indicates whether the Elixir SDK is properly configured for the current project.")
+ .bindSelected(
+ getter = { settings.state.enableStatusBarWidget },
+ setter = { settings.state.enableStatusBarWidget = it }
+ )
+ }
+ }
+// group("Custom IDE Settings") {
+// row {
+// // @todo check enableDeleteSdkSmallIDE
+// checkBox("Enable Delete SDK action in Custom (small) IDEs (e.g. PyCharm, RubyMine)") // TODO: localize
+// .comment("Adds a 'Delete SDK' action to the SDK selection settings window.")
+// .bindSelected(
+// getter = { settings.state.enableDeleteSdkSmallIDE },
+// setter = { settings.state.enableDeleteSdkSmallIDE = it }
+// )
+// }
+// }
+ //
+ }
+ return settingsPanel!!
+ }
+
+ override fun isModified(): Boolean {
+ return settingsPanel?.isModified() ?: false
+ }
+
+ override fun apply() {
+ // Check if settings have been modified BEFORE we apply changes
+ if (!isModified()) {
+ LOG.debug("No modifications detected, skipping apply")
+ return
+ }
+
+ // Store the old state before applying changes
+ val oldState = settings.state.copy()
+ LOG.debug("ElixirExperimentalSettingsConfigurable.apply() - oldState: $oldState")
+
+ // Apply the changes from the panel (this modifies settings.state directly)
+ settingsPanel?.apply()
+
+ // Get the new state after applying
+ val newState = settings.state.copy()
+ LOG.debug("ElixirExperimentalSettingsConfigurable.apply() - newState: $newState")
+
+ // Force the change notification by calling updateState with oldState and newState
+ LOG.debug("Settings were modified, calling updateState(). StatusBarWidget: ${oldState.enableStatusBarWidget} -> ${newState.enableStatusBarWidget}")
+
+ // We need to manually trigger the change notification since the state was modified in-place
+ val messageBus = com.intellij.openapi.application.ApplicationManager.getApplication().messageBus
+ messageBus.syncPublisher(ElixirExperimentalSettings.SETTINGS_CHANGED_TOPIC).settingsChanged(oldState, newState)
+ }
+
+ override fun getDisplayName(): String = "Elixir Experimental Settings"
+
+ override fun reset() {
+ settingsPanel?.reset()
+ }
+
+ override fun disposeUIResources() {
+ settingsPanel = null
+ }
+}
diff --git a/src/org/elixir_lang/spell_checking/literal/Splitter.kt b/src/org/elixir_lang/spell_checking/literal/Splitter.kt
index 5c9f4389b..7c0b52fa5 100644
--- a/src/org/elixir_lang/spell_checking/literal/Splitter.kt
+++ b/src/org/elixir_lang/spell_checking/literal/Splitter.kt
@@ -6,7 +6,7 @@ import com.intellij.spellchecker.inspections.PlainTextSplitter
import com.intellij.util.Consumer
object Splitter : BaseSplitter() {
- override fun split(text: String?, range: TextRange, consumer: Consumer?) {
+ override fun split(text: String?, range: TextRange, consumer: Consumer) {
if (text != null && 1 <= range.length && 0 <= range.startOffset) {
val nonescapedTextRangeList = excludeByPattern(text, range, ESCAPE_SEQUENCE_PATTERN, 0)
diff --git a/src/org/elixir_lang/status_bar_widget/ElixirSdkStatusWidget.kt b/src/org/elixir_lang/status_bar_widget/ElixirSdkStatusWidget.kt
new file mode 100644
index 000000000..2940929bb
--- /dev/null
+++ b/src/org/elixir_lang/status_bar_widget/ElixirSdkStatusWidget.kt
@@ -0,0 +1,244 @@
+package org.elixir_lang.status_bar_widget
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.ProjectJdkTable
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.ui.popup.JBPopup
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.wm.StatusBar
+import com.intellij.openapi.wm.StatusBarWidget
+import com.intellij.util.messages.MessageBusConnection
+import org.elixir_lang.Icons
+import org.elixir_lang.jps.HomePath
+import org.elixir_lang.sdk.elixir.Type
+import org.elixir_lang.sdk.erlang_dependent.SdkAdditionalData
+import org.jetbrains.annotations.NotNull
+
+private val LOG = logger()
+
+class ElixirSdkStatusWidget(@param:NotNull private val project: Project) : StatusBarWidget,
+ StatusBarWidget.MultipleTextValuesPresentation {
+
+ companion object {
+ const val ID = "ElixirSdkStatus"
+ }
+
+ private var statusBar: StatusBar? = null
+ private var messageBusConnection: MessageBusConnection? = null
+
+ // Cached widget presentation data - computed once per update
+ @Volatile
+ private var cachedPresentation: WidgetPresentation? = null
+
+ private data class WidgetPresentation(
+ val text: String,
+ val icon: javax.swing.Icon?,
+ val tooltip: String
+ )
+
+ private sealed interface SdkStatus {
+ data class Configured(
+ val elixirSdk: Sdk,
+ val erlangSdk: Sdk,
+ val elixirVersion: String
+ ) : SdkStatus
+
+ data class Warning(
+ val elixirSdk: Sdk,
+ val erlangSdk: Sdk,
+ val elixirVersion: String,
+ val issues: List
+ ) : SdkStatus
+
+ data class Partial(
+ val elixirSdk: Sdk?,
+ val elixirVersion: String?,
+ val issue: String
+ ) : SdkStatus
+
+ data object NotConfigured : SdkStatus
+ }
+
+ init {
+ updateWidget()
+ }
+
+ override fun ID(): String = ID
+
+ override fun getPresentation(): StatusBarWidget.WidgetPresentation = this
+
+ override fun install(statusBar: StatusBar) {
+ this.statusBar = statusBar
+ setupListeners()
+ updateWidget()
+ }
+
+ override fun dispose() {
+ messageBusConnection?.disconnect()
+ statusBar = null
+ cachedPresentation = null
+ Disposer.dispose(this)
+ LOG.debug("Disposed ElixirSdkStatusWidget")
+ }
+
+ // MultipleTextValuesPresentation implementation
+ override fun getSelectedValue(): String = getCurrentPresentation().text
+
+ override fun getIcon(): javax.swing.Icon? = getCurrentPresentation().icon
+
+ override fun getPopup(): JBPopup? = null
+
+ override fun getTooltipText(): String = getCurrentPresentation().tooltip
+
+ private fun updateWidget() {
+ // Invalidate cached presentation when updating
+ cachedPresentation = null
+ statusBar?.updateWidget(ID)
+ }
+
+ private fun getCurrentPresentation(): WidgetPresentation {
+ return cachedPresentation ?: run {
+ val sdkStatus = detectSdkStatus()
+ val presentation = createPresentation(sdkStatus)
+ cachedPresentation = presentation
+ presentation
+ }
+ }
+
+ private fun createPresentation(status: SdkStatus): WidgetPresentation {
+ return when (status) {
+ is SdkStatus.Configured -> WidgetPresentation(
+ text = "Elixir: ${status.elixirVersion}",
+ icon = Icons.LANGUAGE,
+ tooltip = "Elixir SDK: ${status.elixirVersion} (Configured correctly)"
+ )
+
+ is SdkStatus.Warning -> WidgetPresentation(
+ text = "Elixir: ${status.elixirVersion} ⚠",
+ icon = Icons.LANGUAGE,
+ tooltip = "Elixir SDK: ${status.elixirVersion} (Warning: ${status.issues.joinToString(", ")})"
+ )
+
+ is SdkStatus.Partial -> WidgetPresentation(
+ text = "Elixir: Issues",
+ icon = AllIcons.General.Warning,
+ tooltip = "Elixir SDK: ${status.elixirVersion ?: "Unknown"} (${status.issue})"
+ )
+
+ SdkStatus.NotConfigured -> {
+ val availableElixirSdks = ProjectJdkTable.getInstance().allJdks.filter { it.sdkType is Type }
+ val tooltip = if (availableElixirSdks.isNotEmpty()) {
+ "No Elixir SDK configured for this project (${availableElixirSdks.size} Elixir SDK(s) available)"
+ } else {
+ "No Elixir SDKs configured"
+ }
+ WidgetPresentation(
+ text = "No Elixir SDK",
+ icon = null,
+ tooltip = tooltip
+ )
+ }
+ }
+ }
+
+
+ private fun setupListeners() {
+ messageBusConnection = project.messageBus.connect(this)
+
+ // Listen for project JDK table changes (SDK added/removed/modified)
+ messageBusConnection?.subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, object : ProjectJdkTable.Listener {
+ override fun jdkAdded(jdk: Sdk) {
+ if (jdk.sdkType is Type) {
+ updateWidget()
+ }
+ }
+
+ override fun jdkRemoved(jdk: Sdk) {
+ if (jdk.sdkType is Type) {
+ updateWidget()
+ }
+ }
+
+ override fun jdkNameChanged(jdk: Sdk, previousName: String) {
+ if (jdk.sdkType is Type) {
+ updateWidget()
+ }
+ }
+ })
+
+ // Listen for project root changes (project SDK changes)
+ messageBusConnection?.subscribe(
+ com.intellij.openapi.roots.ModuleRootListener.TOPIC,
+ object : com.intellij.openapi.roots.ModuleRootListener {
+ override fun rootsChanged(event: com.intellij.openapi.roots.ModuleRootEvent) {
+ updateWidget()
+ }
+ })
+ }
+
+ private fun detectSdkStatus(): SdkStatus {
+ val elixirSdk = Type.mostSpecificSdk(project) ?: return SdkStatus.NotConfigured
+ val elixirVersion = elixirSdk.versionString ?: "Unknown"
+
+ if (!isValidSdk(elixirSdk)) {
+ return SdkStatus.Partial(elixirSdk, elixirVersion, "Invalid Elixir SDK")
+ }
+
+ val erlangSdk = getErlangSdk(elixirSdk)
+ ?: return SdkStatus.Partial(elixirSdk, elixirVersion, "Missing Erlang SDK")
+
+ val issues = mutableListOf()
+ if (hasEbinPathIssues(elixirSdk)) {
+ issues.add("Missing Elixir ebin paths or classpath entries")
+ }
+ if (hasErlangSdkIssues(erlangSdk)) {
+ issues.add("Erlang SDK configuration issues")
+ }
+
+ return if (issues.isEmpty()) {
+ SdkStatus.Configured(elixirSdk, erlangSdk, elixirVersion)
+ } else {
+ SdkStatus.Warning(elixirSdk, erlangSdk, elixirVersion, issues)
+ }
+ }
+
+ private fun isValidSdk(sdk: Sdk?): Boolean {
+ if (sdk == null) return false
+ val sdkType = sdk.sdkType as? Type ?: return false
+ val homePath = sdk.homePath ?: return false
+ return sdkType.isValidSdkHome(homePath)
+ }
+
+ private fun getErlangSdk(elixirSdk: Sdk): Sdk? {
+ val additionalData = elixirSdk.sdkAdditionalData as? SdkAdditionalData
+ return additionalData?.getErlangSdk()
+ }
+
+ private fun hasEbinPathIssues(elixirSdk: Sdk): Boolean {
+ val homePath = elixirSdk.homePath ?: return true
+
+ // Check if Elixir SDK has proper ebin paths in lib directory
+ if (!HomePath.hasEbinPath(homePath)) {
+ return true
+ }
+
+ // Check if SDK has proper classpath roots configured
+ val classRoots = elixirSdk.rootProvider.getFiles(com.intellij.openapi.roots.OrderRootType.CLASSES)
+ return classRoots.isEmpty()
+ }
+
+ private fun hasErlangSdkIssues(erlangSdk: Sdk): Boolean {
+ val homePath = erlangSdk.homePath ?: return true
+
+ // Check if Erlang SDK has proper ebin paths
+ if (!HomePath.hasEbinPath(homePath)) {
+ return true
+ }
+
+ // Validate that this is a proper Erlang SDK type
+ return !org.elixir_lang.sdk.erlang_dependent.Type.staticIsValidDependency(erlangSdk)
+ }
+
+}
diff --git a/src/org/elixir_lang/status_bar_widget/ElixirSdkStatusWidgetFactory.kt b/src/org/elixir_lang/status_bar_widget/ElixirSdkStatusWidgetFactory.kt
new file mode 100644
index 000000000..737d15d30
--- /dev/null
+++ b/src/org/elixir_lang/status_bar_widget/ElixirSdkStatusWidgetFactory.kt
@@ -0,0 +1,107 @@
+package org.elixir_lang.status_bar_widget
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.ProjectManager
+import com.intellij.openapi.wm.StatusBar
+import com.intellij.openapi.wm.StatusBarWidget
+import com.intellij.openapi.wm.StatusBarWidgetFactory
+import com.intellij.openapi.wm.WindowManager
+import com.intellij.util.messages.MessageBusConnection
+import org.elixir_lang.settings.ElixirExperimentalSettings
+import org.elixir_lang.settings.ElixirExperimentalSettingsListener
+
+private val LOG = logger()
+
+class ElixirSdkStatusWidgetFactory : StatusBarWidgetFactory {
+ private var messageBusConnection: MessageBusConnection? = null
+
+ init {
+ setupSettingsListener()
+ }
+
+ override fun getId(): String = ElixirSdkStatusWidget.ID
+
+ override fun getDisplayName(): String = "Elixir SDK Status"
+
+ override fun isAvailable(project: Project): Boolean {
+ // Widget is available when the experimental setting is enabled
+ val isAvailable = ElixirExperimentalSettings.instance.state.enableStatusBarWidget
+ LOG.debug("isAvailable called for project ${project.name}, returning $isAvailable")
+ return isAvailable
+ }
+
+ override fun canBeEnabledOn(statusBar: StatusBar): Boolean {
+ // Widget can be enabled when the experimental setting is enabled
+ return ElixirExperimentalSettings.instance.state.enableStatusBarWidget
+ }
+
+ override fun createWidget(project: Project): StatusBarWidget {
+ LOG.debug("Creating widget for project: ${project.name}")
+ return ElixirSdkStatusWidget(project)
+ }
+
+ // Mark this widget as configurable so users can enable/disable it
+ override fun isConfigurable(): Boolean = true
+
+ // isEnabledByDefault checks the experimental settings for each project
+ override fun isEnabledByDefault(): Boolean {
+ // Since this is called without a project context, return false
+ // The actual availability is determined by isAvailable(project)
+ return false
+ }
+
+ private fun setupSettingsListener() {
+ val messageBus = ApplicationManager.getApplication().messageBus
+ messageBusConnection = messageBus.connect()
+ messageBusConnection?.subscribe(
+ ElixirExperimentalSettings.SETTINGS_CHANGED_TOPIC,
+ object : ElixirExperimentalSettingsListener {
+ override fun settingsChanged(oldState: ElixirExperimentalSettings.State, newState: ElixirExperimentalSettings.State) {
+ LOG.debug("Settings changed: statusBarWidget ${oldState.enableStatusBarWidget} -> ${newState.enableStatusBarWidget}")
+ if (oldState.enableStatusBarWidget != newState.enableStatusBarWidget) {
+ LOG.debug("Status bar widget setting changed, updating widget visibility")
+ updateWidgetVisibility(newState.enableStatusBarWidget)
+ }
+ }
+ }
+ )
+ }
+
+ private fun updateWidgetVisibility(shouldShow: Boolean) {
+ ApplicationManager.getApplication().invokeLater {
+ for (project in ProjectManager.getInstance().openProjects) {
+ if (!project.isDisposed) {
+ updateProjectWidget(project, shouldShow)
+ }
+ }
+ }
+ }
+
+ private fun updateProjectWidget(project: Project, shouldShow: Boolean) {
+ val statusBar = WindowManager.getInstance().getStatusBar(project)
+ if (statusBar != null) {
+ val widget = statusBar.getWidget(ElixirSdkStatusWidget.ID) as ElixirSdkStatusWidget?
+
+ LOG.debug("Project ${project.name}: widget exists=${widget != null}, shouldShow=$shouldShow")
+
+ when {
+ shouldShow && widget == null -> {
+ LOG.debug("Adding widget for project: ${project.name}")
+ val newWidget = createWidget(project)
+ statusBar.addWidget(newWidget, "before Position")
+ }
+
+ !shouldShow && widget != null -> {
+ LOG.debug("Removing widget for project: ${project.name}")
+ statusBar.removeWidget(ElixirSdkStatusWidget.ID)
+ }
+
+ else -> {
+ LOG.debug("Widget state already correct for project: ${project.name}")
+ }
+ }
+ }
+ }
+}
diff --git a/testData/org/elixir_lang/injection/html_injection.ex b/testData/org/elixir_lang/injection/html_injection.ex
new file mode 100644
index 000000000..6c8c7ad0a
--- /dev/null
+++ b/testData/org/elixir_lang/injection/html_injection.ex
@@ -0,0 +1,10 @@
+defmodule Foo do
+ def divs() do
+ ~H"""
+
+ """
+ end
+end
diff --git a/tests/org/elixir_lang/injection/HSigilHTMLInjectionTest.kt b/tests/org/elixir_lang/injection/HSigilHTMLInjectionTest.kt
new file mode 100644
index 000000000..650bfb728
--- /dev/null
+++ b/tests/org/elixir_lang/injection/HSigilHTMLInjectionTest.kt
@@ -0,0 +1,26 @@
+package org.elixir_lang.injection
+
+import com.intellij.lang.injection.InjectedLanguageManager
+import com.intellij.psi.PsiElement
+import com.intellij.psi.impl.source.tree.injected.InjectedTestUtil
+import com.intellij.testFramework.fixtures.BasePlatformTestCase
+
+class HSigilHTMLInjectionTest : BasePlatformTestCase() {
+
+ fun testDoBlockDiagnostic() {
+ myFixture.configureByFile("html_injection.ex")
+// InjectedTestUtil.registerMockInjectedLanguageManager(getApplication(), project, getPluginDescriptor())
+
+ // Step 1: Check if the file is loaded correctly
+ assertNotNull("File should be loaded", myFixture.file)
+
+ // test injection
+ val context: PsiElement = InjectedLanguageManager.getInstance(project).getTopLevelFile(myFixture.file)
+
+ context.text
+ }
+
+ override fun getTestDataPath(): String {
+ return "testData/org/elixir_lang/injection"
+ }
+}