diff --git a/experimental/intellij-plugin/build.gradle b/experimental/intellij-plugin/build.gradle index d4d695ebac..083a2e9dfe 100644 --- a/experimental/intellij-plugin/build.gradle +++ b/experimental/intellij-plugin/build.gradle @@ -25,6 +25,11 @@ intellij { buildDir = "$projectDir/build" } +dependencies { + // For JSON + compile "com.fasterxml.jackson.core:jackson-databind:2.8.5" +} + group 'net.corda' version '0.1.0' // Plugin version diff --git a/experimental/intellij-plugin/readme.md b/experimental/intellij-plugin/readme.md index 24318e053d..a8650ab596 100644 --- a/experimental/intellij-plugin/readme.md +++ b/experimental/intellij-plugin/readme.md @@ -13,6 +13,11 @@ Corda Intellij Plugin is a plugin for Intellij Idea IDE which aid Corda applicat ###Running the project You can run or debug the project using provided Intellij Run Configuration `CordaPlugin` or by using the gradle command `./gradlew runIde` IDE's log file is located in `build/idea-sandbox/system/log/idea.log` + + ###Corda Flow Tool + After the IntelliJ (with the Corda plugin) is started, go to View/Tool Windows in the menu bar and in the list of + available tools you should see the 'Corda Flow Tool'. In the tool window, point to the flow snapshot location + (or node's base directory) and you will be able to see and examine all available flow snapshots. ## TODOs * Create a higher quality Corda icon. diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleBuilder.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleBuilder.kt similarity index 99% rename from experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleBuilder.kt rename to experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleBuilder.kt index caa8d0d10d..f03520d569 100644 --- a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleBuilder.kt +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleBuilder.kt @@ -1,4 +1,4 @@ -package net.corda.ideaPlugin.module +package net.corda.ideaplugin.module import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.projectWizard.ProjectSettingsStep diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleType.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleType.kt similarity index 66% rename from experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleType.kt rename to experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleType.kt index e5c053948c..386b9475f7 100644 --- a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleType.kt +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleType.kt @@ -1,4 +1,4 @@ -package net.corda.ideaPlugin.module +package net.corda.ideaplugin.module import com.intellij.openapi.module.ModuleType import com.intellij.openapi.util.IconLoader @@ -6,13 +6,14 @@ import com.intellij.openapi.util.IconLoader class CordaModuleType : ModuleType(ID) { companion object { val ID = "CORDA_MODULE" - val CORDA_ICON = IconLoader.getIcon("/images/corda-icon.png") + val CORDA_ICON = IconLoader.getIcon("/images/corda-module-icon.png") + val CORDA_ICON_BIG = IconLoader.getIcon("/images/corda-module-icon@2x.png") val instance = CordaModuleType() } override fun createModuleBuilder() = CordaModuleBuilder() override fun getNodeIcon(p0: Boolean) = CORDA_ICON - override fun getBigIcon() = CORDA_ICON + override fun getBigIcon() = CORDA_ICON_BIG override fun getName() = "CorDapp" override fun getDescription() = "Corda DLT Platform Application" } diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleWizardStep.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleWizardStep.kt similarity index 99% rename from experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleWizardStep.kt rename to experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleWizardStep.kt index 74ed8de456..09b9570273 100644 --- a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaModuleWizardStep.kt +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaModuleWizardStep.kt @@ -1,4 +1,4 @@ -package net.corda.ideaPlugin.module +package net.corda.ideaplugin.module import com.intellij.ide.util.projectWizard.ModuleWizardStep import com.intellij.openapi.options.ConfigurationException diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaSdkVersionComboBox.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaSdkVersionComboBox.kt similarity index 96% rename from experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaSdkVersionComboBox.kt rename to experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaSdkVersionComboBox.kt index ee3f87d28d..28f6559aab 100644 --- a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaSdkVersionComboBox.kt +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaSdkVersionComboBox.kt @@ -1,4 +1,4 @@ -package net.corda.ideaPlugin.module +package net.corda.ideaplugin.module import com.intellij.openapi.util.Version import com.intellij.ui.ColoredListCellRenderer diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaTemplateProvider.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaTemplateProvider.kt similarity index 97% rename from experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaTemplateProvider.kt rename to experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaTemplateProvider.kt index 6be58df10a..6311ddba51 100644 --- a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaPlugin/module/CordaTemplateProvider.kt +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/module/CordaTemplateProvider.kt @@ -1,4 +1,4 @@ -package net.corda.ideaPlugin.module +package net.corda.ideaplugin.module object CordaTemplateProvider { fun getTemplateFiles(cordaTemplate: CordaTemplate, language: Language): List { diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/CordaFlowToolWindow.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/CordaFlowToolWindow.kt new file mode 100644 index 0000000000..d87eddcd14 --- /dev/null +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/CordaFlowToolWindow.kt @@ -0,0 +1,212 @@ +package net.corda.ideaplugin.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.content.Content +import com.intellij.ui.content.ContentFactory +import com.intellij.ui.treeStructure.Tree +import net.corda.ideaplugin.module.CordaModuleType +import java.awt.* +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.io.File +import javax.swing.* +import javax.swing.tree.* + +/** + * GUI class for the Corda Flow tool + */ +class CordaFlowToolWindow : ToolWindowFactory { + // Left-hand side tree view + private val flowTree = Tree() + // Right-hand side tree view + private val snapshotTree = Tree() + // Main panel + private val panel = JPanel(GridBagLayout()) + private val textField = JTextField() + private val browseButton = JButton() + private val refreshButton = JButton() + + private val snapshotDataManager = FlowSnapshotTreeDataManager(snapshotTree) + private val flowDataManager = FlowTreeDataManager(flowTree, snapshotDataManager) + + init { + setUpSnapshotTree() + setUpFlowTree() + setUpBrowseButton() + setUpRefreshButton() + setUpTextField() + + // Laying-out left-hand side panel + val flowPanel = JPanel(BorderLayout(3, 3)) + flowPanel.add(BorderLayout.NORTH, JLabel("Flows")) + val flowScrollPane = JBScrollPane(flowTree) + flowScrollPane.border = BorderFactory.createEmptyBorder() + flowPanel.add(BorderLayout.CENTER, flowScrollPane) + + // Laying-out right-hand side panel + val snapshotPanel = JPanel(BorderLayout(3, 3)) + snapshotPanel.add(BorderLayout.NORTH, JLabel("Snapshots")) + val snapshotScrollPane = JBScrollPane(snapshotTree) + snapshotScrollPane.border = BorderFactory.createEmptyBorder() + snapshotPanel.add(BorderLayout.CENTER, snapshotScrollPane) + + // Horizontal divider + val splitPane = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, flowPanel, snapshotPanel) + splitPane.dividerSize = 2 + splitPane.resizeWeight = 0.5 + splitPane.border = BorderFactory.createEmptyBorder() + + // Container for the text field and buttons + val topPanel = JPanel() + topPanel.layout = BoxLayout(topPanel, BoxLayout.LINE_AXIS) + topPanel.add(textField) + topPanel.add(browseButton) + topPanel.add(refreshButton) + + GridBagConstraints().apply { + gridwidth = 1 + weightx = 0.05 + weighty = 0.05 + fill = GridBagConstraints.HORIZONTAL // Fill cell in both direction + insets = Insets(3, 3, 3, 3) + gridy = 0 + panel.add(topPanel, this) + } + GridBagConstraints().apply { + gridwidth = 1 + weightx = 1.0 // Cell takes up all extra horizontal space + weighty = 1.0 // Cell takes up all extra vertical space + fill = GridBagConstraints.BOTH // Fill cell in both direction + insets = Insets(3, 3, 3, 3) + gridy = 1 + panel.add(splitPane, this) + } + } + + // Create the tool window content. + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val contentFactory: ContentFactory = ContentFactory.SERVICE.getInstance() + val content: Content = contentFactory.createContent(panel, "", false) + toolWindow.contentManager.addContent(content) + } + + private fun setUpSnapshotTree() { + snapshotTree.isRootVisible = false + snapshotTree.cellRenderer = SnapshotTreeRenderer() + } + + private fun setUpFlowTree() { + flowTree.isRootVisible = false + flowTree.selectionModel = LeafOnlyTreeSelectionModel() + flowTree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION + flowTree.cellRenderer = FlowTreeRenderer() + flowTree.addTreeSelectionListener { + val node = flowTree.lastSelectedPathComponent + if (node is DefaultMutableTreeNode) { + val dir = node.userObject as File + if (dir.exists()) { + snapshotDataManager.loadSnapshots(dir.listFiles().filter { + !it.isDirectory && it.name.startsWith(FlowSnapshotTreeDataManager.SNAPSHOT_FILE_PREFIX) + }) + } + } else { + snapshotDataManager.clear() + } + } + } + + private fun setUpBrowseButton() { + browseButton.icon = AllIcons.General.Ellipsis + browseButton.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent?) { + val chooser = JFileChooser() + chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + val chooserValue = chooser.showOpenDialog(null) + if (chooserValue == JFileChooser.APPROVE_OPTION) { + var selectedFile = chooser.selectedFile + if (selectedFile.name != FlowTreeDataManager.FLOWS_DIRECTORY) { + val subDirectory = File(selectedFile, FlowTreeDataManager.FLOWS_DIRECTORY) + if (subDirectory.exists()) { + selectedFile = subDirectory + } else { + textField.text = "Could not find the ${FlowTreeDataManager.FLOWS_DIRECTORY} directory" + clear() + return + } + } + textField.text = selectedFile.canonicalPath + flowDataManager.loadFlows(selectedFile) + } + } + }) + } + + private fun setUpRefreshButton() { + refreshButton.icon = AllIcons.Actions.Refresh + refreshButton.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent?) { + clear() + flowDataManager.loadFlows() + } + }) + } + + private fun clear() { + flowTree.clearSelection() + flowDataManager.clear() + snapshotTree.clearSelection() + snapshotDataManager.clear() + } + + private fun setUpTextField() { + textField.isEditable = false + textField.isFocusable = false + textField.background = Color(0, 0, 0, 0) + textField.text = "Choose flow data location (i.e. node base directory)" + } + + private class FlowTreeRenderer : DefaultTreeCellRenderer() { + override fun getTreeCellRendererComponent(tree: JTree?, value: Any?, sel: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean): Component { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus) + val node = value as DefaultMutableTreeNode + if (node.isLeaf) { + icon = AllIcons.Debugger.Frame + } + val userObject = node.userObject + if (userObject is File) { + text = userObject.name + } + return this + } + } + + private class SnapshotTreeRenderer : DefaultTreeCellRenderer() { + override fun getTreeCellRendererComponent(tree: JTree?, value: Any?, sel: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean): Component { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus) + val descriptor = (value as DefaultMutableTreeNode).userObject as SnapshotDataDescriptor + icon = descriptor.icon + if (!descriptor.key.isNullOrEmpty()) { + text = "${descriptor.key}: ${descriptor.data.toString()}" + } + return this + } + } + + private class LeafOnlyTreeSelectionModel : DefaultTreeSelectionModel() { + override fun addSelectionPaths(paths: Array?) { + super.addSelectionPaths(filterPaths(paths)) + } + + override fun setSelectionPaths(paths: Array?) { + super.setSelectionPaths(filterPaths(paths)) + } + + private fun filterPaths(paths: Array?): Array? { + return paths?.filter { (it.lastPathComponent as DefaultMutableTreeNode).isLeaf }?.toTypedArray() + } + } +} \ No newline at end of file diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt new file mode 100644 index 0000000000..90953d01ce --- /dev/null +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt @@ -0,0 +1,117 @@ +package net.corda.ideaplugin.toolwindow + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.intellij.icons.AllIcons +import java.io.File +import javax.swing.Icon +import javax.swing.JTree +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel +import javax.swing.tree.MutableTreeNode + +/** + * Snapshot tree data descriptor. It is used as userObject in the [DefaultMutableTreeNode] class. + */ +class SnapshotDataDescriptor(val data: Any?, val icon: Icon, val key: String? = null) { + override fun toString(): String = data?.toString() ?: "null" +} + +/** + * Manager class for flow snapshots. It is responsible for parsing data read from a snapshot file and constructing + * tree model from it. + */ +class FlowSnapshotTreeDataManager(tree: JTree) { + companion object { + const val SNAPSHOT_FILE_PREFIX = "flowStackSnapshot" + } + + // Root node for the snapshot hierarchy, which is an empty node. + private val root = DefaultMutableTreeNode(SnapshotDataDescriptor(null, AllIcons.Json.Object)) + // Snapshot tree model + private val snapshotModel = DefaultTreeModel(root) + + private val mapper = ObjectMapper() + + init { + tree.model = snapshotModel + snapshotModel.reload() + } + + /** + * Removes all current data from the snapshot hierarchy and refreshes the model. + */ + fun clear() { + root.removeAllChildren() + snapshotModel.reload() + } + + /** + * Constructs tree model from snapshot files + */ + fun loadSnapshots(snapshots: List) { + root.removeAllChildren() + snapshots.forEach { + insertNodeToSnapshotModel(it) + } + snapshotModel.reload() + } + + /** + * Adds snapshot file to the snapshot hierarchy. The content of the file is processed and + * the model is updated accordingly. + */ + fun addNodeToSnapshotModel(snapshotFile: File) { + val insertionIndex = -(root.childNodes().map { + (it.userObject as SnapshotDataDescriptor).key + }.binarySearch(extractFileName(snapshotFile))) - 1 + insertNodeToSnapshotModel(snapshotFile, insertionIndex) + } + + /** + * Removes the snapshot file from the snapshot hierarchy. The model is also updated after this operation. + */ + fun removeNodeFromSnapshotModel(snapshotFile: File) { + val node = root.childNodes().find { + (it.userObject as SnapshotDataDescriptor).data == extractFileName(snapshotFile) + } as MutableTreeNode? + if (node != null) { + snapshotModel.removeNodeFromParent(node) + snapshotModel.nodesWereRemoved(root, intArrayOf(), arrayOf(node)) + return + } + } + + private fun insertNodeToSnapshotModel(snapshotFile: File, insertionIndex: Int = -1) { + buildChildrenModel(mapper.readTree(snapshotFile), root, extractFileName(snapshotFile), insertionIndex) + } + + private fun extractFileName(file: File): String { + return file.name.substring(0, file.name.lastIndexOf(".")) + } + + private fun buildChildrenModel( + node: JsonNode?, + parent: DefaultMutableTreeNode, key: String? = null, + insertionIndex: Int = -1) { + val child: DefaultMutableTreeNode + if (node == null || !node.isContainerNode) { + child = DefaultMutableTreeNode(SnapshotDataDescriptor(node, AllIcons.Debugger.Db_primitive, key)) + addToModelAndRefresh(snapshotModel, child, parent, insertionIndex) + } else { + if (node.isArray) { + child = DefaultMutableTreeNode(SnapshotDataDescriptor(key, AllIcons.Debugger.Db_array)) + addToModelAndRefresh(snapshotModel, child, parent, insertionIndex) + node.mapIndexed { index: Int, item: JsonNode? -> + buildChildrenModel(item, child, index.toString()) + } + } else { + child = DefaultMutableTreeNode(SnapshotDataDescriptor(key, AllIcons.Json.Object)) + addToModelAndRefresh(snapshotModel, child, parent, insertionIndex) + node.fields().forEach { + buildChildrenModel(it.value, child, it.key) + } + } + } + } +} \ No newline at end of file diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowTreeDataManager.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowTreeDataManager.kt new file mode 100644 index 0000000000..82054a0b60 --- /dev/null +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowTreeDataManager.kt @@ -0,0 +1,226 @@ +package net.corda.ideaplugin.toolwindow + +import java.io.File +import java.nio.file.* +import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE +import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE +import javax.swing.JTree +import javax.swing.SwingWorker +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel + +/** + * Manager class for flow list data. + */ +class FlowTreeDataManager(val tree: JTree, val snapshotModel: FlowSnapshotTreeDataManager) { + companion object { + const val FLOWS_DIRECTORY = "flowStackSnapshots" + } + + // Root node (i.e. [FLOWS_DIRECTORY]) of the flow directory hierarchy. + private val root = DefaultMutableTreeNode() + // Flow tree model + private val flowModel = DefaultTreeModel(root) + + // Watcher service used to monitor directory contents addition and deletion. + private val watcher: WatchService = FileSystems.getDefault().newWatchService() + // Registered watch keys for the corresponding directories + private val watchKeys = hashMapOf() + // Watch keys polling thread. Responsible for processing changes ot the contents of the monitored directories + private val dirObserver: DirObserver = DirObserver() + + init { + tree.model = flowModel + flowModel.reload() + dirObserver.execute() + } + + /** + * Builds the flow directory hierarchy with the root being associated with the passed [flowsDirectory]. + * If the parameter is missing the function rebuilds current hierarchy and reloads (refreshes) current model. + */ + fun loadFlows(flowsDirectory: File? = root.userObjectAsFile()) { + root.userObject = flowsDirectory ?: return + root.removeAllChildren() + + // Invalidate all current watch keys + invalidateWatchKeys() + + // We need to add a watch key to the parent + startWatching(flowsDirectory) + + // We expect 2-level directory nesting. The first level are dates and the second level are flow IDs. + // Dates directories + flowsDirectory.listFiles().filter { it.isDirectory }.forEach { + insertDateDirectory(it) + } + flowModel.reload() + } + + /* + * Inserts date directory to the node hierarchy. A date directory is considered to be on the first hierarchy level. + */ + private fun insertDateDirectory(dateDir: File, insertionIndex: Int = -1) { + startWatching(dateDir) + val dateNode = DefaultMutableTreeNode(dateDir) + addToModelAndRefresh(flowModel, dateNode, root, insertionIndex) + // Flows directories + dateDir.listFiles().filter { it.isDirectory }.forEach { + startWatching(it) + addToModelAndRefresh(flowModel, DefaultMutableTreeNode(it), dateNode) + } + } + + /** + * Removes current hierarchy. + */ + fun clear() { + invalidateWatchKeys() + root.removeAllChildren() + flowModel.reload() + } + + private fun isSelected(dir: File?): Boolean { + val node = tree.selectedNode() + return node != null && dir == node.userObjectAsFile() + } + + private fun startWatching(dir: File) { + val key = dir.toPath().register(watcher, ENTRY_CREATE, ENTRY_DELETE) + watchKeys.put(key, dir) + } + + private fun invalidateWatchKeys() { + watchKeys.keys.forEach { it.cancel() } + watchKeys.clear() + } + + private fun addNodeToFlowModel(dir: File) { + // We consider addition only of either a date or flow directory + val parent = dir.parentFile + if (parent.name == FLOWS_DIRECTORY) { + // if a date directory has been added this means that that its parent is [FLOWS_DIRECTORY] + insertDateDirectory(dir, findInsertionIndex(root.childNodes(), dir.name)) + } else if (parent.parentFile.name == FLOWS_DIRECTORY) { + // if a flow directory has been added this means that that the parent of its parent is [FLOWS_DIRECTORY] + val parentNode = root.childNodes().findByFile(parent) ?: return + flowModel.insertNodeInto(DefaultMutableTreeNode(dir), parentNode, findInsertionIndex(parentNode.childNodes(), dir.name)) + startWatching(dir) + } + } + + private fun removeNodeFromFlowModel(dir: File) { + val selectedNode = tree.selectedNode() + if (selectedNode != null && selectedNode.userObjectAsFile() == dir) { + // Reload flows if the [dir] is currently selected + loadFlows() + return + } + val parent = dir.parentFile + var parentNode: DefaultMutableTreeNode? = null + if (parent.name == FLOWS_DIRECTORY) { + // if a date directory has been added this means that that its parent is [FLOWS_DIRECTORY] + parentNode = root + } else if (parent.parentFile.name == FLOWS_DIRECTORY) { + // if a flow directory has been added this means that that the parent of its parent is [FLOWS_DIRECTORY] + parentNode = root.childNodes().findByFile(parent) + } + if (parentNode != null) { + val node = parentNode.childNodes().findByFile(dir) ?: return + flowModel.removeNodeFromParent(node) + } + } + + private fun findInsertionIndex(nodes: List, name: String): Int { + return -nodes.toList().map { (it.userObject as File).name }.binarySearch(name) - 1 + } + + /* + * Swing thread polling the watcher service for any changes (i.e. entry creation or deletion). + * Once a change is detected the event is processed accordingly. + */ + private inner class DirObserver : SwingWorker() { + override fun doInBackground(): Void? { + while (true) { + // wait for key to be signaled + val key = try { + watcher.take() + } catch (x: InterruptedException) { + return null + } + + publish(key) + + if (!key.reset()) { + watchKeys.remove(key) + } + } + } + + override fun process(keys: MutableList?) { + keys?.forEach { + processKey(it) + } + } + + private fun processKey(key: WatchKey) { + key.pollEvents().forEach { + val kind = it.kind() + + // The OVERFLOW event can occur regardless if events are lost or discarded. + if (kind == StandardWatchEventKinds.OVERFLOW) { + return + } + processEvent(it as WatchEvent<*>, key) + } + } + + private fun processEvent(event: WatchEvent<*>, key: WatchKey) { + val parent = getParent(key) + val file = File(parent, (event.context() as Path).toString()) + when (event.kind()) { + ENTRY_CREATE -> { + if (!file.isDirectory) { + // We only need to do anything if the flow snapshots are being currently examined + if (isSelected(parent) && file.name.startsWith(FlowSnapshotTreeDataManager.SNAPSHOT_FILE_PREFIX)) { + snapshotModel.addNodeToSnapshotModel(file) + } + } else { + addNodeToFlowModel(file) + } + } + ENTRY_DELETE -> { + // We only need to do anything if the flow snapshots are being currently examined + if (isSelected(parent) && file.name.startsWith(FlowSnapshotTreeDataManager.SNAPSHOT_FILE_PREFIX)) { + snapshotModel.removeNodeFromSnapshotModel(file) + } else { + removeNodeFromFlowModel(file) + } + } + } + } + + private fun getParent(key: WatchKey): File? { + return watchKeys[key] + } + } +} + +internal fun DefaultMutableTreeNode.userObjectAsFile() = userObject as? File +internal fun DefaultMutableTreeNode.childNodes() = children().toList().mapNotNull { it as? DefaultMutableTreeNode } +private fun List.findByFile(file: File) = find { it.userObjectAsFile() == file } +private fun JTree.selectedNode() = lastSelectedPathComponent as? DefaultMutableTreeNode + +internal fun addToModelAndRefresh(model: DefaultTreeModel, + child: DefaultMutableTreeNode, + parent: DefaultMutableTreeNode, + insertionIndex: Int = -1) { + val indices = if (insertionIndex < 0) { + parent.add(child) + intArrayOf(parent.childCount - 1) + } else { + parent.insert(child, insertionIndex) + intArrayOf(insertionIndex) + } + model.nodesWereInserted(parent, indices) +} \ No newline at end of file diff --git a/experimental/intellij-plugin/src/main/resources/META-INF/plugin.xml b/experimental/intellij-plugin/src/main/resources/META-INF/plugin.xml index 96093296b6..c5fbae5966 100644 --- a/experimental/intellij-plugin/src/main/resources/META-INF/plugin.xml +++ b/experimental/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -26,7 +26,8 @@ org.jetbrains.plugins.gradle - + + diff --git a/experimental/intellij-plugin/src/main/resources/images/corda-icon.png b/experimental/intellij-plugin/src/main/resources/images/corda-icon.png deleted file mode 100644 index 308055c3e2..0000000000 Binary files a/experimental/intellij-plugin/src/main/resources/images/corda-icon.png and /dev/null differ diff --git a/experimental/intellij-plugin/src/main/resources/images/corda-module-icon.png b/experimental/intellij-plugin/src/main/resources/images/corda-module-icon.png new file mode 100644 index 0000000000..183ce8e7d0 Binary files /dev/null and b/experimental/intellij-plugin/src/main/resources/images/corda-module-icon.png differ diff --git a/experimental/intellij-plugin/src/main/resources/images/corda-module-icon@2x.png b/experimental/intellij-plugin/src/main/resources/images/corda-module-icon@2x.png new file mode 100644 index 0000000000..f942c2e9e2 Binary files /dev/null and b/experimental/intellij-plugin/src/main/resources/images/corda-module-icon@2x.png differ diff --git a/experimental/intellij-plugin/src/main/resources/images/corda-toolwindow-icon.png b/experimental/intellij-plugin/src/main/resources/images/corda-toolwindow-icon.png new file mode 100644 index 0000000000..35cd13c03f Binary files /dev/null and b/experimental/intellij-plugin/src/main/resources/images/corda-toolwindow-icon.png differ