diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore
new file mode 100644
index 000000000..d20c0fe4b
--- /dev/null
+++ b/.codegraph/.gitignore
@@ -0,0 +1,5 @@
+# CodeGraph data files — local to each machine, not for committing.
+# Ignore everything in .codegraph/ except this file itself, so transient
+# files (the database, daemon.pid, sockets, logs) never show up in git.
+*
+!.gitignore
diff --git a/.idea/inspectionProfiles/idea_default.xml b/.idea/inspectionProfiles/idea_default.xml
index 6b1a6c6de..94cc27b33 100644
--- a/.idea/inspectionProfiles/idea_default.xml
+++ b/.idea/inspectionProfiles/idea_default.xml
@@ -664,9 +664,6 @@
-
-
-
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..840737d4a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,152 @@
+# AGENTS.md
+
+This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is the **Hyperskill Academy** IntelliJ Platform plugin (formerly JetBrains Academy plugin). It enables educational features in
+JetBrains IDEs for learning programming and creating interactive courses. The plugin integrates
+with [Hyperskill](https://hi.hyperskill.org/how-we-teach).
+
+## Build Commands
+
+```bash
+# Build the entire plugin
+./gradlew buildPlugin
+
+# Run tests for a specific module
+# Note: hs-core tests must run via :intellij-plugin:test (hs-core uses base plugin without test runtime)
+./gradlew :intellij-plugin:test
+./gradlew :intellij-plugin:hs-Java:test
+
+# Run a single test class
+./gradlew :intellij-plugin:test --tests "org.hyperskill.academy.learning.format.CourseFormatTest"
+
+# Run a single test method
+./gradlew :intellij-plugin:test --tests "org.hyperskill.academy.learning.format.CourseFormatTest.testSomething"
+
+# Run IDE with the plugin (various IDEs supported)
+./gradlew runIde # Default IDE based on baseIDE property
+./gradlew runIdea # IntelliJ IDEA Ultimate
+./gradlew runPyCharm # PyCharm
+./gradlew runCLion # CLion (classic engine)
+./gradlew runCLion-Nova # CLion (Nova engine)
+./gradlew runGoLand
+./gradlew runWebStorm
+./gradlew runRustRover
+
+# Verify plugin compatibility
+./gradlew verifyPlugin
+
+# Run in split mode (backend/frontend separation)
+./gradlew runInSplitMode
+```
+
+## Platform Version Targeting
+
+The plugin supports multiple IntelliJ Platform versions. The target version is controlled by `environmentName` in `gradle.properties`:
+
+- `environmentName=252` targets 2025.2.x, `environmentName=253` targets 2025.3.x
+- Version-specific properties are in `gradle-252.properties`, `gradle-253.properties`, etc.
+- Branch-specific source overrides go in `branches//src/` directories
+
+To switch versions, change `environmentName` and reload Gradle.
+
+## Architecture
+
+### Module Structure
+
+- **`hs-edu-format`**: Standalone library containing course format data models (Course, Lesson, Task, etc.), YAML/JSON serialization, and
+ API definitions. Has no IntelliJ dependencies.
+
+- **`intellij-plugin/hs-core`**: Core plugin functionality - course management, task checking, UI components, Hyperskill integration
+
+- **`intellij-plugin/hs-jvm-core`**: Shared JVM language support (Gradle, JUnit integration)
+
+- **Language modules** (`hs-Java`, `hs-Kotlin`, `hs-Python`, `hs-Rust`, `hs-Cpp`, `hs-Go`, `hs-Php`, `hs-JavaScript`, `hs-Scala`,
+ `hs-Shell`, `hs-sql`, `hs-CSharp`): Language-specific configurators and checkers
+
+- **Feature modules** (`hs-features/*`): Optional features like AI debugger, GitHub integration, remote environments
+
+### Source Layout
+
+```
+src/ # Main source code
+resources/ # Resources
+testSrc/ # Test source code
+testResources/ # Test resources
+branches/252/ # Platform-specific overrides for 2025.2
+```
+
+### Key Patterns
+
+- All plugin module classes must be in the `org.hyperskill.academy` package (enforced by `verifyClasses` task)
+- Kotlin stdlib is excluded from dependencies (bundled with IDE) — see `kotlin.stdlib.default.dependency=false`
+- Tests use IntelliJ's test framework (`LightPlatformCodeInsightTestCase`, etc.)
+- Convention plugins in `buildSrc/` provide shared Gradle configuration:
+ - `common-conventions` — base Java/Kotlin setup (Java 21, Kotlin 2.1)
+ - `intellij-plugin-common-conventions` — IntelliJ plugin modules with branch-specific sources
+ - `intellij-plugin-module-conventions` — full IntelliJ Platform module setup with IDE caching
+
+### IntelliJ Platform Plugin
+
+Uses [IntelliJ Platform Gradle Plugin](https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html) v2.x. Key
+extension points:
+
+- `intellijPlatform.pluginConfiguration` for plugin metadata
+- `intellijPlatform.pluginVerification` for compatibility checks
+- `intellijPlatform.caching { ides { enabled = true } }` for IDE download caching
+
+## Handling Platform Compatibility
+
+When code differs between platform versions:
+
+1. **Prefer deprecated APIs** if they work across all supported versions. Add `// BACKCOMPAT: ` comment to mark
+ for future cleanup when that version is dropped.
+
+2. **Extract platform-specific code** to `branches//src/` directories when APIs are incompatible. Keep
+ platform-specific code minimal.
+
+3. **Use runtime checks** with `ApplicationInfo.getInstance().build` when code must compile on all platforms but behave
+ differently.
+
+4. **Platform-specific XML**: Create `platform-.xml` in `branches//resources/META-INF/` and include via
+ XInclude.
+
+## Configuration Files
+
+- `gradle.properties`: Main build configuration (plugin version, target IDE, feature flags)
+- `gradle-252.properties`, `gradle-253.properties`: Platform-specific plugin/IDE versions
+- `secret.properties`: OAuth client IDs (not committed, created from template)
+- `gradle/libs.versions.toml`: Dependency version catalog
+
+## Code Style
+
+- Only English is allowed in the code (comments, identifiers, strings)
+
+## Debugging
+
+When running the plugin with `./gradlew runIde` (or `runIdea`, `runPyCharm`, etc.), logs are located in the **sandbox directory**, not in the system logs:
+
+```
+intellij-plugin/build/idea-sandbox/idea-sandbox-/log_runIdea/idea.log
+```
+
+For example:
+```
+intellij-plugin/build/idea-sandbox/idea-sandbox-253/log_runIdea/idea.log
+```
+
+To view logs in real-time:
+```bash
+tail -f intellij-plugin/build/idea-sandbox/idea-sandbox-*/log_runIdea/idea.log
+```
+
+To search for specific log entries:
+```bash
+grep "YourSearchPattern" intellij-plugin/build/idea-sandbox/idea-sandbox-*/log_runIdea/idea.log
+```
+
+## Issue Tracker
+
+Report issues to [YouTrack EDU project](https://youtrack.jetbrains.com/issues/EDU).
diff --git a/build.gradle.kts b/build.gradle.kts
index d27ced9fe..389d33b31 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,9 +7,9 @@ plugins {
idea {
project {
- jdkName = "21"
- languageLevel = IdeaLanguageLevel("11")
vcs = "Git"
+ jdkName = "25"
+ languageLevel = IdeaLanguageLevel("25")
}
module {
excludeDirs.add(file("dependencies"))
diff --git a/buildSrc/src/main/kotlin/intellij-plugin-common-conventions.gradle.kts b/buildSrc/src/main/kotlin/intellij-plugin-common-conventions.gradle.kts
index 568520965..40facdc38 100644
--- a/buildSrc/src/main/kotlin/intellij-plugin-common-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/intellij-plugin-common-conventions.gradle.kts
@@ -1,5 +1,7 @@
import groovy.util.Node
import groovy.xml.XmlParser
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.jvm.JvmTargetValidationMode
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
@@ -34,6 +36,20 @@ kotlin {
}
}
+if (environmentName.toInt() >= 261) {
+ // For 2026.1+ our Kotlin is compiled with `jvmTarget = 25` (see the KotlinCompile block below),
+ // so our own Kotlin output is Java 25 bytecode. javac from older JDKs cannot read Java 25 class
+ // files ("class file has wrong version 69.0, should be 65.0"), which breaks any module whose
+ // Java sources reference Kotlin-compiled classes (e.g. hs-Python). So the Java toolchain must
+ // match the Kotlin target and use JDK 25 as well, while keeping the Java 21 language level
+ // (sourceCompatibility/targetCompatibility) common for all platform versions.
+ java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(25)
+ }
+ }
+}
+
// It's not possible to use version catalogs in convention plugin as usual,
// so we have to get the catalog itself and libraries manually
// See https://docs.gradle.org/current/userguide/version_catalogs.html#sec:buildsrc-version-catalog
@@ -54,6 +70,11 @@ tasks {
withType {
// Prevents unexpected incremental compilation errors after changing value of `environmentName` property
inputs.property("environmentName", providers.gradleProperty("environmentName"))
+ if (environmentName.toInt() >= 261) {
+ // IntelliJ Platform 2026.1+ contains inline Kotlin APIs compiled to Java 25 bytecode.
+ compilerOptions.jvmTarget = JvmTarget.JVM_25
+ jvmTargetValidationMode.set(JvmTargetValidationMode.WARNING)
+ }
}
processResources {
diff --git a/buildSrc/src/main/kotlin/intellijUtils.kt b/buildSrc/src/main/kotlin/intellijUtils.kt
index ba9b37640..c45a66a47 100644
--- a/buildSrc/src/main/kotlin/intellijUtils.kt
+++ b/buildSrc/src/main/kotlin/intellijUtils.kt
@@ -96,17 +96,18 @@ val Project.rustPlugins: List
)
val Project.cppPlugins: List
- get() = listOfNotNull(
- baseVersion.toTypeWithVersion()
- .version
- .takeUnless { /* TODO remove with 252 */ it.startsWith("2025.2") }
- ?.let { "com.intellij.cmake" },
- "com.intellij.cidr.lang",
- "com.intellij.clion",
- "com.intellij.nativeDebug",
- "org.jetbrains.plugins.clion.test.google",
- "org.jetbrains.plugins.clion.test.catch"
- )
+ get() {
+ val clionBranch = platformBranch(clionVersion)
+ return listOfNotNull(
+ "com.intellij.cmake".takeUnless { /* TODO remove with 252 */ clionBranch == 252 },
+ // The classic language engine was removed from the CLion distribution in 2026.2 (Nova engine only)
+ "com.intellij.cidr.lang".takeIf { clionBranch < 262 },
+ "com.intellij.clion",
+ "com.intellij.nativeDebug",
+ "org.jetbrains.plugins.clion.test.google",
+ "org.jetbrains.plugins.clion.test.catch"
+ )
+ }
val Project.sqlPlugins: List
get() = listOf(
@@ -166,6 +167,32 @@ fun IntelliJPlatformExtension.PluginVerification.Ides.intellijIde(versionWithCod
}
}
+// Branch number of an IDE dependency notation, e.g. "IU-262.8665-EAP-CANDIDATE-SNAPSHOT" -> 262,
+// "CL-262-EAP-SNAPSHOT" -> 262, "RD-2026.1.2" -> 261, "IU-2025.3" -> 253
+fun platformBranch(versionWithCode: String): Int {
+ val version = versionWithCode.toTypeWithVersion().version
+ return if (version.startsWith("20")) {
+ val parts = version.split('.')
+ (parts[0].substring(2) + parts[1]).toInt()
+ }
+ else {
+ version.substringBefore('.').substringBefore('-').toInt()
+ }
+}
+
+// Since 2026.2 (262), some parts of the platform were extracted from the core classpath
+// into separate bundled plugins/modules and require explicit dependencies to compile:
+// - SM test runner -> `intellij.testRunner.plugin` plugin, modules `intellij.platform.smRunner` and `intellij.platform.testRunner`
+// - JCEF -> `com.intellij.modules.jcef` plugin, modules `intellij.platform.ui.jcef` and `intellij.libraries.jcef`
+// - SQLite -> `intellij.platform.sqlite` platform module
+// These module names don't exist in the layout of older IDE versions, so they should be added
+// only when the IDE the module is compiled against is new enough.
+fun IntelliJPlatformDependenciesExtension.bundledModulesSince(ideVersionWithCode: String, sinceBranch: Int, vararg modules: String) {
+ if (platformBranch(ideVersionWithCode) >= sinceBranch) {
+ bundledModules(*modules)
+ }
+}
+
fun IntelliJPlatformDependenciesExtension.intellijPlugins(vararg notations: String) {
for (notation in notations) {
when {
diff --git a/gradle-262.properties b/gradle-262.properties
new file mode 100644
index 000000000..1d6d7de83
--- /dev/null
+++ b/gradle-262.properties
@@ -0,0 +1,10 @@
+customSinceBuild=262.8377
+customUntilBuild=262.*
+
+# Existent IDE versions can be found in the following repos:
+# https://www.jetbrains.com/intellij-repository/releases/
+# https://www.jetbrains.com/intellij-repository/snapshots/
+ideaVersion=IU-262.8665-EAP-CANDIDATE-SNAPSHOT
+clionVersion=CL-262-EAP-SNAPSHOT
+pycharmVersion=PC-262-EAP-SNAPSHOT
+riderVersion=RD-2026.1.2
diff --git a/gradle.properties b/gradle.properties
index cf70a88c9..eab601468 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,7 +1,7 @@
-# supported values: 252, 253, 261
-environmentName=261
+# supported values: 252, 253, 261, 262
+environmentName=262
-pluginVersion=2026.14
+pluginVersion=2026.15
# type of IDE (IDEA, CLion, etc.) used to build/test running
# for more details see `Different IDEs` section in `PlatformVersions.md`
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index d997cfc60..b1b8ef56b 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 c61a118f7..eb84db68d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
networkTimeout=10000
+retries=0
+retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index adff685a0..249efbb03 100755
--- a/gradlew
+++ b/gradlew
@@ -20,7 +20,7 @@
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
diff --git a/gradlew.bat b/gradlew.bat
index e509b2dd8..8508ef684 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,29 +65,18 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/intellij-plugin/hs-CSharp/branches/262/src/org/hyperskill/academy/csharp/compatibilityUtils.kt b/intellij-plugin/hs-CSharp/branches/262/src/org/hyperskill/academy/csharp/compatibilityUtils.kt
new file mode 100644
index 000000000..549876369
--- /dev/null
+++ b/intellij-plugin/hs-CSharp/branches/262/src/org/hyperskill/academy/csharp/compatibilityUtils.kt
@@ -0,0 +1,36 @@
+package org.hyperskill.academy.csharp
+
+import com.intellij.openapi.project.Project
+import com.jetbrains.rd.ide.model.RdPath
+import com.jetbrains.rider.model.RdUnitTestSession
+import com.jetbrains.rider.projectView.SolutionDescriptionFactory
+
+// In 253, SolutionDescriptionFactory.existing requires a displayName parameter
+internal fun createExistingSolutionDescription(solutionPath: String) =
+ SolutionDescriptionFactory.existing(solutionPath, displayName = null)
+
+internal fun String.asRdPath(): RdPath = RdPath("local", this)
+
+/**
+ * Helper class to hold test result data across platform versions.
+ * In 252, this data comes from RdUnitTestResultData.
+ * In 253, the API changed significantly and we construct this from available sources.
+ */
+data class TestResultData(
+ val exceptionLines: String
+)
+
+// In 253, resultData property was removed from RdUnitTestSession
+// The test result output is now accessed differently through the session protocol
+// For now, we provide a no-op implementation - tests will pass/fail but without detailed output
+internal fun adviseResultData(
+ @Suppress("UNUSED_PARAMETER") project: Project,
+ @Suppress("UNUSED_PARAMETER") rdSession: RdUnitTestSession,
+ @Suppress("UNUSED_PARAMETER") nodeId: Int,
+ @Suppress("UNUSED_PARAMETER") callback: (TestResultData) -> Unit
+) {
+ // TODO: BACKCOMPAT 253 - The resultData API was removed in 253.
+ // Need to find the replacement API for getting detailed test failure output.
+ // For now, the callback is not invoked, which means detailed error messages
+ // won't be available, but test pass/fail status will still work.
+}
diff --git a/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/checker/CppCatchEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/checker/CppCatchEduTaskChecker.kt
new file mode 100644
index 000000000..e0a335a14
--- /dev/null
+++ b/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/checker/CppCatchEduTaskChecker.kt
@@ -0,0 +1,26 @@
+package org.hyperskill.academy.cpp.checker
+
+import com.intellij.execution.configurations.ConfigurationFactory
+import com.intellij.openapi.project.Project
+import com.jetbrains.cidr.execution.testing.tcatch.CidrCatchTestRunConfigurationType
+import org.hyperskill.academy.cpp.CppCatchCourseBuilder
+import org.hyperskill.academy.cpp.CppConfigurator
+import org.hyperskill.academy.cpp.CppProjectSettings
+import org.hyperskill.academy.learning.EduCourseBuilder
+import org.hyperskill.academy.learning.checker.TaskChecker
+import org.hyperskill.academy.learning.checker.TaskCheckerProvider
+import org.hyperskill.academy.learning.courseFormat.tasks.EduTask
+
+class CppCatchConfigurator : CppConfigurator() {
+ override val courseBuilder: EduCourseBuilder
+ get() = CppCatchCourseBuilder()
+
+ override val taskCheckerProvider: TaskCheckerProvider =
+ object : CppTaskCheckerProvider() {
+ override fun getEduTaskChecker(task: EduTask, project: Project): TaskChecker =
+ object : CppEduTaskChecker(task, envChecker, project) {
+ override fun getFactory(): ConfigurationFactory =
+ CidrCatchTestRunConfigurationType.getInstance().getFactory()
+ }
+ }
+}
diff --git a/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/checker/CppGEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/checker/CppGEduTaskChecker.kt
new file mode 100644
index 000000000..d5aaff1c4
--- /dev/null
+++ b/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/checker/CppGEduTaskChecker.kt
@@ -0,0 +1,26 @@
+package org.hyperskill.academy.cpp.checker
+
+import com.intellij.execution.configurations.ConfigurationFactory
+import com.intellij.openapi.project.Project
+import com.jetbrains.cidr.execution.testing.google.CidrGoogleTestRunConfigurationType
+import org.hyperskill.academy.cpp.CppConfigurator
+import org.hyperskill.academy.cpp.CppGTestCourseBuilder
+import org.hyperskill.academy.cpp.CppProjectSettings
+import org.hyperskill.academy.learning.EduCourseBuilder
+import org.hyperskill.academy.learning.checker.TaskChecker
+import org.hyperskill.academy.learning.checker.TaskCheckerProvider
+import org.hyperskill.academy.learning.courseFormat.tasks.EduTask
+
+class CppGTestConfigurator : CppConfigurator() {
+ override val courseBuilder: EduCourseBuilder
+ get() = CppGTestCourseBuilder()
+
+ override val taskCheckerProvider: TaskCheckerProvider =
+ object : CppTaskCheckerProvider() {
+ override fun getEduTaskChecker(task: EduTask, project: Project): TaskChecker =
+ object : CppEduTaskChecker(task, envChecker, project) {
+ override fun getFactory(): ConfigurationFactory =
+ CidrGoogleTestRunConfigurationType.getInstance().getFactory()
+ }
+ }
+}
diff --git a/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/cmakeCompat.kt b/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/cmakeCompat.kt
new file mode 100644
index 000000000..aa30c5914
--- /dev/null
+++ b/intellij-plugin/hs-Cpp/branches/262/src/org/hyperskill/academy/cpp/cmakeCompat.kt
@@ -0,0 +1,10 @@
+package org.hyperskill.academy.cpp
+
+import com.jetbrains.cidr.cpp.toolchains.CMakeExecutableTool
+
+internal fun makeCmakeExecutable() {
+ val cmakeFile = CMakeExecutableTool.getBundledCMakeToolBinary(CMakeExecutableTool.ToolKind.CMAKE)
+ if (cmakeFile.exists() && !cmakeFile.canExecute()) {
+ cmakeFile.setExecutable(true)
+ }
+}
diff --git a/intellij-plugin/hs-Cpp/build.gradle.kts b/intellij-plugin/hs-Cpp/build.gradle.kts
index f4a6a1ef7..6933b6ad3 100644
--- a/intellij-plugin/hs-Cpp/build.gradle.kts
+++ b/intellij-plugin/hs-Cpp/build.gradle.kts
@@ -11,6 +11,8 @@ tasks {
dependencies {
intellijPlatform {
intellijIde(clionVersion)
+ // Before 2026.2 the module came to the classpath with the classic engine (`com.intellij.cidr.lang`) plugin
+ bundledModulesSince(clionVersion, 262, "intellij.clion.runFile")
intellijPlugins(cppPlugins)
testIntellijPlatformFramework(project)
diff --git a/intellij-plugin/hs-Go/build.gradle.kts b/intellij-plugin/hs-Go/build.gradle.kts
index f80139243..a82e507c1 100644
--- a/intellij-plugin/hs-Go/build.gradle.kts
+++ b/intellij-plugin/hs-Go/build.gradle.kts
@@ -5,6 +5,7 @@ plugins {
dependencies {
intellijPlatform {
intellijIde(ideaVersion)
+ bundledModulesSince(ideaVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(goPlugin, intelliLangPlugin)
// Workaround to make tests work - the module is not loaded automatically
diff --git a/intellij-plugin/hs-JavaScript/build.gradle.kts b/intellij-plugin/hs-JavaScript/build.gradle.kts
index f4dc76fbb..518e16639 100644
--- a/intellij-plugin/hs-JavaScript/build.gradle.kts
+++ b/intellij-plugin/hs-JavaScript/build.gradle.kts
@@ -5,6 +5,7 @@ plugins {
dependencies {
intellijPlatform {
intellijIde(ideaVersion)
+ bundledModulesSince(ideaVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(javaScriptPlugins)
}
diff --git a/intellij-plugin/hs-JavaScript/src/org/hyperskill/academy/javascript/learning/JsLanguageSettings.kt b/intellij-plugin/hs-JavaScript/src/org/hyperskill/academy/javascript/learning/JsLanguageSettings.kt
index 42bff0035..42bc13798 100644
--- a/intellij-plugin/hs-JavaScript/src/org/hyperskill/academy/javascript/learning/JsLanguageSettings.kt
+++ b/intellij-plugin/hs-JavaScript/src/org/hyperskill/academy/javascript/learning/JsLanguageSettings.kt
@@ -1,7 +1,6 @@
package org.hyperskill.academy.javascript.learning
import com.intellij.javascript.nodejs.interpreter.NodeInterpreterUtil
-import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreter
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterField
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager
import com.intellij.openapi.project.ProjectManager
@@ -29,8 +28,11 @@ class JsLanguageSettings : LanguageSettings() {
return true
}
}
- interpreterField.addChangeListener { interpreter: NodeJsInterpreter? ->
- jsSettings.selectedInterpreter = interpreter
+ // The listener parameter type differs between platform versions
+ // (`NodeJsInterpreter?` before 2026.2, `NodeJsInterpreterRef?` since),
+ // so read the current interpreter from the field instead of using the parameter
+ interpreterField.addChangeListener { _ ->
+ jsSettings.selectedInterpreter = interpreterField.interpreter
notifyListeners()
}
interpreterField.interpreterRef = NodeJsInterpreterManager.getInstance(defaultProject).interpreterRef
diff --git a/intellij-plugin/hs-Python/branches/252/src/org/hyperskill/academy/python/learning/newproject/compat.kt b/intellij-plugin/hs-Python/branches/252/src/org/hyperskill/academy/python/learning/newproject/compat.kt
new file mode 100644
index 000000000..5732c19b7
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/252/src/org/hyperskill/academy/python/learning/newproject/compat.kt
@@ -0,0 +1,29 @@
+package org.hyperskill.academy.python.learning.newproject
+
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.sdk.PyDetectedSdk
+import com.jetbrains.python.sdk.PySdkToInstall
+import com.jetbrains.python.sdk.detectSystemWideSdks
+
+private val LOG = logger()
+
+/**
+ * If [sdk] is an "install Python" suggestion ([PySdkToInstall]), downloads and installs Python
+ * and returns the freshly detected system-wide SDK, or `null` if the installation failed.
+ * Returns `null` if [sdk] is not such a suggestion.
+ */
+@Suppress("UnstableApiUsage")
+internal fun installSdkIfSuggested(sdk: Sdk?): PyDetectedSdk? {
+ if (sdk !is PySdkToInstall) return null
+
+ return invokeAndWaitIfNeeded {
+ sdk.install(null) {
+ detectSystemWideSdks(null, emptyList())
+ }.getOrElse {
+ LOG.warn(it)
+ null
+ }
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/253/src/org/hyperskill/academy/python/learning/newproject/compat.kt b/intellij-plugin/hs-Python/branches/253/src/org/hyperskill/academy/python/learning/newproject/compat.kt
new file mode 100644
index 000000000..5732c19b7
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/253/src/org/hyperskill/academy/python/learning/newproject/compat.kt
@@ -0,0 +1,29 @@
+package org.hyperskill.academy.python.learning.newproject
+
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.sdk.PyDetectedSdk
+import com.jetbrains.python.sdk.PySdkToInstall
+import com.jetbrains.python.sdk.detectSystemWideSdks
+
+private val LOG = logger()
+
+/**
+ * If [sdk] is an "install Python" suggestion ([PySdkToInstall]), downloads and installs Python
+ * and returns the freshly detected system-wide SDK, or `null` if the installation failed.
+ * Returns `null` if [sdk] is not such a suggestion.
+ */
+@Suppress("UnstableApiUsage")
+internal fun installSdkIfSuggested(sdk: Sdk?): PyDetectedSdk? {
+ if (sdk !is PySdkToInstall) return null
+
+ return invokeAndWaitIfNeeded {
+ sdk.install(null) {
+ detectSystemWideSdks(null, emptyList())
+ }.getOrElse {
+ LOG.warn(it)
+ null
+ }
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/261/src/org/hyperskill/academy/python/learning/newproject/compat.kt b/intellij-plugin/hs-Python/branches/261/src/org/hyperskill/academy/python/learning/newproject/compat.kt
new file mode 100644
index 000000000..5732c19b7
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/261/src/org/hyperskill/academy/python/learning/newproject/compat.kt
@@ -0,0 +1,29 @@
+package org.hyperskill.academy.python.learning.newproject
+
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.sdk.PyDetectedSdk
+import com.jetbrains.python.sdk.PySdkToInstall
+import com.jetbrains.python.sdk.detectSystemWideSdks
+
+private val LOG = logger()
+
+/**
+ * If [sdk] is an "install Python" suggestion ([PySdkToInstall]), downloads and installs Python
+ * and returns the freshly detected system-wide SDK, or `null` if the installation failed.
+ * Returns `null` if [sdk] is not such a suggestion.
+ */
+@Suppress("UnstableApiUsage")
+internal fun installSdkIfSuggested(sdk: Sdk?): PyDetectedSdk? {
+ if (sdk !is PySdkToInstall) return null
+
+ return invokeAndWaitIfNeeded {
+ sdk.install(null) {
+ detectSystemWideSdks(null, emptyList())
+ }.getOrElse {
+ LOG.warn(it)
+ null
+ }
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/PyTargetEnvCreationManager.java b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/PyTargetEnvCreationManager.java
new file mode 100644
index 000000000..6439ff70c
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/PyTargetEnvCreationManager.java
@@ -0,0 +1,387 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.RunCanceledByUserException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.CapturingProcessHandler;
+import com.intellij.execution.process.ProcessOutput;
+import com.intellij.execution.target.TargetEnvironment;
+import com.intellij.execution.target.TargetEnvironmentRequest;
+import com.intellij.execution.target.TargetProgressIndicator;
+import com.intellij.execution.target.TargetedCommandLine;
+import com.intellij.execution.target.local.LocalTargetEnvironment;
+import com.intellij.execution.target.value.TargetEnvironmentFunctions;
+import com.intellij.execution.util.ExecUtil;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.EmptyProgressIndicator;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.python.community.helpersLocator.PythonHelpersLocator;
+import com.intellij.util.concurrency.annotations.RequiresReadLock;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.net.HttpConfigurable;
+import com.jetbrains.python.HelperPackage;
+import com.jetbrains.python.PythonHelper;
+import com.jetbrains.python.errorProcessing.Exe;
+import com.jetbrains.python.errorProcessing.ExecErrorImpl;
+import com.jetbrains.python.errorProcessing.ExecErrorReason;
+import com.jetbrains.python.errorProcessing.PyError;
+import com.jetbrains.python.packaging.common.PythonPackage;
+import com.jetbrains.python.packaging.pip.PipParseUtils;
+import com.jetbrains.python.psi.LanguageLevel;
+import com.jetbrains.python.run.PythonExecution;
+import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory;
+import com.jetbrains.python.run.PythonScriptExecution;
+import com.jetbrains.python.run.PythonScripts;
+import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest;
+import com.jetbrains.python.sdk.PyDetectedSdk;
+import com.jetbrains.python.sdk.PySdkExtKt;
+import com.jetbrains.python.sdk.PythonSdkType;
+import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
+import com.jetbrains.python.sdk.impl.PySdkBundle;
+import com.jetbrains.python.venvReader.VirtualEnvReaderKt;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+
+import static com.jetbrains.python.packaging.CompatKt.createForException;
+import static com.jetbrains.python.packaging.CompatKt.createForTimeout;
+import static com.jetbrains.python.packaging.repository.CompatKt.encodeCredentialsForUrl;
+import static com.jetbrains.python.sdk.CompatKt.adminPermissionsNeeded;
+
+/**
+ * @deprecated TODO: explain
+ */
+@SuppressWarnings("ALL")
+@Deprecated(forRemoval = true)
+@ApiStatus.Internal
+public class PyTargetEnvCreationManager {
+ private static final Logger LOG = Logger.getInstance(PyTargetEnvCreationManager.class);
+ private final @NotNull Sdk mySdk;
+
+ protected static final String SETUPTOOLS_VERSION = "44.1.1";
+ protected static final String PIP_VERSION = "24.3.1";
+
+ protected static final String SETUPTOOLS_WHEEL_NAME = "setuptools-" + SETUPTOOLS_VERSION + "-py2.py3-none-any.whl";
+ protected static final String PIP_WHEEL_NAME = "pip-" + PIP_VERSION + "-py2.py3-none-any.whl";
+
+ protected static final int ERROR_NO_SETUPTOOLS = 3;
+
+
+ protected static final String PACKAGING_TOOL = "packaging_tool.py";
+ protected static final int TIMEOUT = 10 * 60 * 1000;
+
+ protected static final String INSTALL = "install";
+ protected static final String UNINSTALL = "uninstall";
+ private final AtomicBoolean myUpdatingCache = new AtomicBoolean(false);
+ protected String mySeparator = File.separator;
+ protected volatile @Nullable List myPackagesCache = null;
+
+ public PyTargetEnvCreationManager(final @NotNull Sdk sdk) {
+ mySdk = sdk;
+ }
+
+ public @NotNull String createVirtualEnv(@NotNull String destinationDir, boolean useGlobalSite) throws ExecutionException {
+ final Sdk sdk = getSdk();
+ final LanguageLevel languageLevel = getOrRequestLanguageLevelForSdk(sdk);
+
+ if (languageLevel.isOlderThan(LanguageLevel.PYTHON27)) {
+ throw new ExecutionException(PySdkBundle.message("python.sdk.packaging.creating.virtual.environment.for.python.not.supported",
+ languageLevel, LanguageLevel.PYTHON27));
+ }
+
+ HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest = getPythonTargetInterpreter();
+ TargetEnvironmentRequest targetEnvironmentRequest = helpersAwareTargetRequest.getTargetEnvironmentRequest();
+
+ PythonScriptExecution pythonExecution = PythonScripts.prepareHelperScriptExecution(
+ isLegacyPython(languageLevel) ? PythonHelper.LEGACY_VIRTUALENV_ZIPAPP : PythonHelper.VIRTUALENV_ZIPAPP,
+ helpersAwareTargetRequest);
+ if (useGlobalSite) {
+ pythonExecution.addParameter("--system-site-packages");
+ }
+ pythonExecution.addParameter(destinationDir);
+ // TODO [targets] Pass `parentDir = null`
+ getPythonProcessResult(pythonExecution, false, true, targetEnvironmentRequest);
+
+ final Path binary = VirtualEnvReaderKt.VirtualEnvReader().findPythonInPythonRoot(Path.of(destinationDir));
+ final char separator = targetEnvironmentRequest.getTargetPlatform().getPlatform().fileSeparator;
+ final String binaryFallback = destinationDir + separator + "bin" + separator + "python";
+
+ return (binary != null) ? binary.toString() : binaryFallback;
+ }
+
+ private @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter() throws ExecutionException {
+ HelpersAwareTargetEnvironmentRequest request = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(getSdk(),
+ ProjectManager.getInstance()
+ .getDefaultProject());
+ if (request == null) {
+ throw new ExecutionException(PySdkBundle.message("python.sdk.package.managing.not.supported.for.sdk", getSdk().getName()));
+ }
+ return request;
+ }
+
+ /**
+ * Is it a legacy python version that we still support
+ */
+ private static @NotNull Boolean isLegacyPython(@NotNull LanguageLevel languageLevel) {
+ return languageLevel.isPython2() || languageLevel.isOlderThan(LanguageLevel.PYTHON37);
+ }
+
+ private @NotNull String getPythonProcessResult(@NotNull PythonExecution pythonExecution,
+ boolean askForSudo,
+ boolean showProgress,
+ @NotNull TargetEnvironmentRequest targetEnvironmentRequest) throws ExecutionException {
+ ProcessOutputWithCommandLine result = getPythonProcessOutput(pythonExecution, askForSudo, showProgress, targetEnvironmentRequest);
+ String path = result.getExePath();
+ List args = result.getArgs();
+ ProcessOutput processOutput = result.getProcessOutput();
+ int exitCode = processOutput.getExitCode();
+ if (processOutput.isTimeout()) {
+ // TODO [targets] Make cancellable right away?
+ PyError pyError = createForTimeout(PySdkBundle.message("python.sdk.packaging.timed.out"),
+ path,
+ args);
+ throw new PyExecutionException(pyError);
+ }
+ else if (exitCode != 0) {
+ throw new PyExecutionException(PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode), path, args, processOutput);
+ }
+ return processOutput.getStdout();
+ }
+
+ protected final @NotNull Sdk getSdk() {
+ return mySdk;
+ }
+
+ private @NotNull PyTargetEnvCreationManager.ProcessOutputWithCommandLine getPythonProcessOutput(@NotNull PythonExecution pythonExecution,
+ boolean askForSudo,
+ boolean showProgress,
+ @NotNull TargetEnvironmentRequest targetEnvironmentRequest)
+ throws ExecutionException {
+ // TODO [targets] Use `showProgress = true`
+ // TODO [targets] Use `workingDir`
+ // TODO [targets] Use `useUserSite` (handle use sudo)
+ TargetProgressIndicator targetProgressIndicator = TargetProgressIndicator.EMPTY;
+ TargetEnvironment targetEnvironment = targetEnvironmentRequest.prepareEnvironment(targetProgressIndicator);
+ for (Map.Entry entry : targetEnvironment.getUploadVolumes()
+ .entrySet()) {
+ try {
+ entry.getValue().upload(".", TargetProgressIndicator.EMPTY);
+ }
+ catch (IOException e) {
+ throw new ExecutionException(e);
+ }
+ }
+ // TODO [targets] Should `interpreterParameters` be here?
+ TargetedCommandLine targetedCommandLine = PythonScripts.buildTargetedCommandLine(pythonExecution,
+ targetEnvironment,
+ getSdk(),
+ Collections.emptyList()
+ );
+ // TODO [targets] Set parent directory of interpreter as the working directory
+
+ LOG.info("Running packaging tool");
+
+ // TODO [targets] Apply environment variables: setPythonUnbuffered(...), setPythonDontWriteBytecode(...), resetHomePathChanges(...)
+ // TODO [targets] Apply flavor from PythonSdkFlavor.getFlavor(mySdk)
+ ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
+ Process process = createProcess(targetEnvironment, targetedCommandLine, askForSudo, indicator);
+ List commandLine = targetedCommandLine.collectCommandsSynchronously();
+ String commandLineString = StringUtil.join(commandLine, " ");
+ final CapturingProcessHandler handler =
+ new CapturingProcessHandler(process, targetedCommandLine.getCharset(), commandLineString);
+ final ProcessOutput result;
+ if (showProgress && indicator != null) {
+ handler.addProcessListener(new IndicatedProcessOutputListener(indicator));
+ result = handler.runProcessWithProgressIndicator(indicator);
+ }
+ else {
+ // TODO [targets] Check if timeout is ok for all targets
+ result = handler.runProcess(TIMEOUT);
+ }
+ if (result.isCancelled()) {
+ throw new RunCanceledByUserException();
+ }
+ result.checkSuccess(LOG);
+ final int exitCode = result.getExitCode();
+ String helperPath = ContainerUtil.getFirstItem(commandLine, "");
+ List args = commandLine.subList(Math.min(1, commandLine.size()), commandLine.size());
+ if (exitCode != 0) {
+ final String message = StringUtil.isEmptyOrSpaces(result.getStdout()) && StringUtil.isEmptyOrSpaces(result.getStderr())
+ ? PySdkBundle.message("python.conda.permission.denied")
+ : PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode);
+ throw new PyExecutionException(message, helperPath, args, result);
+ }
+ return new ProcessOutputWithCommandLine(helperPath, args, result);
+ }
+
+ private @NotNull Process createProcess(@NotNull TargetEnvironment targetEnvironment,
+ @NotNull TargetedCommandLine targetedCommandLine,
+ boolean askForSudo,
+ @Nullable ProgressIndicator indicator) throws ExecutionException {
+ if (askForSudo) {
+ if (!(targetEnvironment instanceof LocalTargetEnvironment)) {
+ // TODO [targets] Execute process on non-local target using sudo
+ LOG.warn("Sudo flag is ignored");
+ }
+ else if (adminPermissionsNeeded(getSdk())) {
+ // This is hack to process sudo flag in the local environment
+ GeneralCommandLine localCommandLine = ((LocalTargetEnvironment)targetEnvironment).createGeneralCommandLine(targetedCommandLine);
+ return executeOnLocalMachineWithSudo(localCommandLine);
+ }
+ }
+ // TODO [targets] Pass meaningful progress indicator
+ return targetEnvironment.createProcess(targetedCommandLine, Objects.requireNonNullElseGet(indicator, EmptyProgressIndicator::new));
+ }
+
+ protected void installUsingPipWheel(String @NotNull ... pipArgs) throws ExecutionException {
+ HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest = getPythonTargetInterpreter();
+ PythonScriptExecution pythonExecution =
+ PythonScripts.prepareHelperScriptExecution(getPipHelperPackage(), helpersAwareTargetRequest);
+ pythonExecution.addParameter(INSTALL);
+ pythonExecution.addParameters(pipArgs);
+
+ getPythonProcessResult(pythonExecution, true, true, helpersAwareTargetRequest.getTargetEnvironmentRequest());
+ }
+
+ @RequiresReadLock(generateAssertion = false)
+ protected static @NotNull LanguageLevel getOrRequestLanguageLevelForSdk(@NotNull Sdk sdk) throws ExecutionException {
+ if (sdk instanceof PyDetectedSdk) {
+ final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk);
+ if (flavor != null && sdk.getHomePath() != null) {
+ return flavor.getLanguageLevel(sdk.getHomePath());
+ }
+ throw new ExecutionException(PySdkBundle.message("python.sdk.packaging.cannot.retrieve.version", sdk.getHomePath()));
+ }
+ // Use the cached version for an already configured SDK
+ return PythonSdkType.getLanguageLevelForSdk(sdk);
+ }
+
+ protected static @Nullable String getProxyString() {
+ final HttpConfigurable settings = HttpConfigurable.getInstance();
+ if (settings != null && settings.USE_HTTP_PROXY) {
+ final String credentials;
+ if (settings.PROXY_AUTHENTICATION) {
+ credentials = String.format("%s:%s@", settings.getProxyLogin(), settings.getPlainProxyPassword());
+ }
+ else {
+ credentials = "";
+ }
+ return "http://" + credentials + String.format("%s:%d", settings.PROXY_HOST, settings.PROXY_PORT);
+ }
+ return null;
+ }
+
+ protected static @NotNull List makeSafeToDisplayCommand(@NotNull List cmdline) {
+ final List safeCommand = new ArrayList<>(cmdline);
+ for (int i = 0; i < safeCommand.size(); i++) {
+ if (cmdline.get(i).equals("--proxy") && i + 1 < cmdline.size()) {
+ safeCommand.set(i + 1, makeSafeUrlArgument(cmdline.get(i + 1)));
+ }
+ if (cmdline.get(i).equals("--index-url") && i + 1 < cmdline.size()) {
+ safeCommand.set(i + 1, makeSafeUrlArgument(cmdline.get(i + 1)));
+ }
+ }
+ return safeCommand;
+ }
+
+ private static @NotNull String makeSafeUrlArgument(@NotNull String urlArgument) {
+ try {
+ final URI proxyUri = new URI(urlArgument);
+ final String credentials = proxyUri.getUserInfo();
+ if (credentials != null) {
+ final int colonIndex = credentials.indexOf(":");
+ if (colonIndex >= 0) {
+ final String login = credentials.substring(0, colonIndex);
+ final String password = credentials.substring(colonIndex + 1);
+ final String maskedPassword = StringUtil.repeatSymbol('*', password.length());
+ final String maskedCredentials = login + ":" + maskedPassword;
+ if (urlArgument.contains(credentials)) {
+ return urlArgument.replaceFirst(Pattern.quote(credentials), maskedCredentials);
+ }
+ else {
+ final String encodedCredentials = encodeCredentialsForUrl(login, password);
+ return urlArgument.replaceFirst(Pattern.quote(encodedCredentials), maskedCredentials);
+ }
+ }
+ }
+ }
+ catch (URISyntaxException ignored) {
+ }
+ return urlArgument;
+ }
+
+ protected static @NotNull List parsePackagingToolOutput(@NotNull String output) {
+ List<@NotNull PythonPackage> packageList = PipParseUtils.parseListResult(output);
+ List packages = new ArrayList<>();
+ for (PythonPackage pythonPackage : packageList) {
+ PyPackage pkg = new PyPackage(pythonPackage.getName(), pythonPackage.getVersion());
+ packages.add(pkg);
+ }
+ return packages;
+ }
+
+ private static void applyWorkingDir(@NotNull PythonScriptExecution execution, @Nullable String workingDir) {
+ if (workingDir == null) {
+ // TODO [targets] Set the parent of home path as the working directory
+ }
+ else {
+ execution.setWorkingDir(TargetEnvironmentFunctions.constant(workingDir));
+ }
+ }
+
+ private static @NotNull Process executeOnLocalMachineWithSudo(@NotNull GeneralCommandLine localCommandLine) throws ExecutionException {
+ try {
+ return ExecUtil.sudo(localCommandLine, PySdkBundle.message("python.sdk.packaging.enter.your.password.to.make.changes"));
+ }
+ catch (IOException e) {
+ String exePath = localCommandLine.getExePath();
+ List args = localCommandLine.getCommandLineList(exePath);
+ throw new PyExecutionException(createForException(e, null, exePath, args));
+ }
+ }
+
+
+ private static @NotNull HelperPackage getPipHelperPackage() {
+ return new PythonHelper.ScriptPythonHelper(PIP_WHEEL_NAME + "/" + PyPackageUtil.PIP,
+ PythonHelpersLocator.getCommunityHelpersRoot().toFile(),
+ Collections.emptyList());
+ }
+
+ private static class ProcessOutputWithCommandLine {
+ private final @NotNull String myExePath;
+ private final @NotNull List myArgs;
+ private final @NotNull ProcessOutput myProcessOutput;
+
+ private ProcessOutputWithCommandLine(@NotNull String exePath,
+ @NotNull List args,
+ @NotNull ProcessOutput output) {
+ myExePath = exePath;
+ myArgs = args;
+ myProcessOutput = output;
+ }
+
+ private @NotNull String getExePath() { return myExePath; }
+
+ private @NotNull List getArgs() { return myArgs; }
+
+ private @NotNull ProcessOutput getProcessOutput() { return myProcessOutput; }
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/compat.kt
new file mode 100644
index 000000000..08caa4c45
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/compat.kt
@@ -0,0 +1,31 @@
+package com.jetbrains.python.packaging
+
+import com.intellij.openapi.util.NlsContexts
+import com.jetbrains.python.errorProcessing.Exe
+import com.jetbrains.python.errorProcessing.ExecError
+import com.jetbrains.python.errorProcessing.ExecErrorImpl
+import com.jetbrains.python.errorProcessing.ExecErrorReason
+import java.io.IOException
+
+fun createForTimeout(
+ additionalMessageToUser: @NlsContexts.DialogMessage String?,
+ command: String,
+ args: List,
+): ExecError = ExecErrorImpl(
+ exe = Exe.fromString(command),
+ args = args.toTypedArray(),
+ errorReason = ExecErrorReason.Timeout,
+ additionalMessageToUser = additionalMessageToUser,
+)
+
+fun createForException(
+ exception: IOException,
+ additionalMessageToUser: @NlsContexts.DialogMessage String?,
+ command: String,
+ args: List,
+): ExecError = ExecErrorImpl(
+ exe = Exe.fromString(command),
+ args = args.toTypedArray(),
+ errorReason = ExecErrorReason.CantStart(null, exception.localizedMessage),
+ additionalMessageToUser = additionalMessageToUser,
+)
\ No newline at end of file
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/repository/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/repository/compat.kt
new file mode 100644
index 000000000..71b67e1b5
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/repository/compat.kt
@@ -0,0 +1,8 @@
+package com.jetbrains.python.packaging.repository
+
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+
+internal fun encodeCredentialsForUrl(login: String, password: String): String {
+ return "${URLEncoder.encode(login, StandardCharsets.UTF_8)}:${URLEncoder.encode(password, StandardCharsets.UTF_8)}"
+}
\ No newline at end of file
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/add/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/add/compat.kt
new file mode 100644
index 000000000..b87d94380
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/add/compat.kt
@@ -0,0 +1,121 @@
+package com.jetbrains.python.sdk.add
+
+import com.intellij.openapi.application.AppUIExecutor
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.fileChooser.FileChooser
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.ui.ComboBox
+import com.intellij.openapi.ui.ComponentWithBrowseButton
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.ui.AnimatedIcon
+import com.intellij.ui.components.fields.ExtendableTextComponent
+import com.intellij.ui.components.fields.ExtendableTextField
+import com.jetbrains.python.sdk.PyDetectedSdk
+import com.jetbrains.python.sdk.PySdkListCellRenderer
+import com.jetbrains.python.sdk.PythonSdkType
+import javax.swing.JTextField
+import javax.swing.plaf.basic.BasicComboBoxEditor
+
+/**
+ * Minimal reimplementation of `com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox`,
+ * which was removed from the Python plugin in 262 together with the whole v1 "add SDK" UI.
+ *
+ * Combobox items are plain [Sdk] instances.
+ */
+class PySdkPathChoosingComboBox : ComponentWithBrowseButton>(ComboBox(), null) {
+
+ private val busyIconExtension: ExtendableTextComponent.Extension =
+ ExtendableTextComponent.Extension { AnimatedIcon.Default.INSTANCE }
+
+ private val busyEditor: BasicComboBoxEditor = object : BasicComboBoxEditor() {
+ override fun createEditorComponent(): JTextField = ExtendableTextField().apply { isEditable = false }
+ }
+
+ init {
+ childComponent.renderer = PySdkListCellRenderer()
+ addActionListener {
+ val descriptor = PythonSdkType.getInstance().homeChooserDescriptor
+ FileChooser.chooseFiles(descriptor, null, null) { chosenFiles ->
+ val virtualFile = chosenFiles.firstOrNull() ?: return@chooseFiles
+ val path = FileUtil.toSystemDependentName(virtualFile.path)
+ childComponent.selectedItem = items.find { it.homePath == path } ?: PyDetectedSdk(path).also { addSdkItemOnTop(it) }
+ }
+ }
+ }
+
+ val selectedSdk: Sdk?
+ get() = childComponent.selectedItem as? Sdk
+
+ val items: List
+ get() = (0 until childComponent.itemCount).mapNotNull { childComponent.getItemAt(it) as? Sdk }
+
+ fun addSdkItem(sdk: Sdk) {
+ childComponent.addItem(sdk)
+ }
+
+ private fun addSdkItemOnTop(sdk: Sdk) {
+ childComponent.insertItemAt(sdk, 0)
+ }
+
+ fun setBusy(busy: Boolean) {
+ if (busy) {
+ childComponent.isEditable = true
+ childComponent.editor = busyEditor
+ (busyEditor.editorComponent as ExtendableTextField).addExtension(busyIconExtension)
+ }
+ else {
+ (busyEditor.editorComponent as ExtendableTextField).removeExtension(busyIconExtension)
+ childComponent.isEditable = false
+ }
+ repaint()
+ }
+}
+
+/**
+ * Kept for source compatibility with previous platform versions where the combobox could contain
+ * a "new sdk" item. This reimplementation never adds such an item.
+ */
+class NewPySdkComboBoxItem
+
+/**
+ * Copy-pasted from intellij sources.
+ * In 261 marked as deprecated to be removed, removed in 262.
+ * TODO: rewrite
+ */
+fun addInterpretersAsync(
+ sdkComboBox: PySdkPathChoosingComboBox,
+ sdkObtainer: () -> List,
+ onAdded: (List) -> Unit,
+) {
+ ApplicationManager.getApplication().executeOnPooledThread {
+ val executor = AppUIExecutor.onUiThread(ModalityState.any())
+ executor.execute { sdkComboBox.setBusy(true) }
+ var sdks = emptyList()
+ try {
+ sdks = sdkObtainer()
+ }
+ finally {
+ executor.execute {
+ sdkComboBox.setBusy(false)
+ sdkComboBox.removeAllItems()
+ sdks.forEach(sdkComboBox::addSdkItem)
+ onAdded(sdks)
+ }
+ }
+ }
+}
+
+/**
+ * Keeps [NewPySdkComboBoxItem] if it is present in the combobox.
+ */
+private fun PySdkPathChoosingComboBox.removeAllItems() {
+ if (childComponent.itemCount > 0 && childComponent.getItemAt(0) is NewPySdkComboBoxItem) {
+ while (childComponent.itemCount > 1) {
+ childComponent.removeItemAt(1)
+ }
+ }
+ else {
+ childComponent.removeAllItems()
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/compat.kt
new file mode 100644
index 000000000..94846f122
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/compat.kt
@@ -0,0 +1,47 @@
+package com.jetbrains.python.sdk
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleUtil
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.util.UserDataHolder
+import com.intellij.openapi.vfs.VirtualFile
+import com.jetbrains.python.sdk.skeleton.PySkeletonUtil
+import java.nio.file.Files
+import java.nio.file.Paths
+
+internal fun Project.excludeInnerVirtualEnv(sdk: Sdk) {
+ val binary = sdk.homeDirectory ?: return
+ ModuleUtil.findModuleForFile(binary, this)?.excludeInnerVirtualEnv(sdk)
+}
+
+/**
+ * Replacement for `com.jetbrains.python.sdk.findBaseSdks` removed in 262.
+ *
+ * The original implementation also returned system-wide SDKs from [existingSdks];
+ * all call sites in this plugin pass an empty list, so only detection is performed here.
+ */
+@Suppress("DEPRECATION_ERROR")
+fun findBaseSdks(existingSdks: List, module: Module?, context: UserDataHolder): List {
+ return detectSystemWideSdks(module, existingSdks, context)
+}
+
+/**
+ * Replacement for the `com.jetbrains.python.sdk.sdkSeemsValid` extension, which is not available in 262.
+ */
+val Sdk.sdkSeemsValid: Boolean
+ get() = isSdkSeemsValid
+
+/**
+ * TODO: 262 — `PySdkToInstall` and `getSdksToInstall()` became internal in the Python plugin,
+ * so "install Python" SDK suggestions are not offered on this platform.
+ */
+fun getSdksToInstall(): List = emptyList()
+
+internal fun Sdk.adminPermissionsNeeded(): Boolean {
+ val pathToCheck = sitePackagesDirectory?.path ?: homePath ?: return false
+ return !Files.isWritable(Paths.get(pathToCheck))
+}
+
+private val Sdk.sitePackagesDirectory: VirtualFile?
+ get() = PySkeletonUtil.getSitePackagesDirectory(this)
\ No newline at end of file
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/configuration/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/configuration/compat.kt
new file mode 100644
index 000000000..16a9c1dea
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/configuration/compat.kt
@@ -0,0 +1,112 @@
+package com.jetbrains.python.sdk.configuration
+
+import com.intellij.execution.ExecutionException
+import com.intellij.openapi.Disposable
+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.project.Project
+import com.intellij.openapi.projectRoots.ProjectJdkTable
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.UserDataHolder
+import com.intellij.openapi.util.UserDataHolderBase
+import com.intellij.openapi.vfs.StandardFileSystems
+import com.intellij.platform.ide.progress.ModalTaskOwner
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import com.intellij.util.concurrency.annotations.RequiresEdt
+import com.jetbrains.python.packaging.PyPackageManagers
+import com.jetbrains.python.packaging.PyTargetEnvCreationManager
+import com.jetbrains.python.sdk.PythonSdkType
+import com.jetbrains.python.sdk.baseDir
+import com.jetbrains.python.sdk.excludeInnerVirtualEnv
+import com.jetbrains.python.sdk.impl.PySdkBundle
+import com.jetbrains.python.sdk.setAssociationToModule
+import com.jetbrains.python.sdk.setAssociationToPath
+import com.jetbrains.python.sdk.targetEnvConfiguration
+import com.jetbrains.python.target.ui.TargetPanelExtension
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Reimplementation of `com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously`:
+ * in 262 its building blocks (`installSdkIfNeeded`, `createSdkByGenerateTask`, `pyModalBlocking`,
+ * `PyTargetAwareAdditionalData.getInterpreterVersion`, `setAssociationToModuleAsync`) became internal
+ * or were removed from the Python plugin.
+ *
+ * TODO: 262 — only local base SDKs are supported, the Targets API branch was dropped.
+ */
+@ApiStatus.Internal
+@RequiresEdt
+@Suppress("UNUSED_PARAMETER")
+fun createVirtualEnvAndSdkSynchronously(
+ baseSdk: Sdk,
+ existingSdks: List,
+ venvRoot: String,
+ projectBasePath: String?,
+ project: Project?,
+ module: Module?,
+ context: UserDataHolder = UserDataHolderBase(),
+ inheritSitePackages: Boolean = false,
+ makeShared: Boolean = false,
+ targetPanelExtension: TargetPanelExtension? = null,
+): Sdk {
+ if (baseSdk.targetEnvConfiguration != null) {
+ // TODO: 262 — target-based SDK creation APIs (`createSdkForTarget` and friends) are internal now
+ throw ExecutionException("Creating virtual environments for target-based SDKs is not supported")
+ }
+
+ // Historically `installSdkIfNeeded` installed Python first when `baseSdk` was a `PySdkToInstall`.
+ // On 262 `PySdkToInstall` is internal and never reaches this method, so `baseSdk` is always an installed SDK.
+ val installedSdk: Sdk = baseSdk
+
+ val projectPath = projectBasePath ?: module?.baseDir?.path ?: project?.basePath
+ val task = object : Task.WithResult(project, PySdkBundle.message("python.creating.venv.title"), false) {
+ override fun compute(indicator: ProgressIndicator): String {
+ indicator.isIndeterminate = true
+ val sdk = if (installedSdk is Disposable && Disposer.isDisposed(installedSdk)) {
+ ProjectJdkTable.getInstance().findJdk(installedSdk.name)!!
+ }
+ else {
+ installedSdk
+ }
+
+ try {
+ return PyTargetEnvCreationManager(sdk).createVirtualEnv(venvRoot, inheritSitePackages)
+ }
+ finally {
+ PyPackageManagers.getInstance().clearCache(sdk)
+ }
+ }
+ }
+ val venvSdk = createSdkByGenerateTask(task, existingSdks)
+
+ if (!makeShared) {
+ when {
+ module != null -> runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") { venvSdk.setAssociationToModule(module) }
+ projectPath != null -> runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") { venvSdk.setAssociationToPath(projectPath) }
+ }
+ }
+
+ project?.excludeInnerVirtualEnv(venvSdk)
+
+ // Note: previous versions of this compat code constructed (but never ran) a background task
+ // storing the preferred virtual env base path in `PySdkSettings`, so it is intentionally omitted here.
+
+ return venvSdk
+}
+
+/**
+ * Replacement for `com.jetbrains.python.sdk.createSdkByGenerateTask` removed in 262:
+ * runs [generateSdkHomePath] under a progress indicator and sets up an SDK for the resulting interpreter path.
+ */
+private fun createSdkByGenerateTask(
+ generateSdkHomePath: Task.WithResult,
+ existingSdks: List,
+): Sdk {
+ val homePath = ProgressManager.getInstance().run(generateSdkHomePath)
+ val homeFile = StandardFileSystems.local().refreshAndFindFileByPath(homePath)
+ ?: throw ExecutionException("Python interpreter is not found at $homePath")
+ return SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile, PythonSdkType.getInstance(), null, null)
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt
new file mode 100644
index 000000000..ce6a36944
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt
@@ -0,0 +1,16 @@
+package org.hyperskill.academy.python.learning
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import com.jetbrains.python.sdk.setAssociationToModule
+
+// Note: unlike previous versions, this file does not provide `installRequiredPackages(reporter, packageManager, requirements)`:
+// it had no callers, and the APIs it relied on (`PythonPackageManager.installPackage`, `repositoryManager`, `toInstallRequest`)
+// became internal in 262. Package installation goes through `PyEduUtils.installRequiredPackages(project, sdk)` instead.
+
+internal fun setAssociationToModule(sdk: Sdk, module: Module) {
+ runWithModalProgressBlocking(module.project, "") {
+ sdk.setAssociationToModule(module)
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/newproject/compat.kt b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/newproject/compat.kt
new file mode 100644
index 000000000..39c519f16
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/newproject/compat.kt
@@ -0,0 +1,12 @@
+package org.hyperskill.academy.python.learning.newproject
+
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.sdk.PyDetectedSdk
+
+/**
+ * TODO: 262 — `PySdkToInstall` and `getSdksToInstall()` became internal in the Python plugin,
+ * so "install Python" suggestions are never offered on this platform
+ * (see the `getSdksToInstall` compat shim in `com.jetbrains.python.sdk`)
+ * and there is never anything to install here.
+ */
+internal fun installSdkIfSuggested(@Suppress("UNUSED_PARAMETER") sdk: Sdk?): PyDetectedSdk? = null
diff --git a/intellij-plugin/hs-Python/build.gradle.kts b/intellij-plugin/hs-Python/build.gradle.kts
index 16cd5d373..0890bc4b1 100644
--- a/intellij-plugin/hs-Python/build.gradle.kts
+++ b/intellij-plugin/hs-Python/build.gradle.kts
@@ -7,6 +7,7 @@ dependencies {
// needed to load `org.toml.lang plugin` for Python plugin in tests
val ideVersion = if (isRiderIDE) ideaVersion else baseVersion
intellijIde(ideVersion)
+ bundledModulesSince(ideVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(pythonPlugin)
testIntellijPlugins(tomlPlugin)
diff --git a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt
index 6583b3852..40483dcee 100644
--- a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt
+++ b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt
@@ -1,7 +1,6 @@
package org.hyperskill.academy.python.learning.newproject
import com.intellij.openapi.application.WriteAction
-import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
@@ -31,22 +30,12 @@ open class PyCourseProjectGenerator(
onConfigurationFinished: () -> Unit
) {
var sdk = projectSettings.sdk
- if (sdk is PySdkToInstall) {
- val selectedSdk = sdk
-
- @Suppress("UnstableApiUsage")
- val installedSdk = invokeAndWaitIfNeeded {
- selectedSdk.install(null) {
- detectSystemWideSdks(null, emptyList())
- }.getOrElse {
- LOG.warn(it)
- null
- }
- }
- if (installedSdk != null) {
- createAndAddVirtualEnv(project, projectSettings, installedSdk)
- sdk = projectSettings.sdk
- }
+ // Platform-dependent: installs Python first if the selected SDK is an "install Python" suggestion.
+ // See `installSdkIfSuggested` implementations in `branches//src`.
+ val installedSdk = installSdkIfSuggested(sdk)
+ if (installedSdk != null) {
+ createAndAddVirtualEnv(project, projectSettings, installedSdk)
+ sdk = projectSettings.sdk
}
else if (sdk is PySdkToCreateVirtualEnv) {
val homePath = sdk.homePath ?: error("Home path is not passed during fake python sdk creation")
diff --git a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt
index 95ec51443..2ddff1dbb 100644
--- a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt
+++ b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt
@@ -170,8 +170,9 @@ open class PyLanguageSettings : LanguageSettings() {
is PyDetectedSdk -> {
// PyDetectedSdk has empty `sdk.versionString`, so we should manually get language level from homePath if it exists
+ // `PythonSdkFlavor.getFlavor` is used instead of the `sdkFlavor` extension because the latter is internal since 262
homePath?.let {
- sdkFlavor.getLanguageLevel(it)
+ PythonSdkFlavor.getFlavor(this)?.getLanguageLevel(it)
} ?: LanguageLevel.getDefault()
}
diff --git a/intellij-plugin/hs-Rust/build.gradle.kts b/intellij-plugin/hs-Rust/build.gradle.kts
index 6a08a4ca4..6b670c449 100644
--- a/intellij-plugin/hs-Rust/build.gradle.kts
+++ b/intellij-plugin/hs-Rust/build.gradle.kts
@@ -6,6 +6,7 @@ dependencies {
intellijPlatform {
val ideVersion = if (!isIdeaIDE && !isClionIDE) ideaVersion else baseVersion
intellijIde(ideVersion)
+ bundledModulesSince(ideVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(rustPlugins)
}
diff --git a/intellij-plugin/hs-Scala/build.gradle.kts b/intellij-plugin/hs-Scala/build.gradle.kts
index 17403eb7b..fc6e251b1 100644
--- a/intellij-plugin/hs-Scala/build.gradle.kts
+++ b/intellij-plugin/hs-Scala/build.gradle.kts
@@ -5,6 +5,7 @@ plugins {
dependencies {
intellijPlatform {
intellijIde(ideaVersion)
+ bundledModulesSince(ideaVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(jvmPlugins)
intellijPlugins(scalaPlugin)
diff --git a/intellij-plugin/hs-core/branches/262/resources/META-INF/platform-modules.xml b/intellij-plugin/hs-core/branches/262/resources/META-INF/platform-modules.xml
new file mode 100644
index 000000000..fc67c8504
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/resources/META-INF/platform-modules.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt
new file mode 100644
index 000000000..65feb01b2
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt
@@ -0,0 +1,5 @@
+package org.hyperskill.academy.platform
+
+import com.intellij.openapi.vcs.merge.MergeDialogCustomizer
+
+abstract class MergeDialogCustomizerCompat : MergeDialogCustomizer()
diff --git a/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt
new file mode 100644
index 000000000..75c32e461
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt
@@ -0,0 +1,12 @@
+package org.hyperskill.academy.platform
+
+import com.intellij.diff.merge.MergeModelBase
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.project.Project
+
+abstract class MergeModelBaseCompat(
+ project: Project,
+ document: Document,
+) : MergeModelBase(project, document) {
+ override fun onChangeUpdated(index: Int) {}
+}
diff --git a/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt
new file mode 100644
index 000000000..28cc744d0
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt
@@ -0,0 +1,52 @@
+package org.hyperskill.academy.platform
+
+import com.intellij.ide.impl.OpenProjectTask
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.project.Project
+
+/**
+ * Compatibility helper to construct OpenProjectTask for 253+.
+ */
+object OpenProjectTaskCompat {
+
+ @JvmStatic
+ fun buildForOpen(
+ forceOpenInNewFrame: Boolean,
+ isNewProject: Boolean,
+ isProjectCreatedWithWizard: Boolean,
+ runConfigurators: Boolean,
+ projectName: String?,
+ projectToClose: Project?,
+ beforeInit: ((Project) -> Unit)? = null,
+ preparedToOpen: ((Project, Module) -> Unit)? = null
+ ): OpenProjectTask {
+ return OpenProjectTask(
+ forceOpenInNewFrame = forceOpenInNewFrame,
+ forceReuseFrame = false,
+ projectToClose = projectToClose,
+ isNewProject = isNewProject,
+ useDefaultProjectAsTemplate = true,
+ project = null,
+ projectName = projectName,
+ showWelcomeScreen = true,
+ callback = null,
+ line = -1,
+ column = -1,
+ isRefreshVfsNeeded = false,
+ runConfigurators = runConfigurators,
+ runConversionBeforeOpen = true,
+ projectWorkspaceId = null,
+ projectFrameTypeId = null,
+ isProjectCreatedWithWizard = isProjectCreatedWithWizard,
+ preloadServices = false,
+ beforeInit = if (beforeInit != null) { { beforeInit(it) } } else null,
+ beforeOpen = null,
+ preparedToOpen = if (preparedToOpen != null) { { preparedToOpen(it.project, it) } } else null,
+ preventIprLookup = false,
+ processorChooser = null,
+ implOptions = null,
+ projectRootDir = null,
+ createModule = true
+ )
+ }
+}
diff --git a/intellij-plugin/hs-core/branches/262/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt b/intellij-plugin/hs-core/branches/262/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt
new file mode 100644
index 000000000..56b22a27d
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt
@@ -0,0 +1,23 @@
+package org.hyperskill.academy.learning
+
+import com.intellij.ide.plugins.IdeaPluginDescriptorImpl
+import com.intellij.ide.plugins.PluginMainDescriptor
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+
+internal fun collectFromModules(
+ pluginDescriptor: IdeaPluginDescriptorImpl,
+ collect: (moduleDescriptor: IdeaPluginDescriptorImpl) -> Unit
+) {
+ // In 2025.3, contentModules is only available on PluginMainDescriptor
+ if (pluginDescriptor is PluginMainDescriptor) {
+ for (module in pluginDescriptor.contentModules) {
+ (module as? IdeaPluginDescriptorImpl)?.let { collect(it) }
+ }
+ }
+}
+
+// In 2025.3, ActionUtil.updateAction was removed. Use action.update(e) directly.
+internal fun updateAction(action: AnAction, e: AnActionEvent) {
+ action.update(e)
+}
diff --git a/intellij-plugin/hs-core/build.gradle.kts b/intellij-plugin/hs-core/build.gradle.kts
index 1124b71e2..2c2c383b2 100644
--- a/intellij-plugin/hs-core/build.gradle.kts
+++ b/intellij-plugin/hs-core/build.gradle.kts
@@ -21,6 +21,14 @@ dependencies {
intellijPlatform {
intellijIde(baseVersion)
bundledModules("intellij.platform.vcs.impl")
+ bundledModulesSince(
+ baseVersion, 262,
+ "intellij.platform.smRunner",
+ "intellij.platform.testRunner",
+ "intellij.platform.ui.jcef",
+ "intellij.libraries.jcef",
+ "intellij.platform.sqlite",
+ )
testIntellijPlugins(commonTestPlugins)
testIntellijPlatformFramework(project)
}
diff --git a/intellij-plugin/hs-core/resources/META-INF/hs-core.xml b/intellij-plugin/hs-core/resources/META-INF/hs-core.xml
index 1003f4ce2..4afcaa474 100644
--- a/intellij-plugin/hs-core/resources/META-INF/hs-core.xml
+++ b/intellij-plugin/hs-core/resources/META-INF/hs-core.xml
@@ -6,6 +6,19 @@
+
+ com.intellij.modules.jcef
+
+
+
+
+
+
messages.EduCoreBundle
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt
index 3387f761d..978134eee 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt
@@ -14,6 +14,17 @@ import org.hyperskill.academy.learning.stepik.StepikUser
import org.hyperskill.academy.learning.stepik.StepikUserInfo
import org.jdom.Element
+// Since 2026.2 JCEF is provided by the separate user-disableable `com.intellij.modules.jcef` plugin,
+// so its classes may be completely missing from the classpath
+fun isJCEFSupported(): Boolean {
+ return try {
+ JBCefApp.isSupported()
+ }
+ catch (e: LinkageError) {
+ false
+ }
+}
+
@State(name = "HsSettings", storages = [Storage("hyperskill.xml")])
class EduSettings : PersistentStateComponent {
@Transient
@@ -80,7 +91,7 @@ class EduSettings : PersistentStateComponent {
}
private fun initialJavaUiLibrary(): JavaUILibrary {
- return if (JBCefApp.isSupported()) {
+ return if (isJCEFSupported()) {
JavaUILibrary.JCEF
}
else {
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt
index 9499ad974..edb5755d4 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt
@@ -8,12 +8,12 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.dsl.builder.*
-import com.intellij.ui.jcef.JBCefApp
import org.hyperskill.academy.learning.EduSettings
import org.hyperskill.academy.learning.EduUtilsKt.isEduProject
import org.hyperskill.academy.learning.JavaUILibrary
import org.hyperskill.academy.learning.JavaUILibrary.JCEF
import org.hyperskill.academy.learning.JavaUILibrary.SWING
+import org.hyperskill.academy.learning.isJCEFSupported
import org.hyperskill.academy.learning.messages.EduCoreBundle
import org.hyperskill.academy.learning.taskToolWindow.ui.TaskToolWindowFactory
import org.hyperskill.academy.learning.taskToolWindow.ui.TaskToolWindowView
@@ -69,7 +69,7 @@ class SwitchTaskPanelAction : DumbAwareAction(EduCoreBundle.lazyMessage("action.
private fun collectAvailableJavaUiLibraries(): List {
val availableJavaUiLibraries = mutableListOf(SWING)
- if (JBCefApp.isSupported()) {
+ if (isJCEFSupported()) {
availableJavaUiLibraries += JCEF
}
return availableJavaUiLibraries
diff --git a/intellij-plugin/hs-jvm-core/build.gradle.kts b/intellij-plugin/hs-jvm-core/build.gradle.kts
index 7a8120ee9..d94f159e5 100644
--- a/intellij-plugin/hs-jvm-core/build.gradle.kts
+++ b/intellij-plugin/hs-jvm-core/build.gradle.kts
@@ -7,6 +7,7 @@ plugins {
dependencies {
intellijPlatform {
intellijIde(ideaVersion)
+ bundledModulesSince(ideaVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(jvmPlugins)
testIntellijPlatformFramework(project, TestFrameworkType.Plugin.Java)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 378bd9a76..a98920dbb 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,16 @@
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ maven("https://oss.sonatype.org/content/repositories/snapshots/")
+ }
+}
+
+plugins {
+ // Allows Gradle to automatically download JDKs required by toolchains (e.g. JDK 25 for platform 2026.2+)
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+
rootProject.name = "hyperskill-plugin"
include(
"hs-edu-format",
@@ -38,11 +51,3 @@ buildCache {
directory = File(rootDir, ".gradle/build-cache")
}
}
-
-pluginManagement {
- repositories {
- mavenCentral()
- gradlePluginPortal()
- maven("https://oss.sonatype.org/content/repositories/snapshots/")
- }
-}