diff --git a/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryManager.kt b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryManager.kt index 62165a7add1..99852c09809 100644 --- a/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryManager.kt +++ b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryManager.kt @@ -31,21 +31,33 @@ import com.intellij.openapi.roots.impl.OrderEntryUtil import com.intellij.openapi.roots.libraries.Library import com.intellij.openapi.roots.libraries.LibraryTable import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent +import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import com.intellij.util.FileContentUtilCore import com.intellij.util.PathUtil import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.idea.framework.KotlinJavaScriptLibraryDetectionUtil import org.jetbrains.kotlin.idea.project.ProjectStructureUtil.isJsKotlinModule import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils +import java.io.File +import java.util.* import java.util.concurrent.atomic.AtomicBoolean -public class KotlinJavaScriptLibraryManager private constructor(private var myProject: Project?) : ProjectComponent, ModuleRootListener { +public class KotlinJavaScriptLibraryManager private constructor(private var myProject: Project?) : ProjectComponent, ModuleRootListener, BulkFileListener { private val myMuted = AtomicBoolean(false) + private val libraryFileStampMap: MutableMap = Collections.synchronizedMap(hashMapOf()) + override fun getComponentName(): String = "KotlinJavascriptLibraryManager" override fun projectOpened() { val project = myProject!! project.messageBus.connect(project).subscribe(ProjectTopics.PROJECT_ROOTS, this) + project.messageBus.connect(project).subscribe(VirtualFileManager.VFS_CHANGES, this) DumbService.getInstance(project).smartInvokeLater() { updateProjectLibrary() } } @@ -59,6 +71,22 @@ public class KotlinJavaScriptLibraryManager private constructor(private var myPr myProject = null } + override fun after(events: List) { + if (myMuted.get()) return + + val changedFiles = events + .filter { it !is MyVFileContentChangeEvent && it is VFileContentChangeEvent } + .map { it.file } + .filterNotNull() + + val files = update(changedFiles, addToMapIfAbsent = false) + val application = ApplicationManager.getApplication() + refreshFiles(files, application.isUnitTestMode) + } + + override fun before(events: List) { + } + override fun beforeRootsChange(event: ModuleRootEvent) { } @@ -66,7 +94,7 @@ public class KotlinJavaScriptLibraryManager private constructor(private var myPr if (myMuted.get()) return ApplicationManager.getApplication().invokeLater(Runnable { - DumbService.getInstance(myProject!!).runWhenSmart() { updateProjectLibrary() } + DumbService.getInstance(myProject!!).runWhenSmart() { updateProjectLibrary() } }, ModalityState.NON_MODAL, myProject!!.disposed) } @@ -92,38 +120,42 @@ public class KotlinJavaScriptLibraryManager private constructor(private var myPr val clsRootUrls: MutableList = arrayListOf() val srcRootUrls: MutableList = arrayListOf() + val rootFiles: MutableList = arrayListOf() ModuleRootManager.getInstance(module).orderEntries().librariesOnly().forEachLibrary() { library -> - if (KotlinJavaScriptLibraryDetectionUtil.isKotlinJavaScriptLibrary(library)) { - var addSources = false - for (clsRootFile in library.getFiles(OrderRootType.CLASSES)) { - val path = PathUtil.getLocalPath(clsRootFile) - assert(path != null) { "expected not-null path for ${clsRootFile.name}" } + if (KotlinJavaScriptLibraryDetectionUtil.isKotlinJavaScriptLibrary(library)) { + var addSources = false + for (clsRootFile in library.getFiles(OrderRootType.CLASSES)) { + val path = PathUtil.getLocalPath(clsRootFile) + assert(path != null) { "expected not-null path for ${clsRootFile.name}" } - val metadataList = KotlinJavascriptMetadataUtils.loadMetadata(path!!) - if (metadataList.filter { !it.isAbiVersionCompatible }.isNotEmpty()) continue + val metadataList = KotlinJavascriptMetadataUtils.loadMetadata(path!!) + if (metadataList.filter { !it.isAbiVersionCompatible }.isNotEmpty()) continue - val classRoot = KotlinJavaScriptMetaFileSystem.getInstance().refreshAndFindFileByPath("$path!/") - classRoot?.let { - clsRootUrls.add(it.url) - addSources = true - } - } - if (addSources) { - srcRootUrls.addAll(library.getFiles(OrderRootType.SOURCES).map { it.url }) + VfsUtil.findFileByIoFile(File(path), true)?.let { rootFiles.add(it) } + val classRoot = KotlinJavaScriptMetaFileSystem.getInstance().refreshAndFindFileByPath("$path!/") + classRoot?.let { + clsRootUrls.add(it.url) + addSources = true } } - true + if (addSources) { + srcRootUrls.addAll(library.getFiles(OrderRootType.SOURCES).map { it.url }) + } } + true + } - val changesToApply = ChangesToApply(clsRootUrls, srcRootUrls) + val filesToRefresh = update(rootFiles, addToMapIfAbsent = true) + + val changesToApply = ChangesToApply(clsRootUrls, srcRootUrls, filesToRefresh) resetLibraries(module, changesToApply, LIBRARY_NAME, synchronously) } } private fun resetLibraries(module: Module, changesToApply: ChangesToApply, libraryName: String, synchronously: Boolean) = - applyChange(module, changesToApply, synchronously, libraryName) + applyChange(module, changesToApply, synchronously, libraryName) private fun applyChange(module: Module, changesToApply: ChangesToApply, synchronously: Boolean, libraryName: String) { if (synchronously) { @@ -148,10 +180,52 @@ public class KotlinJavaScriptLibraryManager private constructor(private var myPr } } + private fun update(changedFiles: List, addToMapIfAbsent: Boolean): List { + if (changedFiles.isEmpty()) return emptyList() + + val files = arrayListOf() + synchronized(libraryFileStampMap) { + changedFiles.forEach { file -> + PathUtil.getLocalPath(file)?.let { path -> + val timeStamp = libraryFileStampMap.get(path) + if (addToMapIfAbsent && timeStamp != file.timeStamp || + !addToMapIfAbsent && timeStamp != null) { + libraryFileStampMap[path] = file.timeStamp + files.add(file) + } + } + } + } + + return files + } + + private fun refreshFiles(files: List, synchronously: Boolean) { + val events = files.filter { !it.isDirectory && it.isValid }.map { MyVFileContentChangeEvent(it) } + if (events.isEmpty()) return + + val commitInWriteAction = Runnable { + ApplicationManager.getApplication().runWriteAction() { + val publisher = ApplicationManager.getApplication().messageBus.syncPublisher(VirtualFileManager.VFS_CHANGES) + publisher.before(events) + publisher.after(events) + } + } + + if (synchronously) { + commitInWriteAction.run() + } + else { + ApplicationManager.getApplication().invokeLater(commitInWriteAction, myProject!!.disposed) + } + } + @Synchronized private fun applyChangeImpl(module: Module, changesToApply: ChangesToApply, libraryName: String) { if (module.isDisposed) return + refreshFiles(changesToApply.filesToRefresh, synchronously = true) + val model = ModuleRootManager.getInstance(module).modifiableModel val libraryTableModel = model.moduleLibraryTable.modifiableModel @@ -212,7 +286,19 @@ public class KotlinJavaScriptLibraryManager private constructor(private var myPr private fun findLibraryByName(module: Module, libraryName: String) = OrderEntryUtil.findLibraryOrderEntry(ModuleRootManager.getInstance(module), libraryName)?.library - private class ChangesToApply(val clsUrlsToAdd: List = listOf(), val srcUrlsToAdd: List = listOf()) + private class ChangesToApply(val clsUrlsToAdd: List = listOf(), val srcUrlsToAdd: List = listOf(), val filesToRefresh: List = listOf()) + + private class MyVFileContentChangeEvent( + file: VirtualFile + ) : VFileContentChangeEvent( + FileContentUtilCore.FORCE_RELOAD_REQUESTOR, + file, + file.modificationStamp, + -1, /* oldModificationStamp */ + false /* isFromRefresh */ + ) { + override fun getPath(): String = PathUtil.getLocalPath(file.path + KotlinJavaScriptMetaFileSystem.ARCHIVE_SUFFIX) + } companion object { @@ -220,6 +306,6 @@ public class KotlinJavaScriptLibraryManager private constructor(private var myPr @JvmStatic public fun getInstance(project: Project): KotlinJavaScriptLibraryManager = - project.getComponent(KotlinJavaScriptLibraryManager::class.java)!! + project.getComponent(KotlinJavaScriptLibraryManager::class.java)!! } } diff --git a/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptMetaFileSystem.kt b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptMetaFileSystem.kt index fc1a0b7d343..395246613f7 100644 --- a/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptMetaFileSystem.kt +++ b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptMetaFileSystem.kt @@ -26,12 +26,12 @@ import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils public class KotlinJavaScriptMetaFileSystem : ArchiveFileSystem() { companion object { + val ARCHIVE_SUFFIX = ".kjsm_archive" + @JvmStatic public fun getInstance(): KotlinJavaScriptMetaFileSystem = VirtualFileManager.getInstance().getFileSystem(KotlinJavascriptMetadataUtils.VFS_PROTOCOL) as KotlinJavaScriptMetaFileSystem } - private val ARCHIVE_SUFFIX = ".kjsm_archive" - override fun getProtocol(): String = KotlinJavascriptMetadataUtils.VFS_PROTOCOL override fun extractRootPath(path: String): String { @@ -41,15 +41,15 @@ public class KotlinJavaScriptMetaFileSystem : ArchiveFileSystem() { } override fun getHandler(entryFile: VirtualFile): KotlinJavaScriptHandler { - val pathToRoot = extractLocalPath(this.extractRootPath(entryFile.path)) + val pathToRoot = extractLocalPath(this.extractRootPath(entryFile.path)).removeSuffix() return VfsImplUtil.getHandler(this, "$pathToRoot$ARCHIVE_SUFFIX") { - KotlinJavaScriptHandler(it.substringBeforeLast(ARCHIVE_SUFFIX)) + KotlinJavaScriptHandler(it.removeSuffix()) } } override fun extractLocalPath(rootPath: String): String = StringUtil.trimEnd(rootPath, JarFileSystem.JAR_SEPARATOR) - override fun composeRootPath(localPath: String): String = localPath + JarFileSystem.JAR_SEPARATOR + override fun composeRootPath(localPath: String): String = localPath.removeSuffix() + JarFileSystem.JAR_SEPARATOR override fun findFileByPath(path: String): VirtualFile? = VfsImplUtil.findFileByPath(this, path) @@ -58,4 +58,6 @@ public class KotlinJavaScriptMetaFileSystem : ArchiveFileSystem() { override fun refreshAndFindFileByPath(path: String): VirtualFile? = VfsImplUtil.refreshAndFindFileByPath(this, path) override fun refresh(asynchronous: Boolean) = VfsImplUtil.refresh(this, asynchronous) + + private fun String.removeSuffix() = substringBeforeLast(ARCHIVE_SUFFIX) } \ No newline at end of file