@@ -3,23 +3,18 @@ package org.hyperskill.academy.learning.framework.impl
33import com.intellij.openapi.Disposable
44import com.intellij.openapi.application.ApplicationManager
55import com.intellij.openapi.application.invokeAndWaitIfNeeded
6+ import com.intellij.openapi.application.runReadAction
67import com.intellij.openapi.application.runWriteAction
78import com.intellij.openapi.diagnostic.Logger
8- import com.intellij.openapi.editor.Document
99import com.intellij.openapi.fileEditor.FileDocumentManager
1010import com.intellij.openapi.fileEditor.FileDocumentManagerListener
1111import com.intellij.openapi.project.Project
12- import com.intellij.openapi.ui.Messages
1312import com.intellij.openapi.util.Disposer
1413import com.intellij.openapi.util.io.FileUtil
1514import com.intellij.openapi.vfs.VfsUtil
1615import com.intellij.openapi.vfs.VirtualFile
1716import com.intellij.util.SlowOperations
18- import com.intellij.util.io.storage.AbstractStorage
19- import org.hyperskill.academy.learning.Err
20- import org.hyperskill.academy.learning.Ok
21- import org.hyperskill.academy.learning.StudyTaskManager
22- import org.hyperskill.academy.learning.courseDir
17+ import org.hyperskill.academy.learning.*
2318import org.hyperskill.academy.learning.courseFormat.CheckStatus
2419import org.hyperskill.academy.learning.courseFormat.FrameworkLesson
2520import org.hyperskill.academy.learning.courseFormat.TaskFile
@@ -34,12 +29,10 @@ import org.hyperskill.academy.learning.framework.FrameworkStorageListener
3429import org.hyperskill.academy.learning.framework.propagateFilesOnNavigation
3530import org.hyperskill.academy.learning.framework.storage.Change
3631import org.hyperskill.academy.learning.framework.storage.FileEntry
37- import org.hyperskill.academy.learning.framework.ui.PropagationConflictDialog
3832import org.hyperskill.academy.learning.framework.storage.UserChanges
39- import org.hyperskill.academy.learning.messages.EduCoreBundle
33+ import org.hyperskill.academy.learning.framework.ui.PropagationConflictDialog
4034import org.hyperskill.academy.learning.stepik.PyCharmStepOptions
4135import org.hyperskill.academy.learning.stepik.hyperskill.api.HyperskillConnector
42- import org.hyperskill.academy.learning.toCourseInfoHolder
4336import org.hyperskill.academy.learning.ui.getUIName
4437import org.hyperskill.academy.learning.yaml.YamlFormatSynchronizer
4538import org.jetbrains.annotations.TestOnly
@@ -243,12 +236,10 @@ class FrameworkLessonManagerImpl(private val project: Project) : FrameworkLesson
243236 " The task is not a part of this lesson"
244237 }
245238
246- // For current task, read from disk
239+ // For current task, read from disk including user-created files
247240 if (lesson.currentTaskIndex + 1 == task.index) {
248241 val taskDir = task.getDir(project.courseDir) ? : return emptyMap()
249- val initialFiles = task.allFiles
250- val changes = getUserChangesFromFiles(initialFiles, taskDir)
251- return HashMap (initialFiles).apply { changes.apply (this ) }
242+ return getAllFilesFromTaskDir(taskDir, task)
252243 }
253244
254245 // For other tasks, read snapshot directly from storage
@@ -341,9 +332,8 @@ class FrameworkLessonManagerImpl(private val project: Project) : FrameworkLesson
341332 LOG .info(" Navigation refs: current=$currentRef (hasStorage=$currentHasStorage ), target=$targetRef (hasStorage=$targetHasStorage )" )
342333
343334 // 1. Get current disk state (what's currently on disk)
344- // Use template file keys to know which files to read
345- val templateFiles = originalTemplateFilesCache[currentTask.id] ? : currentTask.allFiles
346- val currentDiskState = getTaskStateFromFiles(templateFiles.keys, taskDir)
335+ // Read ALL files from disk, including user-created files
336+ val currentDiskState = getAllFilesFromTaskDir(taskDir, currentTask)
347337 val (currentPropagatableFiles, _) = currentDiskState.split(currentTask)
348338 logTiming(" readCurrentDiskState" )
349339
@@ -480,9 +470,8 @@ class FrameworkLessonManagerImpl(private val project: Project) : FrameworkLesson
480470 // - Target without storage (first visit to this stage)
481471 // - Navigation without merge (ancestor check passed, no Keep/Replace dialog)
482472 if (taskIndexDelta > 0 && ! mergeCommitCreated) {
483- // Read user files from disk, then build full snapshot with non-propagatable files
484- val userFileKeys = targetTask.allFiles.keys
485- val finalDiskState = getTaskStateFromFiles(userFileKeys, taskDir)
473+ // Read ALL files from disk, including user-created files
474+ val finalDiskState = getAllFilesFromTaskDir(taskDir, targetTask)
486475 val (finalPropagatableFiles, _) = finalDiskState.split(targetTask)
487476 val fullSnapshot = buildFullSnapshotState(targetTask, finalPropagatableFiles)
488477 logTiming(" buildFullSnapshotState(target)" )
@@ -1367,6 +1356,53 @@ class FrameworkLessonManagerImpl(private val project: Project) : FrameworkLesson
13671356 private val Task .allFilesIncludingTests: FLTaskState
13681357 get() = taskFiles.mapValues { it.value.contents.textualRepresentation }
13691358
1359+ /* *
1360+ * Reads ALL files from task directory, including user-created files.
1361+ * This is needed to capture user-created files that are not in the template.
1362+ *
1363+ * @param taskDir The task directory to read files from
1364+ * @param task The task (used to filter out test directories)
1365+ * @return Map of file paths to content
1366+ */
1367+ private fun getAllFilesFromTaskDir (taskDir : VirtualFile , task : Task ): FLTaskState {
1368+ val result = HashMap <String , String >()
1369+ val documentManager = FileDocumentManager .getInstance()
1370+ val testDirs = task.testDirs
1371+
1372+ // Recursively collect all files from task directory
1373+ fun collectFiles (dir : VirtualFile , pathPrefix : String = "") {
1374+ for (child in dir.children) {
1375+ val relativePath = if (pathPrefix.isEmpty()) child.name else " $pathPrefix /${child.name} "
1376+
1377+ if (child.isDirectory) {
1378+ // Skip test directories - they will be handled separately
1379+ val isTestDir = testDirs.any { testDir ->
1380+ relativePath == testDir || relativePath.startsWith(" $testDir /" )
1381+ }
1382+ if (! isTestDir) {
1383+ collectFiles(child, relativePath)
1384+ }
1385+ }
1386+ else {
1387+ // Read file content
1388+ val text = if (child.isToEncodeContent) {
1389+ child.loadEncodedContent(isToEncodeContent = true )
1390+ }
1391+ else {
1392+ runReadAction { documentManager.getDocument(child)?.text }
1393+ }
1394+
1395+ if (text != null ) {
1396+ result[relativePath] = text
1397+ }
1398+ }
1399+ }
1400+ }
1401+
1402+ collectFiles(taskDir)
1403+ return result
1404+ }
1405+
13701406 /* *
13711407 * Builds complete task state for snapshot: user files from disk + non-propagatable files from cache.
13721408 * Non-propagatable files (test files, hidden files) are taken from cache (not disk)
0 commit comments