Skip to content

Commit

Permalink
Migrate NBT editor provider to newer async api
Browse files Browse the repository at this point in the history
  • Loading branch information
RedNesto committed Dec 20, 2023
1 parent aba632e commit ee61ae7
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 150 deletions.
200 changes: 50 additions & 150 deletions src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,140 +20,78 @@

package com.demonwav.mcdev.nbt.editor

import com.demonwav.mcdev.asset.MCDevBundle
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.invokeAndWait
import com.demonwav.mcdev.util.invokeLater
import com.demonwav.mcdev.nbt.lang.NbttFileType
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.application.runReadAction
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileEditor.AsyncFileEditorProvider
import com.intellij.openapi.fileEditor.FileDocumentManager
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
import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessProvider
import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorProvider
import com.intellij.openapi.fileEditor.impl.text.TextEditorState
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
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.EditorNotifications
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 {
class NbtFileEditorProvider : AsyncFileEditorProvider, DumbAware {
override fun getEditorTypeId() = EDITOR_TYPE_ID
override fun accept(project: Project, file: VirtualFile) = file.fileType == NbtFileType
override fun getPolicy() = FileEditorPolicy.NONE
override fun createEditor(project: Project, file: VirtualFile): FileEditor {
val fileEditor = NbtFileEditor(file) { nbtFile ->
invokeAndWait {
super.createEditor(project, nbtFile)
}
}

runAsync {
val nbtFile = NbtVirtualFile(file, project)
override fun getPolicy() = FileEditorPolicy.HIDE_DEFAULT_EDITOR
override fun createEditorAsync(project: Project, file: VirtualFile): AsyncFileEditorProvider.Builder {
val nbtFile = NbtVirtualFile(file, project)

val allowWrite = runReadAction { NonProjectFileWritingAccessProvider.isWriteAccessAllowed(file, project) }
if (allowWrite) {
NonProjectFileWritingAccessProvider.allowWriting(listOf(nbtFile))
}

fileEditor.ready(nbtFile, project)
val allowWrite = NonProjectFileWritingAccessProvider.isWriteAccessAllowed(nbtFile, project)
if (allowWrite) {
NonProjectFileWritingAccessProvider.allowWriting(listOf(nbtFile))
}

return fileEditor
return NbtEditorBuilder(project, nbtFile)
}

override fun createEditor(project: Project, file: VirtualFile): FileEditor {
val nbtFile = NbtVirtualFile(file, project)
return NbtEditorBuilder(project, nbtFile).build()
}

companion object {
private const val EDITOR_TYPE_ID = "nbt_editor"
}
}

private class NbtEditorBuilder(val project: Project, val nbtFile: NbtVirtualFile) : AsyncFileEditorProvider.Builder() {
override fun build(): FileEditor {
val document = FileDocumentManager.getInstance().getDocument(nbtFile)!!
val backingEditor = EditorFactory.getInstance().createEditor(document, project, NbttFileType, false)
val fileEditor = NbtFileEditor(nbtFile, backingEditor, project)
return fileEditor
}
}

private class NbtFileEditor(
private val file: VirtualFile,
private val editorProvider: (NbtVirtualFile) -> FileEditor,
private val file: NbtVirtualFile,
private val editor: Editor,
project: Project,
) : FileEditor {

private var editor: FileEditor? = null
private val editorCheckedDisposable = Disposer.newCheckedDisposable()
private val component = JPanel(BorderLayout())
private val tempUserData = mutableMapOf<Any?, Any?>()
private var disposed = false

init {
val loading = JBLoadingPanel(null, this)
loading.setLoadingText("Parsing NBT file")
loading.startLoading()
component.add(loading, BorderLayout.CENTER)
}

fun ready(nbtFile: NbtVirtualFile, project: Project) {
if (project.isDisposed) {
return
}

component.removeAll()

val toolbar = NbtToolbar(nbtFile)
nbtFile.toolbar = toolbar
editor = invokeAndWait {
editorProvider(nbtFile)
}
editor?.let { editor ->
try {
Disposer.register(this, editor)
Disposer.register(editor, editorCheckedDisposable)
} catch (e: IncorrectOperationException) {
// The editor can be disposed really quickly when opening a large number of NBT files
// Since everything happens basically at the same time, calling Disposer.isDisposed right before
// returns false but #dispose throws this IOE...
Disposer.dispose(this)
return@let
}
tempUserData.forEach { (k, v) ->
@Suppress("UNCHECKED_CAST")
editor.putUserData(k as Key<Any>, v)
}
tempUserData.clear()
invokeLater {
component.add(toolbar.panel, BorderLayout.NORTH)
component.add(editor.component, BorderLayout.CENTER)
}
EditorNotifications.getInstance(project).updateAllNotifications()
}

// This can be null if the file is too big to be parsed as a psi file
val psiFile = runReadAction {
PsiManager.getInstance(project).findFile(nbtFile) as? NbttFile
} ?: return

WriteCommandAction.writeCommandAction(psiFile)
.shouldRecordActionForActiveDocument(false)
.withUndoConfirmationPolicy(UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION)
.run<Throwable> {
CodeStyleManager.getInstance(project).reformat(psiFile, true)
}

if (disposed) {
return
}
val toolbar = NbtToolbar(file)
file.toolbar = toolbar
(editor as? EditorEx)?.permanentHeaderComponent = toolbar.panel
editor.headerComponent = toolbar.panel
EditorNotifications.getInstance(project).updateAllNotifications()

project.messageBus.connect(this).subscribe(
AnActionListener.TOPIC,
Expand All @@ -169,72 +107,34 @@ private class NbtFileEditor(
return
}

nbtFile.writeFile(this)
file.writeFile(this)
}
},
)
}

override fun isModified() = editor.exec { isModified } ?: false
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
editor.exec { addPropertyChangeListener(listener) }
}
override fun isModified() = false
override fun addPropertyChangeListener(listener: PropertyChangeListener) = Unit

override fun getName() = editor.exec { name } ?: ""
override fun setState(state: FileEditorState) {
editor.exec { setState(state) }
}
override fun getName() = MCDevBundle("nbt.editor.name")
override fun setState(state: FileEditorState) = Unit

override fun getState(level: FileEditorStateLevel): FileEditorState = editor.exec { getState(level) }
?: TextEditorState()
override fun getFile(): VirtualFile = file

override fun getFile(): VirtualFile = editor.exec { file } ?: file
override fun getComponent() = editor.component
override fun getPreferredFocusedComponent() = null
override fun <T : Any?> getUserData(key: Key<T>): T? = editor.getUserData(key)

override fun getComponent() = component
override fun getPreferredFocusedComponent() = editor.exec { preferredFocusedComponent }
override fun <T : Any?> getUserData(key: Key<T>): T? {
if (editor == null) {
@Suppress("UNCHECKED_CAST")
return tempUserData[key] as? T
}
return editor.exec { getUserData(key) }
}
override fun selectNotify() {
editor.exec { selectNotify() }
}
override fun <T : Any?> putUserData(key: Key<T>, value: T?) = editor.putUserData(key, value)

override fun <T : Any?> putUserData(key: Key<T>, value: T?) {
if (editor == null) {
tempUserData[key] = value
return
}
editor.exec { putUserData(key, value) }
}

override fun getCurrentLocation() = editor.exec { currentLocation }
override fun deselectNotify() {
editor.exec { deselectNotify() }
}

override fun getBackgroundHighlighter() = editor.exec { backgroundHighlighter }
override fun isValid() = editor.exec { isValid } ?: true
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
editor.exec { removePropertyChangeListener(listener) }
}
override fun isValid() = true
override fun removePropertyChangeListener(listener: PropertyChangeListener) = Unit

