Skip to content

Commit e104e5c

Browse files
committed
Improve work with course config
1 parent 2d6ac21 commit e104e5c

2 files changed

Lines changed: 51 additions & 8 deletions

File tree

intellij-plugin/hs-core/src/org/hyperskill/academy/learning/InitializationListener.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.intellij.openapi.vfs.VfsUtil
1616
import com.intellij.util.PathUtil
1717
import org.hyperskill.academy.learning.authUtils.OAuthUtils.isBuiltinPortValid
1818
import org.hyperskill.academy.learning.courseFormat.Course
19+
import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillCourse
1920
import org.hyperskill.academy.learning.messages.EduCoreBundle
2021
import org.hyperskill.academy.learning.newproject.coursesStorage.CoursesStorage
2122
import org.hyperskill.academy.learning.notification.EduNotificationManager
@@ -103,21 +104,33 @@ class InitializationListener : AppLifecycleListener, DynamicPluginListener {
103104
val recentPathsInfo = state.additionalInfo
104105
recentPathsInfo.forEach {
105106
val projectPath = it.key
106-
val course = deserializeCourse(projectPath)
107+
val course = deserializeHyperskillCourse(projectPath)
107108
if (course != null) {
108109
// Note: we don't set course progress here, because we didn't load course items here
109110
CoursesStorage.getInstance().addCourse(course, projectPath)
110111
}
111112
}
112113
}
113114

114-
private fun deserializeCourse(projectPath: String): Course? {
115+
/**
116+
* Deserializes course from the given project path, returning only Hyperskill courses.
117+
* Non-Hyperskill courses are handled by other plugins (e.g., JetBrains Academy).
118+
*/
119+
private fun deserializeHyperskillCourse(projectPath: String): Course? {
115120
val projectFile = File(PathUtil.toSystemDependentName(projectPath))
116121
val projectDir = VfsUtil.findFile(projectFile.toPath(), true) ?: return null
117122
val courseConfig = projectDir.findChild(YamlConfigSettings.COURSE_CONFIG) ?: return null
118123
return runReadAction {
119-
ProgressManager.getInstance().computeInNonCancelableSection<Course, Exception> {
120-
YamlMapper.basicMapper().deserializeCourse(VfsUtil.loadText(courseConfig))
124+
ProgressManager.getInstance().computeInNonCancelableSection<Course?, Exception> {
125+
try {
126+
val course = YamlMapper.basicMapper().deserializeCourse(VfsUtil.loadText(courseConfig))
127+
// Only return Hyperskill courses, ignore other course types
128+
if (course is HyperskillCourse) course else null
129+
}
130+
catch (e: Exception) {
131+
// Ignore deserialization errors for non-Hyperskill courses
132+
null
133+
}
121134
}
122135
}
123136
}

intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlFormatSettings.kt

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package org.hyperskill.academy.learning.yaml
33
import com.intellij.openapi.application.ApplicationManager
44
import com.intellij.openapi.project.Project
55
import com.intellij.openapi.util.Key
6+
import com.intellij.openapi.vfs.VfsUtil
7+
import com.intellij.openapi.vfs.VirtualFile
68
import org.hyperskill.academy.learning.StudyTaskManager
79
import org.hyperskill.academy.learning.guessCourseDir
810
import org.hyperskill.academy.learning.isUnitTestMode
@@ -12,8 +14,11 @@ object YamlFormatSettings {
1214
private val YAML_IS_EDU_PROJECT: Key<Boolean> = Key.create("EDU.yaml_is_edu_project")
1315

1416
/**
15-
* Determines whether current project looks like an Edu YAML project by checking
16-
* if course config file exists in the project (course) root.
17+
* Determines whether current project is a Hyperskill course project by checking
18+
* if course config file exists and has type "hyperskill".
19+
*
20+
* Non-Hyperskill course types (like "coursera", "pycharm", etc.) are handled by
21+
* other plugins (e.g., JetBrains Academy plugin) and should be ignored here.
1722
*
1823
* IMPORTANT: Avoids VFS access on EDT. On UI thread, returns cached value if present
1924
* or schedules background computation and returns false as a conservative default.
@@ -26,7 +31,7 @@ object YamlFormatSettings {
2631
return if (app.isDispatchThread) {
2732
// Don't touch VFS on EDT. Compute in background and cache for subsequent calls.
2833
app.executeOnPooledThread {
29-
val result = guessCourseDir()?.findChild(COURSE_CONFIG) != null
34+
val result = isHyperskillCourseProject()
3035
putUserData(YAML_IS_EDU_PROJECT, result)
3136
if (result) {
3237
// Trigger StudyTaskManager initialization to load course once detection is ready
@@ -37,12 +42,37 @@ object YamlFormatSettings {
3742
false
3843
}
3944
else {
40-
val result = guessCourseDir()?.findChild(COURSE_CONFIG) != null
45+
val result = isHyperskillCourseProject()
4146
putUserData(YAML_IS_EDU_PROJECT, result)
4247
result
4348
}
4449
}
4550

51+
/**
52+
* Checks if the project contains a Hyperskill course by examining the course config file.
53+
* Returns true only for courses with type "hyperskill".
54+
*/
55+
private fun Project.isHyperskillCourseProject(): Boolean {
56+
val courseConfig = guessCourseDir()?.findChild(COURSE_CONFIG) ?: return false
57+
return isHyperskillCourseConfig(courseConfig)
58+
}
59+
60+
/**
61+
* Parses the course config file to check if it's a Hyperskill course (type: hyperskill).
62+
* Uses simple text parsing to avoid full YAML deserialization overhead.
63+
*/
64+
private fun isHyperskillCourseConfig(configFile: VirtualFile): Boolean {
65+
return try {
66+
val content = VfsUtil.loadText(configFile)
67+
// Look for "type: hyperskill" or "type:hyperskill" at the beginning of a line
68+
val typeRegex = """(?m)^type:\s*hyperskill\s*$""".toRegex()
69+
typeRegex.containsMatchIn(content)
70+
}
71+
catch (e: Exception) {
72+
false
73+
}
74+
}
75+
4676
// it is here because it's used in test and main code
4777
val YAML_TEST_PROJECT_READY = Key<Boolean>("EDU.yaml_test_project_ready")
4878
val YAML_TEST_THROW_EXCEPTION = Key<Boolean>("EDU.yaml_test_throw_exception")

0 commit comments

Comments
 (0)