diff --git a/src/main/kotlin/com/intellij/plugin/applescript/lang/resolve/AppleScriptDictionaryResolveProcessor.kt b/src/main/kotlin/com/intellij/plugin/applescript/lang/resolve/AppleScriptDictionaryResolveProcessor.kt index cfaccd6f..08be0b45 100644 --- a/src/main/kotlin/com/intellij/plugin/applescript/lang/resolve/AppleScriptDictionaryResolveProcessor.kt +++ b/src/main/kotlin/com/intellij/plugin/applescript/lang/resolve/AppleScriptDictionaryResolveProcessor.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.plugin.applescript.lang.dictionary.project.AppleScriptProjectDictionaryService import com.intellij.plugin.applescript.lang.sdef.ApplicationDictionary import com.intellij.plugin.applescript.lang.sdef.DictionaryComponent +import com.intellij.plugin.applescript.lang.sdef.DictionaryTermCatalog import com.intellij.plugin.applescript.psi.AppleScriptBuiltInClassIdentifier import com.intellij.plugin.applescript.psi.AppleScriptDictionaryClassIdentifierPlural import com.intellij.plugin.applescript.psi.AppleScriptDictionaryClassName @@ -182,20 +183,20 @@ class AppleScriptDictionaryResolveProcessor : AppleScriptPsiScopeProcessor { fun getFilteredResult(): List { val result = ArrayList() - var filterStdCocoaTerminologyFlag = false // true once we've collected from a non-additions dictionary + var shouldFilterCocoa = false val dictionaryRegistry = myProject.getService(AppleScriptProjectDictionaryService::class.java) for (collectedDictionary in collectedDictionaries) { - collectAllComponentsFromDictionary(collectedDictionary, result, filterStdCocoaTerminologyFlag) - filterStdCocoaTerminologyFlag = filterStdCocoaTerminologyFlag || + collectTerms(collectedDictionary, result, shouldFilterCocoa) + shouldFilterCocoa = shouldFilterCocoa || collectedDictionary.getName() != ApplicationDictionary.SCRIPTING_ADDITIONS_LIBRARY } if (dictionaryRegistry != null) { - appendResultsIfNeeded( + appendFallbackTerms( result, myProject, mySortedUseStatements.isNotEmpty(), collectedDictionaries.contains(dictionaryRegistry.getScriptingAdditionsTerminology()), - filterStdCocoaTerminologyFlag, + shouldFilterCocoa, ) } return result @@ -211,81 +212,43 @@ class AppleScriptDictionaryResolveProcessor : AppleScriptPsiScopeProcessor { private val LOG: Logger = Logger.getInstance("#${AppleScriptDictionaryResolveProcessor::class.java.name}") - private fun appendResultsIfNeeded( + private fun appendFallbackTerms( result: MutableCollection, project: Project, - areThereUseStatements: Boolean, - filterStandardAdditions: Boolean, - filterCocoaStandard: Boolean, + hasUseStatements: Boolean, + shouldFilterAdditions: Boolean, + shouldFilterCocoa: Boolean, ) { val dictionaryRegistry = project.getService(AppleScriptProjectDictionaryService::class.java) ?: return - if (!filterStandardAdditions && !areThereUseStatements) { + if (!shouldFilterAdditions && !hasUseStatements) { val scriptAdditions = dictionaryRegistry.getScriptingAdditionsTerminology() - collectAllComponentsFromDictionary(scriptAdditions, result, false) + collectTerms(scriptAdditions, result, false) } - if (!filterCocoaStandard) { + if (!shouldFilterCocoa) { val cocoaStandardTerminology = dictionaryRegistry.getCocoaStandardTerminology() - collectAllComponentsFromDictionary(cocoaStandardTerminology, result, false) + collectTerms(cocoaStandardTerminology, result, false) } } - /** - * Gather all term declarations from [importedDict]. - * - * @param withCocoaStdLibFiltering if true, omit terms already present in the Standard Terminology - */ - private fun collectAllComponentsFromDictionary( - importedDict: ApplicationDictionary?, + private fun collectTerms( + dictionary: ApplicationDictionary?, dictionaryComponents: MutableCollection, - withCocoaStdLibFiltering: Boolean, + shouldFilterCocoa: Boolean, ) { - if (importedDict == null) { - return - } - - if (withCocoaStdLibFiltering) { - collectComponentsMissingFromCocoa(importedDict, dictionaryComponents) + if (dictionary == null) return + + if (shouldFilterCocoa) { + val cocoaStandard = + dictionary.project + .getService(AppleScriptProjectDictionaryService::class.java) + ?.getCocoaStandardTerminology() + dictionaryComponents.addAll(DictionaryTermCatalog.missingTerms(dictionary, cocoaStandard)) } else { - collectAllComponents(importedDict, dictionaryComponents) + dictionaryComponents.addAll(DictionaryTermCatalog.allTerms(dictionary)) } } - - private fun collectComponentsMissingFromCocoa( - importedDict: ApplicationDictionary, - dictionaryComponents: MutableCollection, - ) { - val cocoaStandard = - importedDict.project - .getService(AppleScriptProjectDictionaryService::class.java) - ?.getCocoaStandardTerminology() - - if (cocoaStandard != null) { - importedDict.dictionaryEnumeratorMap.values.filterTo(dictionaryComponents) { - cocoaStandard.findEnumerator(it.getName()) == null - } - importedDict.dictionaryClassMap.values.filterTo(dictionaryComponents) { - cocoaStandard.findClass(it.getName()) == null - } - importedDict.allCommands.filterTo(dictionaryComponents) { - cocoaStandard.findCommand(it.getName()) == null - } - importedDict.dictionaryPropertyMap.values.filterTo(dictionaryComponents) { - cocoaStandard.findCommand(it.getName()) == null - } - } - } - - private fun collectAllComponents( - importedDict: ApplicationDictionary, - dictionaryComponents: MutableCollection, - ) { - dictionaryComponents.addAll(importedDict.dictionaryEnumeratorMap.values) - dictionaryComponents.addAll(importedDict.dictionaryPropertyMap.values) - dictionaryComponents.addAll(importedDict.dictionaryClassMap.values) - dictionaryComponents.addAll(importedDict.allCommands) - } } } diff --git a/src/main/kotlin/com/intellij/plugin/applescript/lang/sdef/DictionaryTermCatalog.kt b/src/main/kotlin/com/intellij/plugin/applescript/lang/sdef/DictionaryTermCatalog.kt new file mode 100644 index 00000000..0208dfc4 --- /dev/null +++ b/src/main/kotlin/com/intellij/plugin/applescript/lang/sdef/DictionaryTermCatalog.kt @@ -0,0 +1,32 @@ +package com.intellij.plugin.applescript.lang.sdef + +internal object DictionaryTermCatalog { + fun allTerms(dictionary: ApplicationDictionary): List = + buildList { + addAll(dictionary.dictionaryEnumeratorMap.values) + addAll(dictionary.dictionaryPropertyMap.values) + addAll(dictionary.dictionaryClassMap.values) + addAll(dictionary.allCommands) + } + + fun missingTerms( + dictionary: ApplicationDictionary, + baseline: ApplicationDictionary?, + ): List { + val availableBaseline = baseline ?: return emptyList() + return buildList { + dictionary.dictionaryEnumeratorMap.values.filterTo(this) { + availableBaseline.findEnumerator(it.getName()) == null + } + dictionary.dictionaryClassMap.values.filterTo(this) { + availableBaseline.findClass(it.getName()) == null + } + dictionary.allCommands.filterTo(this) { + availableBaseline.findCommand(it.getName()) == null + } + dictionary.dictionaryPropertyMap.values.filterTo(this) { + availableBaseline.findProperty(it.getName()) == null + } + } + } +} diff --git a/src/test/kotlin/com/intellij/plugin/applescript/test/sdef/DictionaryTermCatalogTest.kt b/src/test/kotlin/com/intellij/plugin/applescript/test/sdef/DictionaryTermCatalogTest.kt new file mode 100644 index 00000000..36304aee --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/applescript/test/sdef/DictionaryTermCatalogTest.kt @@ -0,0 +1,194 @@ +package com.intellij.plugin.applescript.test.sdef + +import com.intellij.plugin.applescript.AppleScriptFileType +import com.intellij.plugin.applescript.lang.dictionary.project.AppleScriptProjectDictionaryService +import com.intellij.plugin.applescript.lang.sdef.AppleScriptPropertyDefinition +import com.intellij.plugin.applescript.lang.sdef.ApplicationDictionary +import com.intellij.plugin.applescript.lang.sdef.DictionaryTermCatalog +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class DictionaryTermCatalogTest : BasePlatformTestCase() { + fun testPropertyMatchesByKind() { + val imported = buildTestDictionary(project, IMPORTED_TERMS, "Calendar Plus", "calendar-plus.sdef") + val cocoa = buildTestDictionary(project, COCOA_TERMS, "Standard Terminology", "cocoa-standard.sdef") + + val propertyNames = + DictionaryTermCatalog + .missingTerms(imported, cocoa) + .filterIsInstance() + .map { property -> property.getName() } + + assertEquals(listOf("shared label"), propertyNames) + } + + fun testAllTermsKeepOrder() { + val dictionary = buildTestDictionary(project, ORDERED_TERMS, "Calendar Plus", "ordered-terms.sdef") + + val termNames = DictionaryTermCatalog.allTerms(dictionary).map { term -> term.getName() } + + assertEquals(listOf("accepted", "summary", "event", "create event"), termNames) + } + + fun testFilteredOrder() { + val dictionary = buildTestDictionary(project, ORDERED_TERMS, "Calendar Plus", "ordered-terms.sdef") + val emptyBaseline = buildTestDictionary(project, EMPTY_TERMS, "Standard Terminology", "empty-cocoa.sdef") + + val termNames = DictionaryTermCatalog.missingTerms(dictionary, emptyBaseline).map { term -> term.getName() } + + assertEquals(listOf("accepted", "event", "create event", "summary"), termNames) + } + + fun testCompletionKeepsProperty() { + val projectDictionaries = project.getService(AppleScriptProjectDictionaryService::class.java) + try { + cacheCompletionDictionaries(projectDictionaries) + myFixture.configureByText( + AppleScriptFileType, + """ + use application "Calendar Primer" + use application "Calendar Plus" + set selectedValue to + """.trimIndent(), + ) + + myFixture.completeBasic() + val lookupStrings = requireNotNull(myFixture.lookupElementStrings) + + assertTrue("Imported property must remain available", lookupStrings.contains("shared label")) + assertFalse("Cocoa property must be filtered", lookupStrings.contains("standard label")) + } finally { + projectDictionaries.clearCachedDictionariesForTests() + } + } + + fun testCocoaRequired() { + val dictionary = buildTestDictionary(project, IMPORTED_TERMS, "Calendar Plus", "calendar-plus.sdef") + + val terms = DictionaryTermCatalog.missingTerms(dictionary, null) + + assertEmpty("Filtered terms require a Cocoa baseline", terms) + } + + fun testNoUseFallbacks() { + val projectDictionaries = project.getService(AppleScriptProjectDictionaryService::class.java) + + try { + projectDictionaries.clearCachedDictionariesForTests() + projectDictionaries.cacheDictionaryForTests( + ApplicationDictionary.SCRIPTING_ADDITIONS_LIBRARY, + buildTestDictionary( + project, + ADDITIONS_TERMS, + ApplicationDictionary.SCRIPTING_ADDITIONS_LIBRARY, + "additions.sdef", + ), + ) + projectDictionaries.cacheDictionaryForTests( + ApplicationDictionary.COCOA_STANDARD_LIBRARY, + buildTestDictionary( + project, + COCOA_TERMS, + ApplicationDictionary.COCOA_STANDARD_LIBRARY, + "cocoa.sdef", + ), + ) + myFixture.configureByText(AppleScriptFileType, "set selectedValue to ") + + myFixture.completeBasic() + val lookupStrings = requireNotNull(myFixture.lookupElementStrings) + + assertTrue( + "No-use completion must include Scripting Additions", + lookupStrings.contains("addition fallback"), + ) + assertTrue( + "No-use completion must include Cocoa terms", + lookupStrings.contains("standard label"), + ) + } finally { + projectDictionaries.clearCachedDictionariesForTests() + } + } + + private fun cacheCompletionDictionaries(projectDictionaries: AppleScriptProjectDictionaryService) { + projectDictionaries.clearCachedDictionariesForTests() + projectDictionaries.cacheDictionaryForTests( + "Calendar Primer", + buildTestDictionary(project, PRIMER_TERMS, "Calendar Primer", "calendar-primer.sdef"), + ) + projectDictionaries.cacheDictionaryForTests( + "Calendar Plus", + buildTestDictionary(project, IMPORTED_TERMS, "Calendar Plus", "calendar-plus.sdef"), + ) + projectDictionaries.cacheDictionaryForTests( + ApplicationDictionary.COCOA_STANDARD_LIBRARY, + buildTestDictionary(project, COCOA_TERMS, ApplicationDictionary.COCOA_STANDARD_LIBRARY, "cocoa.sdef"), + ) + } + + companion object { + private val IMPORTED_TERMS = + """ + + + + + + + + + """.trimIndent() + + private val PRIMER_TERMS = + """ + + + + + + """.trimIndent() + + private val COCOA_TERMS = + """ + + + + + + + + + """.trimIndent() + + private val ORDERED_TERMS = + """ + + + + + + + + + + + + """.trimIndent() + + private val EMPTY_TERMS = + """ + + + + """.trimIndent() + + private val ADDITIONS_TERMS = + """ + + + + + + """.trimIndent() + } +}