override fun dispose() {
disposed = true
EditorFactory.getInstance().releaseEditor(editor)
}

override fun getStructureViewBuilder() = editor.exec { structureViewBuilder }
override fun equals(other: Any?) = other is NbtFileEditor && other.component == this.component
override fun hashCode() = editor.hashCode()
override fun toString() = editor.toString()

private inline fun <T : Any?> FileEditor?.exec(action: FileEditor.() -> T): T? {
if (editor == null || editorCheckedDisposable.isDisposed) {
return null
}
return this?.action()
}
}
51 changes: 51 additions & 0 deletions src/main/kotlin/nbt/lang/NbttFileViewProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2023 minecraft-dev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, version 3.0 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.nbt.lang

import com.intellij.lang.Language
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.FileViewProvider
import com.intellij.psi.FileViewProviderFactory
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.SingleRootFileViewProvider

class NbttFileViewProvider(manager: PsiManager, file: VirtualFile, eventSystemEnabled: Boolean) :
SingleRootFileViewProvider(manager, file, eventSystemEnabled, NbttLanguage) {

override fun createFile(project: Project, file: VirtualFile, fileType: FileType): PsiFile = NbttFile(this)

override fun createFile(file: VirtualFile, fileType: FileType, language: Language): PsiFile = NbttFile(this)

override fun createFile(lang: Language): PsiFile = NbttFile(this)
}

class NbttFileViewProviderFactory : FileViewProviderFactory {

override fun createFileViewProvider(
file: VirtualFile,
language: Language?,
manager: PsiManager,
eventSystemEnabled: Boolean
): FileViewProvider = NbttFileViewProvider(manager, file, eventSystemEnabled)
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
<additionalTextAttributes scheme="Default" file="colorSchemes/NbttDefault.xml"/>
<additionalTextAttributes scheme="Darcula" file="colorSchemes/NbttDarcula.xml"/>
<codeInsight.parameterNameHints language="NBTT" implementationClass="com.demonwav.mcdev.nbt.lang.format.NbttParameterNameHints" />
<lang.fileViewProviderFactory language="NBTT" implementationClass="com.demonwav.mcdev.nbt.lang.NbttFileViewProviderFactory"/>

<!-- Minecraft localization files -->
<fileType name="MCLang" language="MCLang" implementationClass="com.demonwav.mcdev.translations.lang.LangFileType" fieldName="INSTANCE" extensions="lang"/>
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages/MinecraftDevelopment.properties
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ nbt.lang.errors.parse_timeout=NBT parse timeout exceeded - Parse time: {0}, Time

nbt.lang.errors.wrapped_error_message=Malformed NBT file:\n{0}

nbt.editor.name=NBT Text

nbt.file.save_notify.success.title=Saved NBT file successfully
nbt.file.save_notify.success.content={0} was saved successfully.

Expand Down

0 comments on commit ee61ae7

Please sign in to comment.