From 044497fab6154724364a5299b4cbcebaadadc3ce Mon Sep 17 00:00:00 2001 From: Michael Nedzelsky Date: Thu, 30 Apr 2015 16:00:06 +0300 Subject: [PATCH] JS: add idea-js module: KotlinJavaScriptLibraryManager and KotlinJavaScriptLibraryContentsTreeStructureProvider --- .idea/artifacts/KotlinPlugin.xml | 1 + .idea/modules.xml | 1 + idea/idea-js/idea-js.iml | 19 ++ .../kotlin/idea/js/KotlinJavaScriptHandler.kt | 70 ++++++ ...iptLibraryContentsTreeStructureProvider.kt | 36 +++ .../idea/js/KotlinJavaScriptLibraryManager.kt | 226 ++++++++++++++++++ .../idea/js/KotlinJavaScriptMetaFileSystem.kt | 62 +++++ idea/src/META-INF/plugin.xml | 10 + 8 files changed, 425 insertions(+) create mode 100644 idea/idea-js/idea-js.iml create mode 100644 idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptHandler.kt create mode 100644 idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryContentsTreeStructureProvider.kt create mode 100644 idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryManager.kt create mode 100644 idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptMetaFileSystem.kt diff --git a/.idea/artifacts/KotlinPlugin.xml b/.idea/artifacts/KotlinPlugin.xml index 7b638fed790..1a6fe4316df 100644 --- a/.idea/artifacts/KotlinPlugin.xml +++ b/.idea/artifacts/KotlinPlugin.xml @@ -43,6 +43,7 @@ + diff --git a/.idea/modules.xml b/.idea/modules.xml index 3a722f7d750..a52397b1f41 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -33,6 +33,7 @@ + diff --git a/idea/idea-js/idea-js.iml b/idea/idea-js/idea-js.iml new file mode 100644 index 00000000000..caae7ed606b --- /dev/null +++ b/idea/idea-js/idea-js.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptHandler.kt b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptHandler.kt new file mode 100644 index 00000000000..647ea99fa9c --- /dev/null +++ b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptHandler.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.js + +import com.intellij.openapi.vfs.impl.ArchiveHandler +import com.intellij.openapi.vfs.impl.ArchiveHandler.EntryInfo +import com.intellij.util.ArrayUtil +import gnu.trove.THashMap +import org.jetbrains.kotlin.serialization.js.forEachFile +import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils + +public open class KotlinJavaScriptHandler(path: String) : ArchiveHandler(path) { + + override fun createEntriesMap(): Map { + val map = THashMap() + map.put("", createRootEntry()) + + for(metadata in KotlinJavascriptMetadataUtils.loadMetadata(getFile())) { + val moduleName = metadata.moduleName + metadata.forEachFile { + filePath, fileContent -> getOrCreate(moduleName + "/" + filePath, false, map, fileContent) + } + } + + return map + } + + private fun getOrCreate( + entryPath: String, + isDirectory: Boolean, + map: MutableMap, + content: ByteArray = ArrayUtil.EMPTY_BYTE_ARRAY + ): EntryInfo { + val info = map[entryPath] + if (info != null) return info + + val path = splitPath(entryPath) + val parentInfo = getOrCreate(path.first, true, map) + val newInfo = JsMetaEntryInfo(parentInfo, path.second, isDirectory, content) + map[entryPath] = newInfo + + return newInfo + } + + override fun contentsToByteArray(relativePath: String): ByteArray { + val entryInfo = getEntryInfo(relativePath) + return if (entryInfo is JsMetaEntryInfo) entryInfo.content else ArrayUtil.EMPTY_BYTE_ARRAY + } + + private class JsMetaEntryInfo( + parent: EntryInfo, + shortName: String, + isDirectory: Boolean, + val content: ByteArray + ) : EntryInfo(parent, shortName, isDirectory, ArchiveHandler.DEFAULT_LENGTH, ArchiveHandler.DEFAULT_TIMESTAMP) +} \ No newline at end of file diff --git a/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryContentsTreeStructureProvider.kt b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryContentsTreeStructureProvider.kt new file mode 100644 index 00000000000..639ab9dd6ec --- /dev/null +++ b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryContentsTreeStructureProvider.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.js + +import com.intellij.ide.projectView.TreeStructureProvider +import com.intellij.ide.projectView.ViewSettings +import com.intellij.ide.projectView.impl.nodes.ExternalLibrariesNode +import com.intellij.ide.projectView.impl.nodes.NamedLibraryElementNode +import com.intellij.ide.util.treeView.AbstractTreeNode + +public class KotlinJavaScriptLibraryContentsTreeStructureProvider : TreeStructureProvider { + + override fun modify(parent: AbstractTreeNode<*>, children: Collection>, settings: ViewSettings): Collection> = + if (parent.getProject() == null || parent !is ExternalLibrariesNode) children else filterLibraryNodes(children) + + private fun filterLibraryNodes(children: Collection>): Collection> { + val filteredChildren = children.filterNot { it is NamedLibraryElementNode && KotlinJavaScriptLibraryManager.LIBRARY_NAME == it.getName() } + return if (filteredChildren.size() == children.size()) children else filteredChildren + } + + override fun getData(selected: MutableCollection>?, dataName: String?): Any? = null +} 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 new file mode 100644 index 00000000000..ffb9903dee5 --- /dev/null +++ b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptLibraryManager.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.js + +import com.intellij.ProjectTopics +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.components.ProjectComponent +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleType +import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.* +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.VirtualFile +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.LibraryUtils +import java.io.File +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.platform.platformStatic + +public class KotlinJavaScriptLibraryManager private constructor(private var myProject: Project?) : ProjectComponent, ModuleRootListener { + private val myMuted = AtomicBoolean(false) + + override fun getComponentName(): String = "KotlinJavascriptLibraryManager" + + override fun projectOpened() { + val project = myProject!! + project.getMessageBus().connect(project).subscribe(ProjectTopics.PROJECT_ROOTS, this) + DumbService.getInstance(project).smartInvokeLater() { updateProjectLibrary() } + } + + override fun projectClosed() { + } + + override fun initComponent() { + } + + override fun disposeComponent() { + myProject = null + } + + override fun beforeRootsChange(event: ModuleRootEvent) { + } + + override fun rootsChanged(event: ModuleRootEvent) { + if (myMuted.get()) return + + ApplicationManager.getApplication().invokeLater({ + DumbService.getInstance(myProject!!).runWhenSmart() { updateProjectLibrary() } + }, ModalityState.NON_MODAL, myProject!!.getDisposed()) + } + + TestOnly + public fun syncUpdateProjectLibrary(): Unit = updateProjectLibrary(true) + + /** + * @param synchronously may be true only in tests. + */ + private fun updateProjectLibrary(synchronously: Boolean = false) { + val project = myProject + if (project == null || project.isDisposed()) return + + ApplicationManager.getApplication().assertReadAccessAllowed() + + for (module in ModuleManager.getInstance(project).getModules()) { + if (!isModuleApplicable(module)) continue + + if (!isJsKotlinModule(module)) { + findLibraryByName(module, LIBRARY_NAME)?.let { resetLibraries(module, ChangesToApply(), LIBRARY_NAME, synchronously) } + continue + } + + val clsRootFiles = arrayListOf() + val srcRootFiles = 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.getName()}") + + if (LibraryUtils.isKotlinJavascriptLibraryWithMetadata(File(path))) { + val classRoot = KotlinJavaScriptMetaFileSystem.getInstance().refreshAndFindFileByPath(path!! + "!/") + clsRootFiles.add(classRoot) + addSources = true + } + } + if (addSources) { + srcRootFiles.addAll(arrayListOf(*library.getFiles(OrderRootType.SOURCES))) + } + } + true + } + + val clsRootUrls = clsRootFiles.map { it.getUrl() } + val srcRootUrls = srcRootFiles.map { it.getUrl() } + val changesToApply = ChangesToApply(clsRootUrls, srcRootUrls) + + resetLibraries(module, changesToApply, LIBRARY_NAME, synchronously) + } + } + + private fun resetLibraries(module: Module, changesToApply: ChangesToApply, libraryName: String, synchronously: Boolean) = + applyChange(module, changesToApply, synchronously, libraryName) + + private fun applyChange(module: Module, changesToApply: ChangesToApply, synchronously: Boolean, libraryName: String) { + if (synchronously) { + //for test only + val application = ApplicationManager.getApplication() + if (!application.isUnitTestMode()) { + throw IllegalStateException("Synchronous library update may be done only in test mode") + } + + val token = application.acquireWriteActionLock(javaClass()) + try { + applyChangeImpl(module, changesToApply, libraryName) + } + finally { + token.finish() + } + } + else { + val commit = Runnable { applyChangeImpl(module, changesToApply, libraryName) } + val commitInWriteAction = Runnable { ApplicationManager.getApplication().runWriteAction(commit) } + ApplicationManager.getApplication().invokeLater(commitInWriteAction, myProject!!.getDisposed()) + } + } + + synchronized private fun applyChangeImpl(module: Module, changesToApply: ChangesToApply, libraryName: String) { + if (module.isDisposed()) return + + val model = ModuleRootManager.getInstance(module).getModifiableModel() + val libraryTableModel = model.getModuleLibraryTable().getModifiableModel() + + var library = findLibraryByName(libraryTableModel, libraryName) + + if (library == null) { + if (changesToApply.clsUrlsToAdd.isEmpty()) { + model.dispose() + return + } + + library = libraryTableModel.createLibrary(libraryName)!! + } + + if (changesToApply.clsUrlsToAdd.isEmpty()) { + libraryTableModel.removeLibrary(library) + commitLibraries(null, libraryTableModel, model) + return + } + + val libraryModel = library.getModifiableModel() + + val existingClsUrls = library.getUrls(OrderRootType.CLASSES).toSet() + val existingSrcUrls = library.getUrls(OrderRootType.SOURCES).toSet() + + if (existingClsUrls == changesToApply.clsUrlsToAdd.toSet() && existingSrcUrls == changesToApply.srcUrlsToAdd.toSet()) { + model.dispose() + Disposer.dispose(libraryModel) + return + } + + existingClsUrls.forEach { libraryModel.removeRoot(it, OrderRootType.CLASSES) } + existingSrcUrls.forEach { libraryModel.removeRoot(it, OrderRootType.SOURCES) } + + changesToApply.clsUrlsToAdd.forEach { libraryModel.addRoot(it, OrderRootType.CLASSES) } + changesToApply.srcUrlsToAdd.forEach { libraryModel.addRoot(it, OrderRootType.SOURCES) } + + commitLibraries(libraryModel, libraryTableModel, model) + } + + private fun commitLibraries(libraryModel: Library.ModifiableModel?, tableModel: LibraryTable.ModifiableModel, model: ModifiableRootModel) { + try { + myMuted.set(true) + libraryModel?.commit() + tableModel.commit() + model.commit() + } + finally { + myMuted.set(false) + } + } + + private fun isModuleApplicable(module: Module) = ModuleTypeId.JAVA_MODULE == ModuleType.get(module).getId() + + private fun findLibraryByName(libraryTableModel: LibraryTable.ModifiableModel, libraryName: String) = + libraryTableModel.getLibraries().firstOrNull { libraryName == it.getName() } + + private fun findLibraryByName(module: Module, libraryName: String) = + OrderEntryUtil.findLibraryOrderEntry(ModuleRootManager.getInstance(module), libraryName)?.getLibrary() + + private class ChangesToApply(val clsUrlsToAdd: List = listOf(), val srcUrlsToAdd: List = listOf()) + + companion object { + + public val LIBRARY_NAME: String = "" + + platformStatic + public fun getInstance(project: Project): KotlinJavaScriptLibraryManager = + project.getComponent(javaClass()) + } +} 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 new file mode 100644 index 00000000000..accbaf4c62c --- /dev/null +++ b/idea/idea-js/src/org/jetbrains/kotlin/idea/js/KotlinJavaScriptMetaFileSystem.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.js + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem +import com.intellij.openapi.vfs.newvfs.VfsImplUtil +import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils +import kotlin.platform.platformStatic + +public class KotlinJavaScriptMetaFileSystem : ArchiveFileSystem() { + companion object { + platformStatic + 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 { + val jarSeparatorIndex = path.indexOf(JarFileSystem.JAR_SEPARATOR) + assert(jarSeparatorIndex >= 0) { "Path passed to KotlinJavascriptMetaFileSystem must have separator '!/': " + path } + return path.substring(0, jarSeparatorIndex + JarFileSystem.JAR_SEPARATOR.length()) + } + + override fun getHandler(entryFile: VirtualFile): KotlinJavaScriptHandler { + val pathToRoot = extractLocalPath(this.extractRootPath(entryFile.getPath())) + return VfsImplUtil.getHandler(this, pathToRoot + ARCHIVE_SUFFIX) { + KotlinJavaScriptHandler(it.substringBeforeLast(ARCHIVE_SUFFIX)) + } + } + + override fun extractLocalPath(rootPath: String): String = StringUtil.trimEnd(rootPath, JarFileSystem.JAR_SEPARATOR) + + override fun composeRootPath(localPath: String): String = localPath + JarFileSystem.JAR_SEPARATOR + + override fun findFileByPath(path: String): VirtualFile? = VfsImplUtil.findFileByPath(this, path) + + override fun findFileByPathIfCached(path: String): VirtualFile? = VfsImplUtil.findFileByPathIfCached(this, path) + + override fun refreshAndFindFileByPath(path: String): VirtualFile? = VfsImplUtil.refreshAndFindFileByPath(this, path) + + override fun refresh(asynchronous: Boolean) = VfsImplUtil.refresh(this, asynchronous) +} \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 8a2dc760437..530e8dd6f46 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -36,6 +36,9 @@ org.jetbrains.kotlin.idea.configuration.ui.AbsentJdkAnnotationsComponent + + org.jetbrains.kotlin.idea.js.KotlinJavaScriptLibraryManager + @@ -46,6 +49,12 @@ org.jetbrains.kotlin.idea.versions.KotlinUpdatePluginComponent + + + org.jetbrains.kotlin.idea.js.KotlinJavaScriptMetaFileSystem + org.jetbrains.kotlin.idea.js.KotlinJavaScriptMetaFileSystem + + @@ -995,6 +1004,7 @@ +