diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 4615ac3fc1..548a34f72a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -17,6 +17,7 @@ jobs: environmentName: - 252 - 253 + - 261 fail-fast: false steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index 8c71613a50..8018e49b22 100644 --- a/.gitignore +++ b/.gitignore @@ -43,20 +43,7 @@ classes/ dependencies/ /helpers/venv/ - -# oauth properties files secret.properties -# old location of twitter properties -intellij-plugin/hs-Kotlin/resources/twitter/kotlin_koans/oauth_twitter.properties -intellij-plugin/hs-core/resources/twitter/oauth_twitter.properties -intellij-plugin/hs-core/resources/stepik/stepik.properties -intellij-plugin/hs-core/resources/hyperskill/hyperskill-oauth.properties -intellij-plugin/hs-core/resources/linkedin/linkedin-oauth.properties -intellij-plugin/hs-core/resources/marketplace/marketplace-oauth.properties -intellij-plugin/hs-core/resources/lti/lti-auth.properties -#AES key -hs-edu-format/resources/aes/aes.properties -intellij-plugin/hs-core/resources/aes/aes.properties # don't commit changes in hyperskill css intellij-plugin/hs-core/resources/style/hyperskill_task.css diff --git a/buildSrc/src/main/kotlin/intellij-plugin-module-conventions.gradle.kts b/buildSrc/src/main/kotlin/intellij-plugin-module-conventions.gradle.kts index 8112aec462..343b7a0ae1 100644 --- a/buildSrc/src/main/kotlin/intellij-plugin-module-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/intellij-plugin-module-conventions.gradle.kts @@ -1,5 +1,4 @@ import org.gradle.jvm.tasks.Jar -import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform plugins { @@ -66,6 +65,6 @@ dependencies { intellijPlatform { testIntellijPlugins(commonTestPlugins) - testFramework(TestFrameworkType.Bundled) + testIntellijPlatformFramework(project) } } diff --git a/buildSrc/src/main/kotlin/intellijUtils.kt b/buildSrc/src/main/kotlin/intellijUtils.kt index 3e180c066b..244462469b 100644 --- a/buildSrc/src/main/kotlin/intellijUtils.kt +++ b/buildSrc/src/main/kotlin/intellijUtils.kt @@ -1,6 +1,7 @@ import org.gradle.api.Project import org.gradle.process.JavaForkOptions import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.extensions.IntelliJPlatformDependenciesExtension import kotlin.reflect.KProperty @@ -14,7 +15,7 @@ private const val IDE_RIDER = "rider" val Project.environmentName: String by Properties val Project.pluginVersion: String by Properties -val Project.platformVersion: String get() = "20${StringBuilder(environmentName).insert(environmentName.length - 1, '.')}" +val Project.platformVersion: String get() = "20${StringBuilder(environmentName).insert(2, '.')}" val Project.baseIDE: String by Properties val Project.ideaVersion: String by Properties @@ -78,7 +79,7 @@ val Project.jvmPlugins: List get() = listOf( javaPlugin, "JUnit", - "org.jetbrains.plugins.gradle" + "com.intellij.gradle", ) val Project.javaScriptPlugins: List @@ -94,7 +95,11 @@ val Project.rustPlugins: List ) val Project.cppPlugins: List - get() = listOf( + 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", @@ -134,7 +139,10 @@ fun IntelliJPlatformDependenciesExtension.intellijIde(versionWithCode: String) { // Starting from 2025.3, IntelliJ IDEA Community and Ultimate are unified into a single distribution. // Use the new intellijIdea() helper which resolves the unified artifact. // See: https://blog.jetbrains.com/platform/2025/11/intellij-platform-2025-3-what-plugin-developers-should-know/ - if (type == IntelliJPlatformType.IntellijIdeaUltimate && version.startsWith("2025.3")) { + if (type == IntelliJPlatformType.IntellijIdeaUltimate + && !version.startsWith("2025.2") /* TODO remove with 252 */ + && !version.startsWith("252") + ) { intellijIdea(version) { useInstaller.set(false) useCache.set(true) @@ -177,6 +185,13 @@ fun IntelliJPlatformDependenciesExtension.testIntellijPlugins(notations: List - - \ No newline at end of file diff --git a/intellij-plugin/branches/253/resources/plugin_platform.xml b/intellij-plugin/branches/253/resources/plugin_platform.xml deleted file mode 100644 index 9f5fdc80bc..0000000000 --- a/intellij-plugin/branches/253/resources/plugin_platform.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/intellij-plugin/build.gradle.kts b/intellij-plugin/build.gradle.kts index 90ab965d7a..2ec5ba89d0 100644 --- a/intellij-plugin/build.gradle.kts +++ b/intellij-plugin/build.gradle.kts @@ -1,6 +1,5 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType.* -import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.extensions.IntelliJPlatformTestingExtension import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask @@ -94,7 +93,7 @@ dependencies { // hs-remote-env is excluded - doesn't compile with 2025.2+ pluginModule(implementation(project("hs-localization"))) - testFramework(TestFrameworkType.Bundled) + testIntellijPlatformFramework(project) testIntellijPlugins(commonTestPlugins) } diff --git a/intellij-plugin/hs-CSharp/branches/252/src/com/jetbrains/edu/csharp/compatibilityUtils.kt b/intellij-plugin/hs-CSharp/branches/252/src/com/jetbrains/edu/csharp/compatibilityUtils.kt index 9b75ad62fc..688ac33853 100644 --- a/intellij-plugin/hs-CSharp/branches/252/src/com/jetbrains/edu/csharp/compatibilityUtils.kt +++ b/intellij-plugin/hs-CSharp/branches/252/src/com/jetbrains/edu/csharp/compatibilityUtils.kt @@ -9,6 +9,8 @@ import com.jetbrains.rider.projectView.SolutionDescriptionFactory internal fun createExistingSolutionDescription(solutionPath: String) = SolutionDescriptionFactory.existing(solutionPath) +internal fun String.asRdPath(): String = this + /** * Helper class to hold test result data across platform versions. * In 252, this data comes from RdUnitTestResultData. diff --git a/intellij-plugin/hs-CSharp/branches/253/src/com/jetbrains/edu/csharp/compatibilityUtils.kt b/intellij-plugin/hs-CSharp/branches/253/src/org/hyperskill/academy/csharp/compatibilityUtils.kt similarity index 96% rename from intellij-plugin/hs-CSharp/branches/253/src/com/jetbrains/edu/csharp/compatibilityUtils.kt rename to intellij-plugin/hs-CSharp/branches/253/src/org/hyperskill/academy/csharp/compatibilityUtils.kt index 9e54aa81ae..d8e6a5e31f 100644 --- a/intellij-plugin/hs-CSharp/branches/253/src/com/jetbrains/edu/csharp/compatibilityUtils.kt +++ b/intellij-plugin/hs-CSharp/branches/253/src/org/hyperskill/academy/csharp/compatibilityUtils.kt @@ -8,6 +8,8 @@ import com.jetbrains.rider.projectView.SolutionDescriptionFactory internal fun createExistingSolutionDescription(solutionPath: String) = SolutionDescriptionFactory.existing(solutionPath, displayName = null) +internal fun String.asRdPath(): String = this + /** * Helper class to hold test result data across platform versions. * In 252, this data comes from RdUnitTestResultData. diff --git a/intellij-plugin/hs-CSharp/branches/261/src/org/hyperskill/academy/csharp/compatibilityUtils.kt b/intellij-plugin/hs-CSharp/branches/261/src/org/hyperskill/academy/csharp/compatibilityUtils.kt new file mode 100644 index 0000000000..5498763690 --- /dev/null +++ b/intellij-plugin/hs-CSharp/branches/261/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-CSharp/src/org/hyperskill/academy/csharp/CSharpBackendService.kt b/intellij-plugin/hs-CSharp/src/org/hyperskill/academy/csharp/CSharpBackendService.kt index 9144954f5b..c863c889b4 100644 --- a/intellij-plugin/hs-CSharp/src/org/hyperskill/academy/csharp/CSharpBackendService.kt +++ b/intellij-plugin/hs-CSharp/src/org/hyperskill/academy/csharp/CSharpBackendService.kt @@ -118,10 +118,14 @@ class CSharpBackendService(private val project: Project, private val scope: Coro when (request) { is CsprojRequest.Add -> { - val taskPaths = request.tasks.map { it.csProjPathByTask(project) } val parentId = project.getSolutionEntity()?.getId(project) ?: return + val projectFiles = request.tasks + .asSequence() + .map { it.csProjPathByTask(project) } + .map { it.asRdPath() } + .toList() val parameters = RdPostProcessParameters(false, listOf()) - val addCommand = AddProjectCommand(parentId, taskPaths, listOf(), true, parameters) + val addCommand = AddProjectCommand(parentId, projectFiles, listOf(), true, parameters) try { project.solution.projectModelTasks.addProject.runCommandUnderProgress( diff --git a/intellij-plugin/hs-Cpp/branches/252/main/checker/CppCatchEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/252/main/checker/CppCatchEduTaskChecker.kt new file mode 100644 index 0000000000..aa1e751d0d --- /dev/null +++ b/intellij-plugin/hs-Cpp/branches/252/main/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.cpp.execution.testing.tcatch.CMakeCatchTestRunConfigurationType +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 = + CMakeCatchTestRunConfigurationType.getInstance().getFactory() + } + } +} diff --git a/intellij-plugin/hs-Cpp/branches/252/main/checker/CppGEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/252/main/checker/CppGEduTaskChecker.kt new file mode 100644 index 0000000000..d2a93394c2 --- /dev/null +++ b/intellij-plugin/hs-Cpp/branches/252/main/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.cpp.execution.testing.google.CMakeGoogleTestRunConfigurationType +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 = + CMakeGoogleTestRunConfigurationType.getInstance().getFactory() + } + } +} diff --git a/intellij-plugin/hs-Cpp/branches/253/main/checker/CppCatchEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/253/main/checker/CppCatchEduTaskChecker.kt new file mode 100644 index 0000000000..aa1e751d0d --- /dev/null +++ b/intellij-plugin/hs-Cpp/branches/253/main/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.cpp.execution.testing.tcatch.CMakeCatchTestRunConfigurationType +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 = + CMakeCatchTestRunConfigurationType.getInstance().getFactory() + } + } +} diff --git a/intellij-plugin/hs-Cpp/branches/253/main/checker/CppGEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/253/main/checker/CppGEduTaskChecker.kt new file mode 100644 index 0000000000..d2a93394c2 --- /dev/null +++ b/intellij-plugin/hs-Cpp/branches/253/main/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.cpp.execution.testing.google.CMakeGoogleTestRunConfigurationType +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 = + CMakeGoogleTestRunConfigurationType.getInstance().getFactory() + } + } +} diff --git a/intellij-plugin/hs-Cpp/branches/261/src/org/hyperskill/academy/cpp/checker/CppCatchEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/261/src/org/hyperskill/academy/cpp/checker/CppCatchEduTaskChecker.kt new file mode 100644 index 0000000000..e0a335a142 --- /dev/null +++ b/intellij-plugin/hs-Cpp/branches/261/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/261/src/org/hyperskill/academy/cpp/checker/CppGEduTaskChecker.kt b/intellij-plugin/hs-Cpp/branches/261/src/org/hyperskill/academy/cpp/checker/CppGEduTaskChecker.kt new file mode 100644 index 0000000000..d5aaff1c4b --- /dev/null +++ b/intellij-plugin/hs-Cpp/branches/261/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/build.gradle.kts b/intellij-plugin/hs-Cpp/build.gradle.kts index 4b14124599..f4a6a1ef7d 100644 --- a/intellij-plugin/hs-Cpp/build.gradle.kts +++ b/intellij-plugin/hs-Cpp/build.gradle.kts @@ -13,9 +13,16 @@ dependencies { intellijIde(clionVersion) intellijPlugins(cppPlugins) + testIntellijPlatformFramework(project) } implementation(project(":intellij-plugin:hs-core")) testImplementation(project(":intellij-plugin:hs-core", "testOutput")) + + testImplementation(libs.junit) { + excludeKotlinDeps() + exclude("org.hamcrest") + } + testImplementationWithoutKotlin(libs.hamcrest) } diff --git a/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/CppConfigurators.kt b/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/CppConfigurator.kt similarity index 82% rename from intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/CppConfigurators.kt rename to intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/CppConfigurator.kt index a9b2e4aec6..474e513307 100644 --- a/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/CppConfigurators.kt +++ b/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/CppConfigurator.kt @@ -7,8 +7,6 @@ import org.hyperskill.academy.cpp.CMakeConstants.CMAKE_DIRECTORY import org.hyperskill.academy.cpp.CMakeConstants.CMAKE_GOOGLE_TEST import org.hyperskill.academy.cpp.CMakeConstants.CMAKE_GOOGLE_TEST_DOWNLOAD import org.hyperskill.academy.cpp.CMakeConstants.CMAKE_UTILS -import org.hyperskill.academy.cpp.checker.CppCatchTaskCheckerProvider -import org.hyperskill.academy.cpp.checker.CppGTaskCheckerProvider import org.hyperskill.academy.cpp.checker.CppTaskCheckerProvider import org.hyperskill.academy.learning.EduCourseBuilder import org.hyperskill.academy.learning.EduNames @@ -20,22 +18,6 @@ import org.hyperskill.academy.learning.courseFormat.Course import org.hyperskill.academy.learning.courseGeneration.GeneratorUtils.getInternalTemplateText import javax.swing.Icon -class CppGTestConfigurator : CppConfigurator() { - override val courseBuilder: EduCourseBuilder - get() = CppGTestCourseBuilder() - - override val taskCheckerProvider: TaskCheckerProvider - get() = CppGTaskCheckerProvider() -} - -class CppCatchConfigurator : CppConfigurator() { - override val courseBuilder: EduCourseBuilder - get() = CppCatchCourseBuilder() - - override val taskCheckerProvider: TaskCheckerProvider - get() = CppCatchTaskCheckerProvider() -} - open class CppConfigurator : EduConfigurator { override val courseBuilder: EduCourseBuilder get() = CppCourseBuilder() diff --git a/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppEduTaskChecker.kt b/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppEduTaskChecker.kt index d75a107850..bb32f162e0 100644 --- a/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppEduTaskChecker.kt +++ b/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppEduTaskChecker.kt @@ -8,8 +8,6 @@ import com.intellij.openapi.util.io.FileUtil import com.jetbrains.cidr.cpp.cmake.model.CMakeTarget import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace import com.jetbrains.cidr.cpp.execution.testing.CMakeTestRunConfiguration -import com.jetbrains.cidr.cpp.execution.testing.google.CMakeGoogleTestRunConfigurationType -import com.jetbrains.cidr.cpp.execution.testing.tcatch.CMakeCatchTestRunConfigurationType import com.jetbrains.cidr.execution.BuildTargetAndConfigurationData import com.jetbrains.cidr.execution.BuildTargetData import com.jetbrains.cidr.execution.ExecutableData @@ -57,11 +55,3 @@ open class CppEduTaskChecker(task: EduTask, envChecker: EnvironmentChecker, proj protected open fun getFactory(): ConfigurationFactory? = null } -class CppCatchEduTaskChecker(task: EduTask, envChecker: EnvironmentChecker, project: Project) : - CppEduTaskChecker(task, envChecker, project) { - override fun getFactory(): ConfigurationFactory = CMakeCatchTestRunConfigurationType.getInstance().factory -} - -class CppGEduTaskChecker(task: EduTask, envChecker: EnvironmentChecker, project: Project) : CppEduTaskChecker(task, envChecker, project) { - override fun getFactory(): ConfigurationFactory = CMakeGoogleTestRunConfigurationType.getInstance().factory -} diff --git a/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppTaskCheckerProvider.kt b/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppTaskCheckerProvider.kt index aef9004a0b..957fd47b77 100644 --- a/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppTaskCheckerProvider.kt +++ b/intellij-plugin/hs-Cpp/src/org/hyperskill/academy/cpp/checker/CppTaskCheckerProvider.kt @@ -13,11 +13,3 @@ open class CppTaskCheckerProvider : TaskCheckerProvider { // TODO implement envChecker validation override fun getEduTaskChecker(task: EduTask, project: Project): TaskChecker = CppEduTaskChecker(task, envChecker, project) } - -class CppGTaskCheckerProvider : CppTaskCheckerProvider() { - override fun getEduTaskChecker(task: EduTask, project: Project): TaskChecker = CppGEduTaskChecker(task, envChecker, project) -} - -class CppCatchTaskCheckerProvider : CppTaskCheckerProvider() { - override fun getEduTaskChecker(task: EduTask, project: Project): TaskChecker = CppCatchEduTaskChecker(task, envChecker, project) -} diff --git a/intellij-plugin/hs-Go/build.gradle.kts b/intellij-plugin/hs-Go/build.gradle.kts index 62c5dcb5de..f80139243a 100644 --- a/intellij-plugin/hs-Go/build.gradle.kts +++ b/intellij-plugin/hs-Go/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { intellijPlugins(goPlugin, intelliLangPlugin) // Workaround to make tests work - the module is not loaded automatically bundledModule("com.intellij.modules.ultimate") + testIntellijPlatformFramework(project) } implementation(project(":intellij-plugin:hs-core")) diff --git a/intellij-plugin/hs-Java/build.gradle.kts b/intellij-plugin/hs-Java/build.gradle.kts index ed73c388e0..70d0ed99af 100644 --- a/intellij-plugin/hs-Java/build.gradle.kts +++ b/intellij-plugin/hs-Java/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + plugins { id("intellij-plugin-module-conventions") } @@ -7,6 +9,7 @@ dependencies { intellijIde(ideaVersion) intellijPlugins(jvmPlugins) + testIntellijPlatformFramework(project, TestFrameworkType.Plugin.Java) } implementation(project(":intellij-plugin:hs-core")) diff --git a/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/packaging/PyTargetEnvCreationManager.java b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/packaging/PyTargetEnvCreationManager.java new file mode 100644 index 0000000000..6439ff70c0 --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/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/261/src/com/jetbrains/python/packaging/compat.kt b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/packaging/compat.kt new file mode 100644 index 0000000000..08caa4c45c --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/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/261/src/com/jetbrains/python/packaging/repository/compat.kt b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/packaging/repository/compat.kt new file mode 100644 index 0000000000..71b67e1b5a --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/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/261/src/com/jetbrains/python/sdk/add/compat.kt b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/sdk/add/compat.kt new file mode 100644 index 0000000000..b861d91795 --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/sdk/add/compat.kt @@ -0,0 +1,48 @@ +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.projectRoots.Sdk + +/** + * Copy-pasted from intellij sources. + * In 261 marked as deprecated to be removed. + * 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/261/src/com/jetbrains/python/sdk/compat.kt b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/sdk/compat.kt new file mode 100644 index 0000000000..f9461d1d72 --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/sdk/compat.kt @@ -0,0 +1,22 @@ +package com.jetbrains.python.sdk + +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +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) +} + +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/261/src/com/jetbrains/python/sdk/configuration/compat.kt b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/sdk/configuration/compat.kt new file mode 100644 index 0000000000..515b254cff --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/src/com/jetbrains/python/sdk/configuration/compat.kt @@ -0,0 +1,174 @@ +package com.jetbrains.python.sdk.configuration + +import com.intellij.execution.ExecutionException +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.writeAction +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.project.modules +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.util.io.FileUtil +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.intellij.remote.RemoteSdkException +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.jetbrains.python.Result +import com.jetbrains.python.packaging.PyPackageManagers +import com.jetbrains.python.packaging.PyTargetEnvCreationManager +import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory +import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.flavors.PyFlavorAndData +import com.jetbrains.python.sdk.flavors.PyFlavorData +import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor +import com.jetbrains.python.sdk.impl.PySdkBundle +import com.jetbrains.python.target.PyTargetAwareAdditionalData +import com.jetbrains.python.target.getInterpreterVersion +import com.jetbrains.python.target.ui.TargetPanelExtension +import com.jetbrains.python.ui.pyModalBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus + +/** + * Use [com.jetbrains.python.projectCreation.createVenvAndSdk] unless you need the Targets API. + * + * If you need venv only, please use [com.intellij.python.community.impl.venv.createVenv]: it is cleaner and suspend. + */ +@ApiStatus.Internal +@RequiresEdt +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 { + val targetEnvironmentConfiguration = baseSdk.targetEnvConfiguration + val installedSdk: Sdk = if (targetEnvironmentConfiguration == null) { + installSdkIfNeeded(baseSdk, module, existingSdks, context).getOrThrow() + } + else { + 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 associatedPath = if (!makeShared) projectPath else null + val venvSdk = targetEnvironmentConfiguration.let { + if (it == null) { + // here is the local machine case + createSdkByGenerateTask(task, existingSdks, installedSdk, associatedPath, null) + } + else { + val homePath = ProgressManager.getInstance().run(task) + runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") { + createSdkForTarget(project, it, homePath, existingSdks, targetPanelExtension) + } + } + } + + if (!makeShared) { + when { + module != null -> pyModalBlocking { venvSdk.setAssociationToModule(module) } + projectPath != null -> pyModalBlocking { venvSdk.setAssociationToPath(projectPath) } + } + } + + project?.excludeInnerVirtualEnv(venvSdk) + if (targetEnvironmentConfiguration == null) { + // The method `onVirtualEnvCreated(..)` stores preferred base path to virtual envs. Storing here the base path from the non-local + // target (e.g. a path from SSH machine or a Docker image) ends up with a meaningless default for the local machine. + // If we would like to store preferred paths for non-local targets we need to use some key to identify the exact target. + + object : Task.Backgroundable(project, "...") { + override fun run(progressIndicator: ProgressIndicator) { + PySdkSettings.instance.onVirtualEnvCreated( + sdkPath = installedSdk.homePath, + location = FileUtil.toSystemIndependentName(venvRoot), + projectPath = projectPath, + ) + } + } + } + + return venvSdk +} + +internal suspend fun createSdkForTarget( + project: Project?, + environmentConfiguration: TargetEnvironmentConfiguration, + interpreterPath: String, + existingSdks: Collection, + targetPanelExtension: TargetPanelExtension?, + sdkName: String? = null, +): Sdk = withContext(Dispatchers.IO) { + // TODO [targets] Should flavor be more flexible? + val data = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance())).also { + it.interpreterPath = interpreterPath + it.targetEnvironmentConfiguration = environmentConfiguration + targetPanelExtension?.applyToAdditionalData(it) + } + + val sdkVersion = when (val r = data.getInterpreterVersion()) { + is Result.Success -> r.result.toPythonVersion() + is Result.Failure -> throw RemoteSdkException(r.error.message) // TODO: Return error instead to show it to user + } + + val name = if (!sdkName.isNullOrEmpty()) { + sdkName + } + else { + PythonInterpreterTargetEnvironmentFactory.findDefaultSdkName(project, data, sdkVersion) + } + + val sdk = SdkConfigurationUtil.createSdk(existingSdks, interpreterPath, PythonSdkType.getInstance(), data, name) + if (PythonInterpreterTargetEnvironmentFactory.by(environmentConfiguration)?.needAssociateWithModule() == true) { + // FIXME: multi module project support + project?.modules?.firstOrNull()?.let { + sdk.setAssociationToModuleAsync(it) + } + } + + sdk.sdkModificator.let { modifiableSdk -> + modifiableSdk.versionString = sdkVersion + writeAction { + modifiableSdk.commitChanges() + } + } + + // FIXME: should we persist it? + data.isValid = true + + return@withContext sdk +} \ No newline at end of file diff --git a/intellij-plugin/hs-Python/branches/261/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt b/intellij-plugin/hs-Python/branches/261/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt new file mode 100644 index 0000000000..ff67adeadc --- /dev/null +++ b/intellij-plugin/hs-Python/branches/261/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt @@ -0,0 +1,55 @@ +package org.hyperskill.academy.python.learning + +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.intellij.platform.util.progress.SequentialProgressReporter +import com.jetbrains.python.packaging.PyRequirement +import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.toInstallRequest +import com.jetbrains.python.sdk.setAssociationToModule + +private val LOG = logger() + +// In 253, findPackageSpecificationWithVersionSpec was moved from PythonPackageManager +// to PythonRepositoryManager and renamed to findPackageSpecification +internal suspend fun installRequiredPackages( + reporter: SequentialProgressReporter, + packageManager: PythonPackageManager, + requirements: List +) { + for ((index, pyRequirement) in requirements.withIndex()) { + val progressText = "Installing ${pyRequirement.name} (${index + 1}/${requirements.size})" + reporter.itemStep(progressText) { + val packageSpecification = packageManager.repositoryManager.findPackageSpecification(pyRequirement) + if (packageSpecification != null) { + // Install the package - this does NOT throw exceptions on failure! + try { + LOG.warn("compatibilityUtils: Before installPackage ${pyRequirement.name}") + packageManager.installPackage(packageSpecification.toInstallRequest()) + LOG.warn("compatibilityUtils: After installPackage ${pyRequirement.name} - no exception thrown") + + // Since installPackage doesn't throw exceptions, we can't reliably detect failures here + // The Python plugin just logs warnings but returns normally + // We would need to check installed packages, but that property is protected + } catch (e: Throwable) { + LOG.warn("compatibilityUtils: Exception during installPackage: ${e.javaClass.name}: ${e.message}") + throw RuntimeException("Failed to install ${pyRequirement.name}: ${e.message}", e) + } + } else { + // Package not found in repository - might be URL or unsupported format + val errorMessage = "Cannot find package specification for requirement: ${pyRequirement.presentableText}. " + + "It might be a URL-based requirement which requires installation via pip install -r requirements.txt" + LOG.warn(errorMessage) + throw IllegalStateException(errorMessage) + } + } + } +} + +internal fun setAssociationToModule(sdk: Sdk, module: Module) { + runWithModalProgressBlocking(module.project, "") { + sdk.setAssociationToModule(module) + } +} diff --git a/intellij-plugin/hs-Python/build.gradle.kts b/intellij-plugin/hs-Python/build.gradle.kts index df9c6fd5d1..16cd5d3731 100644 --- a/intellij-plugin/hs-Python/build.gradle.kts +++ b/intellij-plugin/hs-Python/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { intellijPlugins(pythonPlugin) testIntellijPlugins(tomlPlugin) + testIntellijPlatformFramework(project) } implementation(project(":intellij-plugin:hs-core")) diff --git a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/PyEduUtils.kt b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/PyEduUtils.kt index 6f30d4a9af..6dcf1a3002 100644 --- a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/PyEduUtils.kt +++ b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/PyEduUtils.kt @@ -6,6 +6,7 @@ import com.intellij.codeHighlighting.Pass import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.util.ExecUtil +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runReadAction import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileEditor.FileEditorManager @@ -151,18 +152,16 @@ fun installRequiredPackages(project: Project, sdk: Sdk) { indicator.fraction = 1.0 indicator.text = "Installation completed" - // Clear file-level warning that might linger while skeletons are updating - val editorManager = FileEditorManager.getInstance(project) - for (module in ModuleManager.getInstance(project).modules) { - val analyzer = DaemonCodeAnalyzerEx.getInstanceEx(module.project) - if (editorManager.hasOpenFiles()) { - editorManager.openFiles.forEach { file -> - file.findPsiFile(project)?.let { psiFile -> - analyzer.cleanFileLevelHighlights(Pass.LOCAL_INSPECTIONS, psiFile) - } - } + ApplicationManager.getApplication().invokeLater({ + // Clear file-level warning that might linger while skeletons are updating + val editorManager = FileEditorManager.getInstance(project) + + val analyzer = DaemonCodeAnalyzerEx.getInstanceEx(project) + for (file in editorManager.openFiles) { + val psiFile = file.findPsiFile(project) ?: continue + analyzer.cleanFileLevelHighlights(Pass.LOCAL_INSPECTIONS, psiFile) } - } + }, project.disposed) // Installation completed successfully LOG.warn("PyEduUtils: Installation finished successfully") diff --git a/intellij-plugin/hs-core/branches/252/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt b/intellij-plugin/hs-core/branches/252/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt new file mode 100644 index 0000000000..65feb01b21 --- /dev/null +++ b/intellij-plugin/hs-core/branches/252/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/252/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt b/intellij-plugin/hs-core/branches/252/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt new file mode 100644 index 0000000000..7efcc420ec --- /dev/null +++ b/intellij-plugin/hs-core/branches/252/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 reinstallHighlighters(index: Int) {} +} diff --git a/intellij-plugin/hs-core/branches/253/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt b/intellij-plugin/hs-core/branches/253/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt new file mode 100644 index 0000000000..65feb01b21 --- /dev/null +++ b/intellij-plugin/hs-core/branches/253/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/253/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt b/intellij-plugin/hs-core/branches/253/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt new file mode 100644 index 0000000000..7efcc420ec --- /dev/null +++ b/intellij-plugin/hs-core/branches/253/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 reinstallHighlighters(index: Int) {} +} diff --git a/intellij-plugin/hs-core/branches/261/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt b/intellij-plugin/hs-core/branches/261/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt new file mode 100644 index 0000000000..65feb01b21 --- /dev/null +++ b/intellij-plugin/hs-core/branches/261/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/261/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt b/intellij-plugin/hs-core/branches/261/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt new file mode 100644 index 0000000000..75c32e461a --- /dev/null +++ b/intellij-plugin/hs-core/branches/261/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/261/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt b/intellij-plugin/hs-core/branches/261/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt new file mode 100644 index 0000000000..4cf33ed5df --- /dev/null +++ b/intellij-plugin/hs-core/branches/261/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt @@ -0,0 +1,34 @@ +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 { + this.forceOpenInNewFrame = forceOpenInNewFrame + this.isNewProject = isNewProject + this.isProjectCreatedWithWizard = isProjectCreatedWithWizard + this.runConfigurators = runConfigurators + this.projectName = projectName + this.projectToClose = projectToClose + if (beforeInit != null) this.beforeInit = { beforeInit(it) } + if (preparedToOpen != null) this.preparedToOpen = { preparedToOpen(it.project, it) } + } + } +} diff --git a/intellij-plugin/hs-core/branches/261/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt b/intellij-plugin/hs-core/branches/261/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt new file mode 100644 index 0000000000..56b22a27d3 --- /dev/null +++ b/intellij-plugin/hs-core/branches/261/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 47669dc779..1124b71e21 100644 --- a/intellij-plugin/hs-core/build.gradle.kts +++ b/intellij-plugin/hs-core/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.intellij.platform.gradle.TestFrameworkType import java.net.URI plugins { @@ -23,7 +22,7 @@ dependencies { intellijIde(baseVersion) bundledModules("intellij.platform.vcs.impl") testIntellijPlugins(commonTestPlugins) - testFramework(TestFrameworkType.Bundled) + testIntellijPlatformFramework(project) } api(project(":hs-edu-format")) @@ -46,6 +45,7 @@ dependencies { implementationWithoutKotlin(libs.converter.jackson) implementationWithoutKotlin(libs.kotlin.css.jvm) + testImplementationWithoutKotlin(libs.junit) testImplementationWithoutKotlin(libs.mockwebserver) testImplementationWithoutKotlin(libs.mockk) } diff --git a/intellij-plugin/hs-core/resources/hyperskill/oauth.properties b/intellij-plugin/hs-core/resources/hyperskill/oauth.properties new file mode 100644 index 0000000000..1fefd64c28 --- /dev/null +++ b/intellij-plugin/hs-core/resources/hyperskill/oauth.properties @@ -0,0 +1 @@ +hyperskillClientId=GKpYyTCgF2KRywYibxdPZ8wLSfVMAeJU38F1uG1Z diff --git a/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties b/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties index 41b83e6aa0..ca5a61a8ce 100644 --- a/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties +++ b/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties @@ -578,6 +578,7 @@ validation.plugins.required.plugins.three={0}, {1} and {2} plugins are required. # {0} and {1} stand for plugin names, {2} stands for the number of plugins validation.plugins.required.plugins.more={0}, {1} and {2} more plugins are required. validation.plugins.required.plugins.action=Install and Enable +validation.no.SDK=No SDK configured validation.stepik.log.in.needed=Log in to Stepik to synchronize your progress across multiple devices # {0} for path to file containing error diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/DiffConflictResolveStrategy.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/DiffConflictResolveStrategy.kt index c1a66e9756..8f3ebb9105 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/DiffConflictResolveStrategy.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/DiffConflictResolveStrategy.kt @@ -22,6 +22,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.LineTokenizer import org.hyperskill.academy.learning.computeUnderProgress import org.hyperskill.academy.learning.messages.EduCoreBundle +import org.hyperskill.academy.platform.MergeModelBaseCompat class DiffConflictResolveStrategy(private val project: Project) : FLConflictResolveStrategyBase(), Disposable { private val diffSettings = TextDiffSettingsHolder.TextDiffSettings().apply { @@ -183,13 +184,11 @@ class DiffConflictResolveStrategy(private val project: Project) : FLConflictReso project: Project, document: Document, private val initialRanges: List, - ) : MergeModelBase(project, document) { + ) : MergeModelBaseCompat(project, document) { init { setChanges(initialRanges) } - override fun reinstallHighlighters(index: Int) {} - override fun storeChangeState(index: Int): State { return State(index, initialRanges[index].start, initialRanges[index].end) } diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/FLMergeDialogCustomizer.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/FLMergeDialogCustomizer.kt index 9e2af5d66e..2ffd11a6b1 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/FLMergeDialogCustomizer.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/coursecreator/framework/diff/FLMergeDialogCustomizer.kt @@ -1,14 +1,14 @@ package org.hyperskill.academy.coursecreator.framework.diff import com.intellij.openapi.vcs.history.VcsRevisionNumber -import com.intellij.openapi.vcs.merge.MergeDialogCustomizer import com.intellij.openapi.vfs.VirtualFile import org.hyperskill.academy.learning.messages.EduCoreBundle +import org.hyperskill.academy.platform.MergeDialogCustomizerCompat class FLMergeDialogCustomizer( private val currentTaskName: String, private val targetTaskName: String, -) : MergeDialogCustomizer() { +) : MergeDialogCustomizerCompat() { override fun getColumnNames(): List { return listOf(currentTaskName, targetTaskName) } @@ -21,10 +21,6 @@ class FLMergeDialogCustomizer( return EduCoreBundle.message("action.HyperskillEducational.Educator.SyncChangesWithNextTasks.MergeDialog.MultipleFileDialog.title") } - override fun getMultipleFileMergeDescription(files: MutableCollection): String { - return EduCoreBundle.message("action.HyperskillEducational.Educator.SyncChangesWithNextTasks.MergeDialog.MultipleFileDialog.description") - } - override fun getLeftPanelTitle(file: VirtualFile): String { return EduCoreBundle.message( "action.HyperskillEducational.Educator.SyncChangesWithNextTasks.MergeDialog.LeftPanel.title", diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/ErrorState.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/ErrorState.kt index a91a60f021..fcdb07491e 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/ErrorState.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/ErrorState.kt @@ -28,7 +28,11 @@ sealed class ErrorState( object NothingSelected : ErrorState(OK, null, false) object None : ErrorState(OK, null, true) - object Pending : ErrorState(LANGUAGE_SETTINGS_PENDING, null, false) + object SDKPending : ErrorState( + severity = LANGUAGE_SETTINGS_PENDING, + message = ValidationMessage(EduCoreBundle.message("validation.no.SDK")), + courseCanBeStarted = false, + ) object NotLoggedIn : ErrorState( LOGIN_RECOMMENDED, diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/errorsUtil.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/errorsUtil.kt index ba9eb29fc3..0886c045af 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/errorsUtil.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/newproject/ui/errors/errorsUtil.kt @@ -9,7 +9,7 @@ fun getErrorState(course: Course?, validateSettings: (Course) -> SettingsValidat val validationResult = validateSettings(course) languageError = when (validationResult) { - is SettingsValidationResult.Pending -> ErrorState.Pending + is SettingsValidationResult.Pending -> ErrorState.SDKPending is SettingsValidationResult.Ready -> { val validationMessage = validationResult.validationMessage validationMessage?.let { ErrorState.LanguageSettingsError(it) } ?: ErrorState.None diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillOAuthBundle.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillOAuthBundle.kt index eac3f9ec53..0d2463f502 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillOAuthBundle.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillOAuthBundle.kt @@ -4,8 +4,7 @@ import org.hyperskill.academy.learning.messages.EduPropertiesBundle import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey -@NonNls -private const val BUNDLE_NAME = "hyperskill.hyperskill-oauth" +private const val BUNDLE_NAME: @NonNls String = "hyperskill.oauth" object HyperskillOAuthBundle : EduPropertiesBundle(BUNDLE_NAME) { fun value(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String): String { diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/api/HyperskillConnector.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/api/HyperskillConnector.kt index e1009bbd8c..6a9ef4d12e 100644 --- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/api/HyperskillConnector.kt +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/api/HyperskillConnector.kt @@ -56,7 +56,7 @@ abstract class HyperskillConnector : EduOAuthCodeFlowConnector() - private val CLIENT_ID: String = HyperskillOAuthBundle.value("hyperskillClientId") - fun getInstance(): HyperskillConnector = service() /** diff --git a/intellij-plugin/hs-features/ai-debugger-kotlin/src/org/hyperskill/academy/ai/debugger/kotlin/slice/FunctionDataDependency.kt b/intellij-plugin/hs-features/ai-debugger-kotlin/src/org/hyperskill/academy/ai/debugger/kotlin/slice/FunctionDataDependency.kt index ff31e475db..4df85fcce3 100644 --- a/intellij-plugin/hs-features/ai-debugger-kotlin/src/org/hyperskill/academy/ai/debugger/kotlin/slice/FunctionDataDependency.kt +++ b/intellij-plugin/hs-features/ai-debugger-kotlin/src/org/hyperskill/academy/ai/debugger/kotlin/slice/FunctionDataDependency.kt @@ -2,7 +2,7 @@ package org.hyperskill.academy.ai.debugger.kotlin.slice import com.intellij.psi.PsiElement import org.jetbrains.kotlin.idea.completion.reference -import org.jetbrains.kotlin.idea.intentions.branches +import org.jetbrains.kotlin.idea.codeinsight.utils.branches import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle diff --git a/intellij-plugin/hs-jvm-core/build.gradle.kts b/intellij-plugin/hs-jvm-core/build.gradle.kts index 06eae29981..7a8120ee97 100644 --- a/intellij-plugin/hs-jvm-core/build.gradle.kts +++ b/intellij-plugin/hs-jvm-core/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + plugins { id("intellij-plugin-module-conventions") } @@ -7,6 +9,7 @@ dependencies { intellijIde(ideaVersion) intellijPlugins(jvmPlugins) + testIntellijPlatformFramework(project, TestFrameworkType.Plugin.Java) } implementation(project(":intellij-plugin:hs-core")) diff --git a/intellij-plugin/hs-jvm-core/resources/messages/EduJVMBundle.properties b/intellij-plugin/hs-jvm-core/resources/messages/EduJVMBundle.properties index fdf3b936ab..edf17df038 100644 --- a/intellij-plugin/hs-jvm-core/resources/messages/EduJVMBundle.properties +++ b/intellij-plugin/hs-jvm-core/resources/messages/EduJVMBundle.properties @@ -14,3 +14,4 @@ error.unsupported.java.version=Unsupported Java version: {0} error.old.java=Your Java version is {1}, while it should be at least {0}. In the settings section, choose or download a newer version of JDK progress.resolving.suitable.jdk=Resolving suitable JDK progress.setting.suitable.jdk=Setting suitable JDK +progress.warming.suitable.jdk=Warming suitable JDK diff --git a/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/JdkLanguageSettings.kt b/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/JdkLanguageSettings.kt index 3c5d8db93d..efcdaad00a 100644 --- a/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/JdkLanguageSettings.kt +++ b/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/JdkLanguageSettings.kt @@ -145,9 +145,6 @@ open class JdkLanguageSettings : LanguageSettings() { jdkComboBox.isEnabled = false runInBackground(course.project, EduJVMBundle.message("progress.setting.suitable.jdk"), false) { - var errorOccurred = false - var errorMessage: String? = null - // Reset SDK model off-EDT to avoid IllegalStateException from synchronous progress on EDT try { val project = course.project @@ -156,8 +153,9 @@ open class JdkLanguageSettings : LanguageSettings() { } } catch (e: Throwable) { + loadingState = JdkLoadingState.FAILED // best-effort; if reset fails we'll try with whatever the model currently has - errorMessage = e.message + loadingError = e.message } // Add bundled JDK if needed (must be done off-EDT as addSdk requires write action) @@ -174,29 +172,24 @@ open class JdkLanguageSettings : LanguageSettings() { // Check if we found any JDK at all (either suitable or any) val anyJdkAvailable = sdksModel.sdks.any { it.sdkType == JavaSdk.getInstance() } - || ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance()).isNotEmpty() + || ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance()).isNotEmpty() - if (!anyJdkAvailable) { - errorOccurred = true - errorMessage = EduJVMBundle.message("error.no.jdk.available") - } + loadingState = if (anyJdkAvailable) JdkLoadingState.LOADED + else JdkLoadingState.FAILED - // Pre-warm SDK validation and VFS lookups off the EDT to avoid slow operations during UI rendering - prewarmSdkValidation(suitableJdk) + loadingError = if (anyJdkAvailable) null + else EduJVMBundle.message("error.no.jdk.available") + + runInBackground(course.project, EduJVMBundle.message("progress.warming.suitable.jdk"), false) { + // Pre-warm SDK validation and VFS lookups off the EDT to avoid slow operations during UI rendering + prewarmSdkValidation(suitableJdk) + } invokeLater(ModalityState.any()) { jdkComboBox.isEnabled = true jdkComboBox.selectedJdk = suitableJdk jdk = suitableJdk - if (errorOccurred) { - loadingState = JdkLoadingState.FAILED - loadingError = errorMessage - } - else { - loadingState = JdkLoadingState.LOADED - loadingError = null - } notifyListeners() } } diff --git a/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/gradle/generation/EduGradleUtils.kt b/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/gradle/generation/EduGradleUtils.kt index 5b97a6e6f9..a3ba69e101 100644 --- a/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/gradle/generation/EduGradleUtils.kt +++ b/intellij-plugin/hs-jvm-core/src/org/hyperskill/academy/jvm/gradle/generation/EduGradleUtils.kt @@ -101,7 +101,7 @@ object EduGradleUtils { // Fallback to original logic val projectSdkVersion = sdk.javaSdkVersion val internalSdkVersion = computeUnderProgress(project, EduJVMBundle.message("progress.resolving.suitable.jdk"), false) { - ExternalSystemJdkUtil.resolveJdkName(null, USE_INTERNAL_JAVA) + ExternalSystemJdkUtil.resolveJdkName(null as Sdk?, USE_INTERNAL_JAVA) }?.javaSdkVersion // Try to avoid incompatibility between Gradle and JDK versions diff --git a/intellij-plugin/resources/META-INF/plugin.xml b/intellij-plugin/resources/META-INF/plugin.xml index d54e25aa0d..5bbeed509e 100644 --- a/intellij-plugin/resources/META-INF/plugin.xml +++ b/intellij-plugin/resources/META-INF/plugin.xml @@ -5,7 +5,6 @@ JetBrains - diff --git a/settings.gradle.kts b/settings.gradle.kts index ddc7a456af..378bd9a763 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,3 @@ -import java.util.Properties - rootProject.name = "hyperskill-plugin" include( "hs-edu-format", @@ -31,59 +29,6 @@ include( ) // Note: hs-remote-env is excluded - doesn't compile with 2025.2+ - -val secretPropertiesFilename: String = "secret.properties" - -configureSecretProperties() - -fun configureSecretProperties() { - val secretProperties = file(secretPropertiesFilename) - if (!secretProperties.exists()) { - secretProperties.createNewFile() - } - - val properties = loadProperties(secretPropertiesFilename) - - properties.extractAndStore( - "intellij-plugin/hs-core/resources/stepik/stepik.properties", - "stepikClientId", - "cogniterraClientId", - ) - properties.extractAndStore( - "intellij-plugin/hs-core/resources/hyperskill/hyperskill-oauth.properties", - "hyperskillClientId", - ) - properties.extractAndStore( - "intellij-plugin/hs-core/resources/twitter/oauth_twitter.properties", - "xClientId" - ) - properties.extractAndStore( - "intellij-plugin/hs-core/resources/linkedin/linkedin-oauth.properties", - "linkedInClientId", - "linkedInClientSecret" - ) - properties.extractAndStore( - "hs-edu-format/resources/aes/aes.properties", - "aesKey" - ) -} - -fun loadProperties(path: String): Properties { - val properties = Properties() - file(path).bufferedReader().use { properties.load(it) } - return properties -} - -fun Properties.extractAndStore(path: String, vararg keys: String) { - val properties = Properties() - for (key in keys) { - properties[key] = getProperty(key) ?: "" - } - val file = file(path) - file.parentFile?.mkdirs() - file.bufferedWriter().use { properties.store(it, "") } -} - // Note: downloadHyperskillCss() was removed from settings.gradle.kts // CSS download is now done lazily in hs-core/build.gradle.kts processResources task