diff --git a/src/main/kotlin/nbt/NbtVirtualFile.kt b/src/main/kotlin/nbt/NbtVirtualFile.kt index 20646a4e3..28eccf507 100644 --- a/src/main/kotlin/nbt/NbtVirtualFile.kt +++ b/src/main/kotlin/nbt/NbtVirtualFile.kt @@ -24,10 +24,10 @@ import com.demonwav.mcdev.asset.MCDevBundle 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.runReadActionAsync import com.demonwav.mcdev.util.runWriteTaskLater +import com.intellij.lang.Language import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl @@ -41,42 +41,41 @@ import java.io.DataOutputStream import java.util.concurrent.TimeUnit import java.util.zip.GZIPOutputStream -class NbtVirtualFile(private val backingFile: VirtualFile, private val project: Project) : - LightVirtualFile(backingFile.name + ".nbtt", NbttFileType, ""), - IdeDocumentHistoryImpl.SkipFromDocumentHistory { +fun NbtVirtualFile(backingFile: VirtualFile, project: Project): NbtVirtualFile { + var language: Language = NbttLanguage + + var text: String + var compressed: Boolean + var parseSuccessful: Boolean + + try { + val (rootCompound, isCompressed) = Nbt.buildTagTree(backingFile.inputStream, TimeUnit.SECONDS.toMillis(10)) + text = rootCompound.toString() + compressed = isCompressed + parseSuccessful = true + } catch (e: MalformedNbtFileException) { + text = MCDevBundle("nbt.lang.errors.wrapped_error_message", e.message) + compressed = false + parseSuccessful = false + } - val isCompressed: Boolean - lateinit var toolbar: NbtToolbar - val parseSuccessful: Boolean - - init { - 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() - tempCompressed = isCompressed - tempParseSuccessful = true - } catch (e: MalformedNbtFileException) { - text = MCDevBundle("nbt.lang.errors.wrapped_error_message", e.message) - tempCompressed = false - tempParseSuccessful = false - } + if (!parseSuccessful) { + language = PlainTextLanguage.INSTANCE + } - this.isCompressed = tempCompressed - this.parseSuccessful = tempParseSuccessful + return NbtVirtualFile(backingFile, project, language, text, compressed, parseSuccessful) +} - if (!this.parseSuccessful) { - language = PlainTextLanguage.INSTANCE - } +class NbtVirtualFile( + private val backingFile: VirtualFile, + private val project: Project, + language: Language, + text: String, + val isCompressed: Boolean, + val parseSuccessful: Boolean, +) : LightVirtualFile(backingFile.name + ".nbtt", language, text), IdeDocumentHistoryImpl.SkipFromDocumentHistory { - setContent(this, text, false) - } + lateinit var toolbar: NbtToolbar override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) { backingFile.refresh(asynchronous, recursive, postRunnable) diff --git a/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt b/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt index 6c9ce5423..1d67d76e2 100644 --- a/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt +++ b/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt @@ -70,7 +70,8 @@ class NbtFileEditorProvider : PsiAwareTextEditorProvider(), DumbAware { runAsync { val nbtFile = NbtVirtualFile(file, project) - if (NonProjectFileWritingAccessProvider.isWriteAccessAllowed(file, project)) { + val allowWrite = runReadAction { NonProjectFileWritingAccessProvider.isWriteAccessAllowed(file, project) } + if (allowWrite) { NonProjectFileWritingAccessProvider.allowWriting(listOf(nbtFile)) } @@ -94,6 +95,7 @@ private class NbtFileEditor( private val editorCheckedDisposable = Disposer.newCheckedDisposable() private val component = JPanel(BorderLayout()) private val tempUserData = mutableMapOf() + private var disposed = false init { val loading = JBLoadingPanel(null, this) @@ -149,6 +151,10 @@ private class NbtFileEditor( CodeStyleManager.getInstance(project).reformat(psiFile, true) } + if (disposed) { + return + } + project.messageBus.connect(this).subscribe( AnActionListener.TOPIC, object : AnActionListener { @@ -216,7 +222,9 @@ private class NbtFileEditor( editor.exec { removePropertyChangeListener(listener) } } - override fun dispose() {} + override fun dispose() { + disposed = true + } override fun getStructureViewBuilder() = editor.exec { structureViewBuilder } override fun equals(other: Any?) = other is NbtFileEditor && other.component == this.component diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt index e316523a5..35d46a443 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt @@ -23,13 +23,14 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.reference.MixinSelectorParser import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT +import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass +import com.demonwav.mcdev.platform.mixin.util.findMethod import com.demonwav.mcdev.platform.mixin.util.shortName import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.internalName -import com.demonwav.mcdev.util.mapToArray import com.demonwav.mcdev.util.shortName import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder @@ -40,12 +41,10 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiModifier import com.intellij.psi.PsiNewExpression import com.intellij.psi.PsiSubstitutor import com.intellij.psi.util.parentOfType import org.objectweb.asm.Opcodes -import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodInsnNode @@ -109,18 +108,11 @@ class NewInsnInjectionPoint : InjectionPoint() { private val selector: MixinSelector, ) : NavigationVisitor() { override fun visitNewExpression(expression: PsiNewExpression) { - val anonymousName = expression.anonymousClass?.fullQualifiedName?.replace('.', '/') + val anonymousClass = expression.anonymousClass + val anonymousName = anonymousClass?.fullQualifiedName?.replace('.', '/') if (anonymousName != null) { - // guess descriptor - val hasThis = expression.parentOfType()?.hasModifierProperty(PsiModifier.STATIC) == false - val thisType = if (hasThis) expression.parentOfType()?.internalName else null - val argTypes = expression.argumentList?.expressionTypes?.map { it.descriptor } ?: emptyList() - val bytecodeArgTypes = if (thisType != null) listOf(thisType) + argTypes else argTypes - val methodDesc = Type.getMethodDescriptor( - Type.VOID_TYPE, - *bytecodeArgTypes.mapToArray { Type.getType(it) }, - ) - if (selector.matchMethod(anonymousName, "", methodDesc)) { + val method = findClassNodeByPsiClass(anonymousClass)?.findMethod(selector) + if (method != null && selector.matchMethod(anonymousName, method.name, method.desc)) { addResult(expression) } } else { diff --git a/src/main/kotlin/platform/mixin/insight/MixinLineMarkerProvider.kt b/src/main/kotlin/platform/mixin/insight/MixinLineMarkerProvider.kt index 60c4941a9..e8c844841 100644 --- a/src/main/kotlin/platform/mixin/insight/MixinLineMarkerProvider.kt +++ b/src/main/kotlin/platform/mixin/insight/MixinLineMarkerProvider.kt @@ -27,12 +27,12 @@ import com.demonwav.mcdev.platform.mixin.util.mixinTargets import com.intellij.codeInsight.daemon.GutterIconNavigationHandler import com.intellij.codeInsight.daemon.LineMarkerInfo import com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor -import com.intellij.codeInsight.daemon.impl.PsiElementListNavigator -import com.intellij.ide.util.PsiClassListCellRenderer +import com.intellij.codeInsight.navigation.getPsiElementPopup import com.intellij.openapi.editor.markup.GutterIconRenderer import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiIdentifier +import com.intellij.ui.awt.RelativePoint import java.awt.event.MouseEvent class MixinLineMarkerProvider : LineMarkerProviderDescriptor(), GutterIconNavigationHandler { @@ -67,13 +67,8 @@ class MixinLineMarkerProvider : LineMarkerProviderDescriptor(), GutterIconNaviga val targets = psiClass.mixinTargets .mapNotNull { it.findSourceClass(psiClass.project, psiClass.resolveScope, canDecompile = true) } if (targets.isNotEmpty()) { - PsiElementListNavigator.openTargets( - e, - targets.toTypedArray(), - "Choose target class of $name", - null, - PsiClassListCellRenderer(), - ) + getPsiElementPopup(targets.toTypedArray(), "Choose target class of $name") + .show(RelativePoint(e)) } } } diff --git a/src/main/kotlin/translations/TranslationEditorNotificationProvider.kt b/src/main/kotlin/translations/TranslationEditorNotificationProvider.kt index edebc26be..161058119 100644 --- a/src/main/kotlin/translations/TranslationEditorNotificationProvider.kt +++ b/src/main/kotlin/translations/TranslationEditorNotificationProvider.kt @@ -32,8 +32,9 @@ import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.MessageDialogBuilder import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiManager import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationProvider @@ -49,61 +50,67 @@ class TranslationEditorNotificationProvider : EditorNotificationProvider { override fun collectNotificationData( project: Project, file: VirtualFile, - ): Function = Function { createNotificationPanel(file, project) } - - private fun createNotificationPanel(file: VirtualFile, project: Project): InfoPanel? { - val locale = TranslationFiles.getLocale(file) - if (!show || !TranslationFiles.isTranslationFile(file) || locale == TranslationConstants.DEFAULT_LOCALE) { + ): Function? { + if (!show || !TranslationFiles.isTranslationFile(file) || TranslationFiles.isDefaultLocale(file)) { return null } val missingTranslations = getMissingTranslations(project, file) - if (missingTranslations.any()) { - val panel = InfoPanel() - panel.text = "Translation file doesn't match default one (${TranslationConstants.DEFAULT_LOCALE} locale)." - panel.createActionLabel( - "Add missing default entries (won't reflect changes in original English localization)", - ) { - val psi = PsiManager.getInstance(project).findFile(file) ?: return@createActionLabel - psi.applyWriteAction { - val fileEntries = missingTranslations.map { - TranslationFiles.FileEntry.Translation(it.key, it.text) - } - TranslationFiles.addAll(psi, fileEntries.asIterable()) - EditorNotifications.updateAll() - } + if (missingTranslations.none()) { + return null + } - if (psi.findMcpModule() == null) { - // TranslationSorter.query requires an MCP module to work - return@createActionLabel - } + val hasMcpModule = file.findPsiFile(project)?.findMcpModule() != null + return Function { + createNotificationPanel(missingTranslations, hasMcpModule, file, project) + } + } - val sort = Messages.showYesNoDialog( - project, - "Would you like to sort all translations now?", - "Sort Translations", - Messages.getQuestionIcon(), - ) - if (sort == Messages.YES) { - try { - TranslationSorter.query(project, psi, Ordering.LIKE_DEFAULT) - } catch (e: Exception) { - Notification( - "Translations sorting error", - "Error sorting translations", - e.message ?: e.stackTraceToString(), - NotificationType.WARNING, - ).notify(project) - } + private fun createNotificationPanel( + missingTranslations: Sequence, + hasMcpModule: Boolean, + file: VirtualFile, + project: Project + ): InfoPanel { + val panel = InfoPanel() + panel.text = "Translation file doesn't match default one (${TranslationConstants.DEFAULT_LOCALE} locale)." + panel.createActionLabel( + "Add missing default entries (won't reflect changes in original English localization)", + ) { + val psi = PsiManager.getInstance(project).findFile(file) ?: return@createActionLabel + psi.applyWriteAction { + val fileEntries = missingTranslations.map { + TranslationFiles.FileEntry.Translation(it.key, it.text) } + TranslationFiles.addAll(psi, fileEntries.asIterable()) + EditorNotifications.updateAll() + } + + if (!hasMcpModule) { + // TranslationSorter.query requires an MCP module to work + return@createActionLabel } - panel.createActionLabel("Hide notification") { - panel.isVisible = false - show = false + + val sort = MessageDialogBuilder.yesNo("Sort Translations", "Would you like to sort all translations now?") + .ask(project) + if (sort) { + try { + TranslationSorter.query(project, psi, true, Ordering.LIKE_DEFAULT) + } catch (e: Exception) { + Notification( + "Translations sorting error", + "Error sorting translations", + e.message ?: e.stackTraceToString(), + NotificationType.WARNING, + ).notify(project) + } } - return panel } - return null + panel.createActionLabel("Hide notification") { + panel.isVisible = false + show = false + } + return panel } private fun getMissingTranslations(project: Project, file: VirtualFile): Sequence { diff --git a/src/main/kotlin/translations/TranslationFiles.kt b/src/main/kotlin/translations/TranslationFiles.kt index 424daa287..edc914e02 100644 --- a/src/main/kotlin/translations/TranslationFiles.kt +++ b/src/main/kotlin/translations/TranslationFiles.kt @@ -68,6 +68,9 @@ object TranslationFiles { fun getLocale(file: VirtualFile?) = file?.nameWithoutExtension?.lowercase(Locale.ENGLISH) + fun isDefaultLocale(file: VirtualFile?) = + file?.nameWithoutExtension?.lowercase(Locale.ENGLISH) == TranslationConstants.DEFAULT_LOCALE + tailrec fun seekTranslation(element: PsiElement): PsiNamedElement? { // don't use elvis here, K2 doesn't think it's a tail recursive call if you do val res = toTranslation(element)?.let { element as? PsiNamedElement } diff --git a/src/main/kotlin/translations/actions/SortTranslationsAction.kt b/src/main/kotlin/translations/actions/SortTranslationsAction.kt index 564f8fa41..fe0bc6680 100644 --- a/src/main/kotlin/translations/actions/SortTranslationsAction.kt +++ b/src/main/kotlin/translations/actions/SortTranslationsAction.kt @@ -21,7 +21,9 @@ package com.demonwav.mcdev.translations.actions import com.demonwav.mcdev.translations.TranslationFiles +import com.demonwav.mcdev.translations.index.TranslationIndex import com.demonwav.mcdev.translations.sorting.TranslationSorter +import com.demonwav.mcdev.util.mcDomain import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.actionSystem.AnAction @@ -34,7 +36,11 @@ class SortTranslationsAction : AnAction() { val file = e.getData(LangDataKeys.PSI_FILE) ?: return try { - TranslationSorter.query(file.project, file) + TranslationSorter.query( + file.project, + file, + TranslationIndex.hasDefaultTranslations(file.project, file.virtualFile.mcDomain) + ) } catch (e: Exception) { Notification( "Translations sorting error", diff --git a/src/main/kotlin/translations/index/TranslationIndex.kt b/src/main/kotlin/translations/index/TranslationIndex.kt index d760c014b..8abffdd17 100644 --- a/src/main/kotlin/translations/index/TranslationIndex.kt +++ b/src/main/kotlin/translations/index/TranslationIndex.kt @@ -63,6 +63,17 @@ class TranslationIndex : FileBasedIndexExtension( fun getProjectDefaultTranslations(project: Project, domain: String? = null) = getProjectDefaultEntries(project, domain).flatten() + fun hasDefaultTranslations(project: Project, domain: String? = null): Boolean { + return !FileBasedIndex.getInstance() + .processValues( + NAME, + TranslationConstants.DEFAULT_LOCALE, + null, + { _, entry -> entry.sourceDomain != domain }, + GlobalSearchScope.projectScope(project) + ) + } + fun getTranslations(project: Project, file: VirtualFile): Sequence { return getEntries( GlobalSearchScope.fileScope(project, file), diff --git a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt index bbb821daf..927dc6a5c 100644 --- a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt +++ b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt @@ -65,6 +65,7 @@ class NoTranslationInspection : TranslationInspection() { val literalValue = literal.value as String val key = translation?.key?.copy(infix = literalValue)?.full ?: literalValue val result = Messages.showInputDialog( + project, "Enter default value for \"$key\":", "Create Translation", Messages.getQuestionIcon(), diff --git a/src/main/kotlin/translations/sorting/TranslationSorter.kt b/src/main/kotlin/translations/sorting/TranslationSorter.kt index c4f5bdc3a..4e3c7e0df 100644 --- a/src/main/kotlin/translations/sorting/TranslationSorter.kt +++ b/src/main/kotlin/translations/sorting/TranslationSorter.kt @@ -21,15 +21,17 @@ package com.demonwav.mcdev.translations.sorting import com.demonwav.mcdev.translations.Translation -import com.demonwav.mcdev.translations.TranslationConstants import com.demonwav.mcdev.translations.TranslationFiles import com.demonwav.mcdev.translations.actions.TranslationSortOrderDialog import com.demonwav.mcdev.translations.index.TranslationIndex +import com.demonwav.mcdev.util.applyWriteAction import com.demonwav.mcdev.util.lexicographical import com.demonwav.mcdev.util.mcDomain import com.demonwav.mcdev.util.runWriteAction import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleManager import java.util.TreeSet object TranslationSorter { @@ -39,18 +41,22 @@ object TranslationSorter { private val descendingComparator = ascendingComparator.reversed() - fun query(project: Project, file: PsiFile, defaultSelection: Ordering = Ordering.ASCENDING) { - val domain = file.virtualFile.mcDomain - val defaultEntries = TranslationIndex.getProjectDefaultTranslations(project, domain) - val noDefaults = defaultEntries.none() - val isDefaultFile = TranslationFiles.getLocale(file.virtualFile) == TranslationConstants.DEFAULT_LOCALE - val (order, comments) = TranslationSortOrderDialog.show(noDefaults || isDefaultFile, defaultSelection) + fun query( + project: Project, + file: PsiFile, + hasDefaultTranslations: Boolean, + defaultSelection: Ordering = Ordering.ASCENDING + ) { + val (order, comments) = TranslationSortOrderDialog.show( + hasDefaultTranslations || TranslationFiles.isDefaultLocale(file.virtualFile), + defaultSelection + ) if (order == null) { return } - sort(project, file, order, comments) + file.applyWriteAction { sort(project, file, order, comments) } } private fun sort(project: Project, file: PsiFile, ordering: Ordering, keepComments: Int) { @@ -91,6 +97,12 @@ object TranslationSorter { file.runWriteAction { TranslationFiles.replaceAll(file, sorted.asIterable()) + val documentManager = PsiDocumentManager.getInstance(project) + val document = documentManager.getDocument(file) + if (document != null) { + documentManager.commitDocument(document) + CodeStyleManager.getInstance(project).reformat(file, true) + } } }