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 @@
+