User: kyle wood Date: 03 Oct 22 01:30 Revision: abf32c6ee060522323f0427c8ddb24e739d2402a Summary: Re-work some NBT handling for better performance, and handle big files This commit includes some default NBT formatting settings which result in smaller files, and re-implements NbtVirtualFile to be a LightVirtualFile. Part of that re-implementation is to make NBT files only write to disk when saved manually, which is the behavior I originally wanted anyways. This feels much safer and more stable than whatever was happening originally. By being a LightVirtualFile it seems the size limits / requirements on Psi files are relaxed, but I don't fully understand the details. It seems to handle larger NBT files better now. Relevant issues: #1894, #1893 TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=8140&personal=false Index: build.gradle.kts =================================================================== --- build.gradle.kts (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ build.gradle.kts (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -361,7 +361,7 @@ } tasks.runIde { - maxHeapSize = "2G" + maxHeapSize = "4G" jvmArgs("--add-exports=java.base/jdk.internal.vm=ALL-UNNAMED") System.getProperty("debug")?.let { Index: src/main/kotlin/nbt/NbtVirtualFile.kt =================================================================== --- src/main/kotlin/nbt/NbtVirtualFile.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/NbtVirtualFile.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -13,39 +13,39 @@ import com.demonwav.mcdev.nbt.editor.CompressionSelection import com.demonwav.mcdev.nbt.editor.NbtToolbar import com.demonwav.mcdev.nbt.lang.NbttFile +import com.demonwav.mcdev.nbt.lang.NbttFileType import com.demonwav.mcdev.nbt.lang.NbttLanguage -import com.demonwav.mcdev.util.invokeAndWait -import com.demonwav.mcdev.util.runWriteAction +import com.demonwav.mcdev.util.runReadActionAsync +import com.demonwav.mcdev.util.runWriteTaskLater import com.intellij.notification.Notification import com.intellij.notification.NotificationType -import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl +import com.intellij.openapi.fileTypes.PlainTextLanguage import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiFileFactory import com.intellij.psi.PsiManager -import com.intellij.psi.codeStyle.CodeStyleManager -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream +import com.intellij.testFramework.LightVirtualFile +import com.intellij.util.ThreeState import java.io.DataOutputStream import java.util.concurrent.TimeUnit import java.util.zip.GZIPOutputStream -class NbtVirtualFile(private val backingFile: VirtualFile, private val project: Project) : VirtualFile() { +class NbtVirtualFile(private val backingFile: VirtualFile, private val project: Project) : + LightVirtualFile(backingFile.name + ".nbtt", NbttFileType, ""), + IdeDocumentHistoryImpl.SkipFromDocumentHistory { - var bytes: ByteArray val isCompressed: Boolean lateinit var toolbar: NbtToolbar val parseSuccessful: Boolean init { - this.bytes = byteArrayOf() - var text: String + originalFile = backingFile + language = NbttLanguage + var text: String var tempCompressed: Boolean var tempParseSuccessful: Boolean + try { val (rootCompound, isCompressed) = Nbt.buildTagTree(backingFile.inputStream, TimeUnit.SECONDS.toMillis(10)) text = rootCompound.toString() @@ -60,89 +60,67 @@ this.isCompressed = tempCompressed this.parseSuccessful = tempParseSuccessful - if (this.parseSuccessful) { - val psiFile = runReadAction { - PsiFileFactory.getInstance(project).createFileFromText(NbttLanguage, text) + if (!this.parseSuccessful) { + language = PlainTextLanguage.INSTANCE - } + } - invokeAndWait { - psiFile.runWriteAction { - this.bytes = PsiDocumentManager.getInstance(project).getDocument( - CodeStyleManager.getInstance(project).reformat(psiFile, true) as PsiFile - )?.immutableCharSequence?.toString()?.toByteArray() ?: byteArrayOf() + + setContent(this, text, false) - } + } - } - } else { - this.bytes = text.toByteArray() - } - } override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) { backingFile.refresh(asynchronous, recursive, postRunnable) } - override fun getLength() = bytes.size.toLong() - override fun getFileSystem() = backingFile.fileSystem - override fun getPath() = backingFile.path - override fun isDirectory() = false - override fun getTimeStamp() = backingFile.timeStamp - override fun getModificationStamp() = 0L - - override fun getName() = - backingFile.name + (if (parseSuccessful) ".nbtt" else ".txt") // don't highlight syntax on bad files - - override fun contentsToByteArray() = bytes - override fun isValid() = backingFile.isValid - override fun getInputStream() = ByteArrayInputStream(bytes) override fun getParent() = backingFile - override fun getChildren() = emptyArray() override fun isWritable() = backingFile.isWritable - override fun getOutputStream(requestor: Any, newModificationStamp: Long, newTimeStamp: Long) = - VfsUtilCore.outputStreamAddingBOM(NbtOutputStream(this, requestor), this) + override fun isTooLargeForIntelligence() = ThreeState.NO fun writeFile(requester: Any) { + runReadActionAsync { - val nbttFile = PsiManager.getInstance(project).findFile(this) as? NbttFile + val nbttFile = PsiManager.getInstance(project).findFile(this) as? NbttFile + - if (nbttFile == null) { - Notification( - "NBT Save Error", + if (nbttFile == null) { + Notification( + "NBT Save Error", - "Error Saving NBT File", - "The file is not recognised as a NBTT file. This might be caused by wrong file type associations.", + "Error saving NBT file", + "The file is not recognised as a NBTT file. This might be caused by wrong file type associations," + + " or the file could be too large.", - NotificationType.WARNING - ).notify(project) + NotificationType.WARNING + ).notify(project) - return + return@runReadActionAsync - } + } - val rootTag = nbttFile.getRootCompound()?.getRootCompoundTag() + val rootTag = nbttFile.getRootCompound()?.getRootCompoundTag() - if (rootTag == null) { - Notification( - "NBT Save Error", + if (rootTag == null) { + Notification( + "NBT Save Error", - "Error Saving NBT File", + "Error saving NBT file", - "Due to errors in the text representation, ${backingFile.name} could not be saved.", - NotificationType.WARNING - ).notify(project) + "Due to errors in the text representation, ${backingFile.name} could not be saved.", + NotificationType.WARNING + ).notify(project) - return + return@runReadActionAsync - } + } - this.bytes = PsiDocumentManager.getInstance(project).getDocument(nbttFile) - ?.immutableCharSequence?.toString()?.toByteArray() ?: byteArrayOf() - + runWriteTaskLater { - // just to be safe - this.parent.bom = null - val filteredStream = when (toolbar.selection) { - CompressionSelection.GZIP -> GZIPOutputStream(this.parent.getOutputStream(requester)) - CompressionSelection.UNCOMPRESSED -> this.parent.getOutputStream(requester) - } + // just to be safe + this.parent.bom = null + val filteredStream = when (toolbar.selection) { + CompressionSelection.GZIP -> GZIPOutputStream(this.parent.getOutputStream(requester)) + CompressionSelection.UNCOMPRESSED -> this.parent.getOutputStream(requester) + } - DataOutputStream(filteredStream).use { stream -> - rootTag.write(stream) - } + DataOutputStream(filteredStream).use { stream -> + rootTag.write(stream) + } + + Notification( + "NBT Save Success", + "Saved NBT file successfully", + "${backingFile.name} was saved successfully.", + NotificationType.INFORMATION + ).notify(project) - } -} + } + } - -private class NbtOutputStream(private val file: NbtVirtualFile, private val requestor: Any) : ByteArrayOutputStream() { - override fun close() { - file.bytes = toByteArray() - - file.writeFile(requestor) } } Index: src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt =================================================================== --- src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -12,9 +12,17 @@ import com.demonwav.mcdev.nbt.NbtVirtualFile import com.demonwav.mcdev.nbt.filetype.NbtFileType +import com.demonwav.mcdev.nbt.lang.NbttFile import com.demonwav.mcdev.util.invokeLater -import com.intellij.openapi.application.ApplicationManager +import com.intellij.ide.actions.SaveAllAction +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.AnActionResult +import com.intellij.openapi.actionSystem.ex.AnActionListener +import com.intellij.openapi.command.UndoConfirmationPolicy +import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorPolicy import com.intellij.openapi.fileEditor.FileEditorState import com.intellij.openapi.fileEditor.FileEditorStateLevel @@ -26,11 +34,14 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiManager +import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.ui.components.JBLoadingPanel import com.intellij.util.IncorrectOperationException import java.awt.BorderLayout import java.beans.PropertyChangeListener import javax.swing.JPanel +import org.jetbrains.concurrency.runAsync class NbtFileEditorProvider : PsiAwareTextEditorProvider(), DumbAware { override fun getEditorTypeId() = EDITOR_TYPE_ID @@ -39,7 +50,7 @@ override fun createEditor(project: Project, file: VirtualFile): FileEditor { val fileEditor = NbtFileEditor(file) { nbtFile -> super.createEditor(project, nbtFile) } - ApplicationManager.getApplication().executeOnPooledThread { + runAsync { val nbtFile = NbtVirtualFile(file, project) if (NonProjectFileWritingAccessProvider.isWriteAccessAllowed(file, project)) { @@ -47,7 +58,7 @@ } invokeLater { - fileEditor.ready(nbtFile) + fileEditor.ready(nbtFile, project) } } @@ -74,7 +85,11 @@ component.add(loading, BorderLayout.CENTER) } - fun ready(nbtFile: NbtVirtualFile) { + fun ready(nbtFile: NbtVirtualFile, project: Project) { + if (project.isDisposed) { + return + } + component.removeAll() val toolbar = NbtToolbar(nbtFile) @@ -93,8 +108,36 @@ component.add(toolbar.panel, BorderLayout.NORTH) component.add(editor.component, BorderLayout.CENTER) } + + // This can be null if the file is too big to be parsed as a psi file + val psiFile = PsiManager.getInstance(project).findFile(nbtFile) as? NbttFile ?: return + WriteCommandAction.writeCommandAction(psiFile) + .shouldRecordActionForActiveDocument(false) + .withUndoConfirmationPolicy(UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION) + .run { + CodeStyleManager.getInstance(project).reformat(psiFile, true) - } + } + project.messageBus.connect(this).subscribe( + AnActionListener.TOPIC, + object : AnActionListener { + override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) { + if (action !is SaveAllAction) { + return + } + + val selectedEditor = FileEditorManager.getInstance(project).selectedEditor ?: return + + if (selectedEditor !is NbtFileEditor || selectedEditor.editor != editor) { + return + } + + nbtFile.writeFile(this) + } + } + ) + } + override fun isModified() = editor.exec { isModified } ?: false override fun addPropertyChangeListener(listener: PropertyChangeListener) { editor.exec { addPropertyChangeListener(listener) } Index: src/main/kotlin/nbt/editor/NbtToolbar.kt =================================================================== --- src/main/kotlin/nbt/editor/NbtToolbar.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/editor/NbtToolbar.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -30,15 +30,9 @@ if (nbtFile.isCompressed) CompressionSelection.GZIP else CompressionSelection.UNCOMPRESSED lastSelection = selection - saveButton.isVisible = false - if (!nbtFile.isWritable || !nbtFile.parseSuccessful) { compressionBox.isEnabled = false - } else { - compressionBox.addActionListener { - checkModified() - } + } - } if (!nbtFile.parseSuccessful) { panel.isVisible = false @@ -46,7 +40,6 @@ saveButton.addActionListener { lastSelection = selection - checkModified() runWriteTaskLater { nbtFile.writeFile(this) @@ -54,10 +47,6 @@ } } - private fun checkModified() { - saveButton.isVisible = lastSelection != selection - } - val selection get() = compressionBox.selectedItem as CompressionSelection } Index: src/main/kotlin/nbt/filetype/NbtFileType.kt =================================================================== --- src/main/kotlin/nbt/filetype/NbtFileType.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/filetype/NbtFileType.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -19,7 +19,7 @@ override fun getIcon() = PlatformAssets.MINECRAFT_ICON override fun getCharset(file: VirtualFile, content: ByteArray): String? = null override fun getName() = "NBT" - override fun getDescription() = "Named Binary Tag" + override fun getDescription() = "NBT" override fun isBinary() = true override fun isReadOnly() = false } Index: src/main/kotlin/nbt/lang/NbttFileType.kt =================================================================== --- src/main/kotlin/nbt/lang/NbttFileType.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/lang/NbttFileType.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -17,5 +17,5 @@ override fun getIcon() = PlatformAssets.MINECRAFT_ICON override fun getName() = "NBTT" override fun getDefaultExtension() = "nbtt" - override fun getDescription() = "NBT Text Representation (Don't Use This One)" + override fun getDescription() = "NBT text representation (don't use this one)" } Index: src/main/kotlin/nbt/lang/format/NbttCodeStyleSettings.kt =================================================================== --- src/main/kotlin/nbt/lang/format/NbttCodeStyleSettings.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/lang/format/NbttCodeStyleSettings.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -15,6 +15,7 @@ import com.intellij.psi.codeStyle.CommonCodeStyleSettings import com.intellij.psi.codeStyle.CustomCodeStyleSettings +@Suppress("PropertyName") class NbttCodeStyleSettings(container: CodeStyleSettings) : CustomCodeStyleSettings(NbttLanguage.id, container) { @JvmField @@ -24,8 +25,8 @@ var SPACE_BEFORE_COLON = false @JvmField - var LIST_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS + var LIST_WRAPPING = CommonCodeStyleSettings.WRAP_AS_NEEDED @JvmField - var ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS + var ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_AS_NEEDED } Index: src/main/kotlin/nbt/lang/format/NbttLanguageCodeStyleSettingsProvider.kt =================================================================== --- src/main/kotlin/nbt/lang/format/NbttLanguageCodeStyleSettingsProvider.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/lang/format/NbttLanguageCodeStyleSettingsProvider.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -33,11 +33,13 @@ CodeStyleSettingsCustomizableOptions.getInstance().WRAPPING_BRACES, arrayOf( "Do not wrap", - "Wrap always" + "Wrap as needed", + "Wrap always", ), intArrayOf( CommonCodeStyleSettings.DO_NOT_WRAP, - CommonCodeStyleSettings.WRAP_ALWAYS + CommonCodeStyleSettings.WRAP_AS_NEEDED, + CommonCodeStyleSettings.WRAP_ALWAYS, ) ) consumer.showCustomOption( @@ -47,14 +49,17 @@ CodeStyleSettingsCustomizableOptions.getInstance().WRAPPING_BRACES, arrayOf( "Do not wrap", - "Wrap always" + "Wrap as needed", + "Wrap always", ), intArrayOf( CommonCodeStyleSettings.DO_NOT_WRAP, - CommonCodeStyleSettings.WRAP_ALWAYS + CommonCodeStyleSettings.WRAP_AS_NEEDED, + CommonCodeStyleSettings.WRAP_ALWAYS, ) ) } + SettingsType.SPACING_SETTINGS -> { consumer.showStandardOptions( "SPACE_WITHIN_BRACKETS", @@ -78,12 +83,13 @@ consumer.renameStandardOption("SPACE_WITHIN_BRACKETS", "List brackets") consumer.renameStandardOption("SPACE_WITHIN_PARENTHESES", "Array parentheses") } + else -> { } } } - override fun getIndentOptionsEditor(): IndentOptionsEditor? = SmartIndentOptionsEditor() + override fun getIndentOptionsEditor(): IndentOptionsEditor = SmartIndentOptionsEditor() override fun getLanguage() = NbttLanguage @@ -92,27 +98,26 @@ indentOptions: CommonCodeStyleSettings.IndentOptions ) { commonSettings.RIGHT_MARGIN = 150 + indentOptions.USE_TAB_CHARACTER = true + indentOptions.TAB_SIZE = 4 + indentOptions.INDENT_SIZE = indentOptions.TAB_SIZE indentOptions.CONTINUATION_INDENT_SIZE = indentOptions.INDENT_SIZE } } -@Suppress("SameParameterValue") -private fun sample(@Language("NBTT") code: String) = code.trim() - -private val SAMPLE = sample( - """ +@Language("NBTT") +private const val SAMPLE = """ "": { - list: [ - { - "created-on": 1264099775885L - "name": "Compound tag #0" - }, - { - primitive list: [ 0B, 1B, false, true, 14B, ] - array: ints(1, 3, 4) - number: 1264099775885L - }, - ] + list: [ + { + "created-on": 1264099775885L + "name": "Compound tag #0" + }, + { + primitive list: [ 0B, 1B, false, true, 14B, ] + array: ints(1, 3, 4) + number: 1264099775885L + }, + ] } """ -) Index: src/main/kotlin/nbt/lang/format/NbttParameterNameHints.kt =================================================================== --- src/main/kotlin/nbt/lang/format/NbttParameterNameHints.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/lang/format/NbttParameterNameHints.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -37,14 +37,12 @@ is NbttList -> { // Size hint val size = element.getTagList().size - if (size > 50) { - list.add( - InlayInfo( - "$size ${if (size == 1) "child" else "children"}", - element.textRange.startOffset + 1 - ) - ) + list.add( + InlayInfo( + "$size ${if (size == 1) "child" else "children"}", + element.textRange.startOffset + 1 + ) + ) - } if (size > 5) { // Index hints @@ -55,14 +53,12 @@ } is NbttByteArray -> { val size = element.getByteList().size - if (size > 50) { - list.add( - InlayInfo( - "$size ${if (size == 1) "child" else "children"}", - element.node.getChildren(null)[1].textRange.startOffset + 1 - ) - ) + list.add( + InlayInfo( + "$size ${if (size == 1) "child" else "children"}", + element.node.getChildren(null)[1].textRange.startOffset + 1 + ) + ) - } if (size > 5) { // Index hints @@ -73,14 +69,12 @@ } is NbttIntArray -> { val size = element.getIntList().size - if (size > 50) { - list.add( - InlayInfo( - "$size ${if (size == 1) "child" else "children"}", - element.node.getChildren(null)[1].textRange.startOffset + 1 - ) - ) + list.add( + InlayInfo( + "$size ${if (size == 1) "child" else "children"}", + element.node.getChildren(null)[1].textRange.startOffset + 1 + ) + ) - } if (size > 5) { // Index hints Index: src/main/kotlin/nbt/tags/NbtTag.kt =================================================================== --- src/main/kotlin/nbt/tags/NbtTag.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/nbt/tags/NbtTag.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -83,7 +83,7 @@ } for (i in 0 until indentLevel) { - sb.append(" ") + sb.append("\t") } } Index: src/main/kotlin/platform/mcp/at/AtFileType.kt =================================================================== --- src/main/kotlin/platform/mcp/at/AtFileType.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/platform/mcp/at/AtFileType.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -16,7 +16,7 @@ object AtFileType : LanguageFileType(AtLanguage) { override fun getName() = "Access Transformers" - override fun getDescription() = "Access Transformers File" + override fun getDescription() = "Access transformers" override fun getDefaultExtension() = "" override fun getIcon() = PlatformAssets.MCP_ICON } Index: src/main/kotlin/platform/mcp/aw/AwFileType.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/AwFileType.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/platform/mcp/aw/AwFileType.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -17,7 +17,7 @@ override fun getName() = "Access Widener" - override fun getDescription() = "Access Widener File" + override fun getDescription() = "Access widener" override fun getDefaultExtension() = "accesswidener" Index: src/main/kotlin/platform/mixin/config/MixinConfigFileType.kt =================================================================== --- src/main/kotlin/platform/mixin/config/MixinConfigFileType.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/platform/mixin/config/MixinConfigFileType.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -27,7 +27,7 @@ override fun isMyFileType(file: VirtualFile) = file.name.contains(filenameRegex) override fun getName() = "Mixin Configuration" - override fun getDescription() = "Mixin Configuration" + override fun getDescription() = "Mixin configuration" override fun getDefaultExtension() = "" override fun getIcon() = PlatformAssets.MIXIN_ICON } Index: src/main/kotlin/translations/lang/LangFileType.kt =================================================================== --- src/main/kotlin/translations/lang/LangFileType.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/translations/lang/LangFileType.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -14,7 +14,7 @@ import com.intellij.openapi.fileTypes.LanguageFileType object LangFileType : LanguageFileType(MCLangLanguage) { - const val FILE_EXTENSION = "lang" + private const val FILE_EXTENSION = "lang" override fun getIcon() = PlatformAssets.MINECRAFT_ICON override fun getName() = "MCLang" Index: src/main/kotlin/util/utils.kt =================================================================== --- src/main/kotlin/util/utils.kt (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/kotlin/util/utils.kt (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -32,6 +32,8 @@ import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import java.util.Locale +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.runAsync inline fun runWriteTask(crossinline func: () -> T): T { return invokeAndWait { @@ -104,6 +106,12 @@ return result } +inline fun runReadActionAsync(crossinline runnable: () -> T): Promise { + return runAsync { + runReadAction(runnable) + } +} + fun waitForAllSmart() { for (project in ProjectManager.getInstance().openProjects) { if (!project.isDisposed) { Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 0ffe9b30bb220e895ce39a7dfb3b53186ea38cb2) +++ src/main/resources/META-INF/plugin.xml (revision abf32c6ee060522323f0427c8ddb24e739d2402a) @@ -132,6 +132,7 @@ +