diff --git a/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt index ec9db56..545a799 100644 --- a/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt +++ b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt @@ -4,6 +4,8 @@ import com.block.wt.progress.ProgressScope import com.block.wt.progress.asScope import com.block.wt.ui.Notifications import com.block.wt.util.PathHelper +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.ide.projectView.ProjectView import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.WriteAction @@ -15,9 +17,14 @@ 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.roots.ex.ProjectRootManagerEx +import com.intellij.openapi.vcs.ProjectLevelVcsManager import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager +import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtil -import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiManager +import com.intellij.ui.EditorNotifications import git4idea.repo.GitRepositoryManager import com.intellij.openapi.progress.runBlockingCancellable import kotlinx.coroutines.CoroutineScope @@ -78,37 +85,70 @@ class SymlinkSwitchService( } // Phase 3: VFS refresh (20%–45%) - // Re-resolve projectRoot AFTER symlink swap so VFS picks up the new target + // Poke VFS at all relevant paths so it re-reads the symlink entry and rescans the new target tree scope?.fraction(0.20) scope?.text("Refreshing file system...") indicator?.text = "Refreshing file system..." - val projectRoot = project.basePath?.let { VfsUtil.findFileByIoFile(java.io.File(it), true) } + val localFileSystem = LocalFileSystem.getInstance() + val refreshPaths = buildRefreshPaths(symlinkPath, newTarget, config.mainRepoRoot) + withContext(Dispatchers.IO) { + localFileSystem.refreshNioFiles(refreshPaths, false, true, null) + } + val projectRoot = localFileSystem.refreshAndFindFileByNioFile(symlinkPath) + ?: project.basePath?.let { localFileSystem.refreshAndFindFileByNioFile(Path.of(it)) } if (projectRoot != null) { - projectRoot.refresh(false, true) VfsUtil.markDirtyAndRefresh(false, true, true, projectRoot) + // Refresh .git so git4idea reads the new gitdir pointer in Phase 5 + projectRoot.findChild(".git")?.let { VfsUtil.markDirtyAndRefresh(false, true, true, it) } } - // Phase 4: Reload editors (45%–65%) - // Must happen AFTER VFS refresh so documents reload from new worktree content + // Phase 4: Reload editors and project state (45%–65%) + // Propagate VFS changes into documents, PSI, Project View, and daemon scope?.fraction(0.45) - scope?.text("Reloading editors...") - indicator?.text = "Reloading editors..." + scope?.text("Reloading editors and project state...") + indicator?.text = "Reloading editors and project state..." app.invokeAndWait({ + val fdm = FileDocumentManager.getInstance() + val psiDocManager = PsiDocumentManager.getInstance(project) + val psiManager = PsiManager.getInstance(project) + val openFiles = FileEditorManager.getInstance(project).openFiles + WriteAction.run { - val fdm = FileDocumentManager.getInstance() - for (openFile in FileEditorManager.getInstance(project).openFiles) { - // Force VFS to re-read this file's content from disk (not from cache) + for (openFile in openFiles) { openFile.refresh(false, false) + } + for (openFile in openFiles) { val doc = fdm.getCachedDocument(openFile) ?: continue fdm.reloadFromDisk(doc) + psiDocManager.getCachedPsiFile(doc)?.let(psiManager::reloadFromDisk) } } + + psiDocManager.commitAllDocuments() + if (openFiles.isNotEmpty()) { + psiDocManager.reparseFiles(openFiles.toList(), true) + } + psiManager.dropPsiCaches() + + // Fire synthetic roots-changed event so indexer and module system see the new tree + WriteAction.run { + ProjectRootManagerEx.getInstanceEx(project).makeRootsChange({}, false, true) + } + + ProjectView.getInstance(project).refresh() + EditorNotifications.getInstance(project).updateAllNotifications() + DaemonCodeAnalyzer.getInstance(project).restart("wt symlink switch") }, ModalityState.defaultModalityState()) // Phase 5: Git state (65%–85%) + // Re-set VCS mappings to force git4idea to re-read the .git file's new gitdir pointer scope?.fraction(0.65) scope?.text("Updating git state...") indicator?.text = "Updating git state..." + app.invokeAndWait({ + val vcsManager = ProjectLevelVcsManager.getInstance(project) + vcsManager.setDirectoryMappings(vcsManager.directoryMappings.toList()) + }, ModalityState.defaultModalityState()) withContext(Dispatchers.IO) { val repos = GitRepositoryManager.getInstance(project).repositories for (repo in repos) { @@ -116,6 +156,7 @@ class SymlinkSwitchService( } } VcsDirtyScopeManager.getInstance(project).markEverythingDirty() + projectRoot?.let { VcsDirtyScopeManager.getInstance(project).rootDirty(it) } // Phase 6: Refresh list (85%–100%) scope?.fraction(0.85) @@ -138,6 +179,25 @@ class SymlinkSwitchService( } } + private fun buildRefreshPaths( + symlinkPath: Path, + newTarget: Path, + mainRepoRoot: Path?, + ): Set = buildSet { + add(symlinkPath) + add(symlinkPath.resolve(".git")) + add(newTarget) + add(newTarget.resolve(".git")) + project.basePath?.let { basePath -> + add(Path.of(basePath)) + add(Path.of(basePath).resolve(".git")) + } + mainRepoRoot?.let { + add(it) + add(it.resolve(".git")) + } + } + companion object { fun getInstance(project: Project): SymlinkSwitchService = project.service() }